dd-trace 6.21.0 → 6.22.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 (204) hide show
  1. package/ci/diagnose.js +9 -14
  2. package/ci/init.js +1 -2
  3. package/ci/test-optimization-validation/ci-discovery.js +9 -4
  4. package/ci/test-optimization-validation/executable.js +1 -5
  5. package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
  6. package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
  7. package/ci/test-optimization-validation/generated-files.js +6 -4
  8. package/ci/test-optimization-validation/generated-test-contract.js +7 -3
  9. package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
  10. package/ci/test-optimization-validation/manifest-schema.js +6 -7
  11. package/ci/test-optimization-validation/report-writer.js +6 -2
  12. package/ci/test-optimization-validation/runner-command.js +2 -4
  13. package/ci/test-optimization-validation/runner-contract.js +1 -2
  14. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
  15. package/ci/vitest-no-worker-init-setup.mjs +10 -7
  16. package/index.d.ts +25 -14
  17. package/initialize.mjs +1 -3
  18. package/openfeature/rules-based.d.ts +11 -0
  19. package/openfeature/rules-based.js +7 -0
  20. package/package.json +12 -10
  21. package/packages/datadog-core/src/storage.js +16 -0
  22. package/packages/datadog-esbuild/index.js +18 -12
  23. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  24. package/packages/datadog-instrumentations/src/ai.js +3 -3
  25. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  26. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  27. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  28. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  29. package/packages/datadog-instrumentations/src/cucumber.js +48 -33
  30. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  31. package/packages/datadog-instrumentations/src/electron.js +6 -6
  32. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  33. package/packages/datadog-instrumentations/src/fs.js +2 -2
  34. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  35. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  36. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
  37. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  38. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  39. package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
  40. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  41. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
  42. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +7 -7
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  47. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  48. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  49. package/packages/datadog-instrumentations/src/jest.js +3 -10
  50. package/packages/datadog-instrumentations/src/knex.js +9 -5
  51. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  52. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  53. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  54. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -2
  55. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -2
  56. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  57. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  58. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  59. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  60. package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
  61. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  62. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  63. package/packages/datadog-instrumentations/src/openai.js +7 -0
  64. package/packages/datadog-instrumentations/src/pino.js +5 -6
  65. package/packages/datadog-instrumentations/src/redis.js +27 -13
  66. package/packages/datadog-instrumentations/src/router.js +30 -0
  67. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  68. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
  69. package/packages/datadog-instrumentations/src/vitest-main.js +9 -15
  70. package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
  71. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  72. package/packages/datadog-instrumentations/src/vitest.js +2 -2
  73. package/packages/datadog-instrumentations/src/webdriverio.js +5 -8
  74. package/packages/datadog-instrumentations/src/ws.js +1 -4
  75. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  76. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  77. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  78. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  79. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  80. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  82. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  83. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  84. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  85. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  86. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +1 -5
  87. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  88. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  89. package/packages/datadog-plugin-cypress/src/support.js +1 -4
  90. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  91. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  92. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  93. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  94. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  95. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  96. package/packages/datadog-plugin-mocha/src/index.js +7 -10
  97. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +8 -4
  98. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  99. package/packages/datadog-plugin-next/src/index.js +1 -2
  100. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  101. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  102. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  103. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  104. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  105. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  106. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  107. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  108. package/packages/datadog-webpack/index.js +55 -43
  109. package/packages/datadog-webpack/src/loader.js +56 -8
  110. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  111. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  112. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  113. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  114. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  115. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  116. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  117. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  118. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  119. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  120. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  121. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  122. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  123. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  124. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  125. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  126. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  127. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  128. package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
  129. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  132. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  133. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  134. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  135. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  136. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  137. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  138. package/packages/dd-trace/src/config/defaults.js +5 -5
  139. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
  140. package/packages/dd-trace/src/config/helper.js +34 -3
  141. package/packages/dd-trace/src/config/index.js +10 -9
  142. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  143. package/packages/dd-trace/src/config/parsers.js +2 -2
  144. package/packages/dd-trace/src/config/supported-configurations.json +35 -0
  145. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  146. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  147. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -0
  148. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  149. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  150. package/packages/dd-trace/src/dogstatsd.js +1 -1
  151. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  152. package/packages/dd-trace/src/exporters/common/request.js +1 -5
  153. package/packages/dd-trace/src/id.js +14 -1
  154. package/packages/dd-trace/src/lambda/handler.js +179 -19
  155. package/packages/dd-trace/src/lambda/index.js +15 -1
  156. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  157. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +12 -0
  159. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  160. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  161. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  162. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  163. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  165. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
  166. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  167. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  168. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  169. package/packages/dd-trace/src/llmobs/span_processor.js +1 -2
  170. package/packages/dd-trace/src/llmobs/tagger.js +2 -2
  171. package/packages/dd-trace/src/llmobs/util.js +1 -2
  172. package/packages/dd-trace/src/llmobs/writers/base.js +4 -5
  173. package/packages/dd-trace/src/log/log.js +1 -5
  174. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  175. package/packages/dd-trace/src/openfeature/flagging_provider.js +48 -21
  176. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  177. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  179. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  180. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
  181. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  182. package/packages/dd-trace/src/otel-sampling.js +4 -5
  183. package/packages/dd-trace/src/otel-thread-ctx.js +79 -13
  184. package/packages/dd-trace/src/plugins/database.js +7 -16
  185. package/packages/dd-trace/src/plugins/util/git.js +9 -25
  186. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  187. package/packages/dd-trace/src/plugins/util/test.js +48 -4
  188. package/packages/dd-trace/src/priority_sampler.js +1 -2
  189. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  190. package/packages/dd-trace/src/proxy.js +1 -2
  191. package/packages/dd-trace/src/ritm.js +4 -2
  192. package/packages/dd-trace/src/serverless.js +5 -0
  193. package/packages/dd-trace/src/span_processor.js +13 -17
  194. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  195. package/packages/dd-trace/src/util.js +29 -0
  196. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  197. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  198. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  199. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  200. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  201. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  202. package/vendor/dist/protobufjs/index.js +1 -1
  203. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  204. package/vendor/dist/shell-quote/index.js +1 -1
@@ -8,7 +8,7 @@ const {
8
8
  } = require('../../constants/tags')
9
9
  const { audioMimeTypeFromFormat, formatAudioPart } = require('../../audio-utils')
10
10
  const { safeJsonParse } = require('../../util')
11
- const { AUDIO_MIME_TYPES } = require('./constants')
11
+ const { AUDIO_MIME_TYPES, IMAGE_FALLBACK, INPUT_TYPE_IMAGE } = require('./constants')
12
12
  const {
13
13
  extractChatTemplateFromInstructions,
14
14
  normalizePromptVariables,
@@ -16,6 +16,7 @@ const {
16
16
  extractContentParts,
17
17
  hasMultimodalInputs,
18
18
  getModelProviderAndClient,
19
+ getServerToolUsageMetrics,
19
20
  } = require('./utils')
20
21
 
21
22
  const allowedParamKeys = new Set([
@@ -107,6 +108,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
107
108
  this._tagEmbedding(span, inputs, response, error)
108
109
  } else if (operation === 'response') {
109
110
  this.#tagResponse(span, inputs, response, error)
111
+ } else if (operation === 'decision') {
112
+ this.#tagDecision(span, inputs, response, error)
110
113
  }
111
114
 
112
115
  if (!error) {
@@ -148,12 +151,20 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
148
151
  // Responses API - always include
149
152
  const cacheReadTokens = tokenUsage.input_tokens_details.cached_tokens
150
153
  if (cacheReadTokens !== undefined) metrics.cacheReadTokens = cacheReadTokens
154
+ const cacheWriteTokens = tokenUsage.input_tokens_details.cache_write_tokens
155
+ if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
156
+ // Cache writes are billed at a different rate per TTL
157
+ const cacheWriteDetails = tokenUsage.input_tokens_details.cache_write_tokens_details
158
+ if (cacheWriteDetails?.ttl_30m_tokens != null) metrics.cacheWrite30mTokens = cacheWriteDetails.ttl_30m_tokens
159
+ if (cacheWriteDetails?.ttl_12h_tokens != null) metrics.cacheWrite12hTokens = cacheWriteDetails.ttl_12h_tokens
151
160
  } else if (tokenUsage.prompt_tokens_details) {
152
161
  // Chat/Completions API - only include if > 0
153
162
  const cacheReadTokens = tokenUsage.prompt_tokens_details.cached_tokens
154
163
  if (cacheReadTokens != null) {
155
164
  metrics.cacheReadTokens = cacheReadTokens
156
165
  }
166
+ const cacheWriteTokens = tokenUsage.prompt_tokens_details.cache_write_tokens
167
+ if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
157
168
  }
158
169
  // Reasoning tokens - Responses API returns `output_tokens_details`, `completion_tokens_details`
159
170
  const reasoningOutputObject = tokenUsage.output_tokens_details ?? tokenUsage.completion_tokens_details
@@ -161,6 +172,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
161
172
  if (reasoningOutputTokens !== undefined) metrics.reasoningOutputTokens = reasoningOutputTokens
162
173
  }
163
174
 
175
+ Object.assign(metrics, getServerToolUsageMetrics(response))
176
+
164
177
  return metrics
165
178
  }
166
179
 
@@ -464,6 +477,30 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
464
477
 
465
478
  this._tagger.tagMetadata(span, outputMetadata) // update the metadata with the output metadata
466
479
  }
480
+
481
+ #tagDecision (span, inputs, response, error) {
482
+ const { input, questions } = inputs
483
+
484
+ const inputMessages = typeof input === 'string'
485
+ ? [{ role: 'user', content: input }]
486
+ : (Array.isArray(input) ? input : []).map(message => ({
487
+ role: message.role ?? 'user',
488
+ content: decisionMessageContent(message.content),
489
+ }))
490
+
491
+ if (Array.isArray(questions)) {
492
+ this._tagger.tagMetadata(span, { questions })
493
+ }
494
+
495
+ if (error) {
496
+ this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
497
+ return
498
+ }
499
+
500
+ this._tagger.tagLLMIO(span, inputMessages, [
501
+ { role: 'assistant', content: JSON.stringify(response.answers ?? []) },
502
+ ])
503
+ }
467
504
  }
468
505
 
469
506
  // TODO: this will be moved to the APM integration
@@ -485,13 +522,28 @@ function normalizeOpenAIResourceName (resource) {
485
522
  case 'responses.create':
486
523
  return 'createResponse'
487
524
 
525
+ // decisions
526
+ case 'decisions.create':
527
+ return 'createDecision'
528
+
488
529
  default:
489
530
  return resource
490
531
  }
491
532
  }
492
533
 
534
+ // Decision images must be inline data URLs, so they are replaced with a marker instead of copied onto the span
535
+ function decisionMessageContent (content) {
536
+ if (!Array.isArray(content)) return content ?? ''
537
+
538
+ return content
539
+ .map(part => (part?.type === INPUT_TYPE_IMAGE ? IMAGE_FALLBACK : part?.text ?? ''))
540
+ .filter(Boolean)
541
+ .join('\n')
542
+ }
543
+
493
544
  function gateResource (resource) {
494
- return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse'].includes(resource)
545
+ return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse', 'createDecision']
546
+ .includes(resource)
495
547
  ? resource
496
548
  : undefined
497
549
  }
@@ -506,6 +558,8 @@ function getOperation (resource) {
506
558
  return 'embedding'
507
559
  case 'createResponse':
508
560
  return 'response'
561
+ case 'createDecision':
562
+ return 'decision'
509
563
  default:
510
564
  // should never happen
511
565
  return 'unknown'
@@ -198,6 +198,43 @@ function getModelProviderAndClient (baseUrl = '') {
198
198
  return { modelProvider, client: 'OpenAI' }
199
199
  }
200
200
 
201
+ /**
202
+ * Count the billable server-side tool calls in an OpenAI Responses response.
203
+ *
204
+ * Web searches are read from the undocumented `tool_usage.web_search.num_requests` when present, otherwise
205
+ * counted from completed `web_search_call` items whose action is `search` (`open_page` / `find_in_page` steps are
206
+ * not billed). File searches have no usage counter, so each completed `file_search_call` item counts once.
207
+ * Zero counts are omitted.
208
+ *
209
+ * @param {Record<string, unknown>} [response] - Any OpenAI response; only Responses API fields are read
210
+ * @returns {{ webSearchCount?: number, storageSearchCount?: number }}
211
+ */
212
+ function getServerToolUsageMetrics (response) {
213
+ const metrics = {}
214
+ const reportedWebSearches = response?.tool_usage?.web_search?.num_requests
215
+ const hasReportedWebSearches = typeof reportedWebSearches === 'number'
216
+ if (hasReportedWebSearches && reportedWebSearches > 0) metrics.webSearchCount = reportedWebSearches
217
+
218
+ const output = response?.output
219
+ if (!Array.isArray(output)) return metrics
220
+
221
+ for (const item of output) {
222
+ if (item?.status !== 'completed') continue
223
+
224
+ if (item.type === 'web_search_call') {
225
+ if (hasReportedWebSearches) continue
226
+ // Items without an action (older payloads) are assumed to be searches.
227
+ const actionType = item.action?.type
228
+ if (actionType !== undefined && actionType !== 'search') continue
229
+ metrics.webSearchCount = (metrics.webSearchCount ?? 0) + 1
230
+ } else if (item.type === 'file_search_call') {
231
+ metrics.storageSearchCount = (metrics.storageSearchCount ?? 0) + 1
232
+ }
233
+ }
234
+
235
+ return metrics
236
+ }
237
+
201
238
  module.exports = {
202
239
  extractChatTemplateFromInstructions,
203
240
  normalizePromptVariables,
@@ -206,4 +243,5 @@ module.exports = {
206
243
  hasMultimodalInputs,
207
244
  getModelProviderAndClient,
208
245
  getOpenAIModelProvider,
246
+ getServerToolUsageMetrics,
209
247
  }
@@ -84,13 +84,11 @@ function promptFromData (data, source) {
84
84
  }
85
85
 
86
86
  /**
87
- * Copy a prompt with a different source.
88
87
  * @param {ManagedPrompt} prompt
89
- * @param {'registry'|'cache'|'fallback'|'ff'|'resolve'} source
90
88
  */
91
- function withSource (prompt, source) {
92
- if (source === prompt.source) return prompt
93
- return new ManagedPrompt({ ...prompt, source })
89
+ function withCacheSource (prompt) {
90
+ if (SOURCE_CACHE === prompt.source) return prompt
91
+ return new ManagedPrompt({ ...prompt, source: /** @type {ManagedPrompt['source']} */ (SOURCE_CACHE) })
94
92
  }
95
93
 
96
94
  function detailFromBody (body) {
@@ -310,7 +308,7 @@ class PromptManager {
310
308
  const cacheable = generationAtStart === this.cacheGeneration && latest
311
309
  if (result.prompt) {
312
310
  if (cacheable) {
313
- const cached = withSource(result.prompt, SOURCE_CACHE)
311
+ const cached = withCacheSource(result.prompt)
314
312
  if (hot) this.hotCache.set(request.key, cached)
315
313
  if (!request.resolve) this.warmCache.set(request.key, cached)
316
314
  }
@@ -335,7 +333,7 @@ class PromptManager {
335
333
  async #backgroundFetch (request, signal) {
336
334
  const result = await this.#fetchAndCache(request, { hot: false, signal })
337
335
  if (!result.cacheable) return
338
- if (result.prompt) return withSource(result.prompt, SOURCE_CACHE)
336
+ if (result.prompt) return withCacheSource(result.prompt)
339
337
  if (result.notFound) return
340
338
  throw new Error(result.reason)
341
339
  }
@@ -93,7 +93,7 @@ class ManagedPrompt {
93
93
  format (variables = {}) {
94
94
  if (typeof this.template === 'string') return render(this.template, variables)
95
95
  return this.template.flatMap(item => {
96
- if (!isPlaceholder(item)) return [{ ...item, content: render(item.content, variables) }]
96
+ if (!isPlaceholder(item)) return { ...item, content: render(item.content, variables) }
97
97
  if (!Object.hasOwn(variables, item.name)) {
98
98
  throw new TypeError(`Missing message placeholder variable '${item.name}'`)
99
99
  }
@@ -92,9 +92,8 @@ class LLMObsSpanProcessor {
92
92
 
93
93
  // TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
94
94
  process (span) {
95
- if (!this.#config.llmobs.DD_LLMOBS_ENABLED) return
96
95
  // if the span is not in our private tagger map, it is not an llmobs span
97
- if (!LLMObsTagger.tagMap.has(span)) return
96
+ if (!this.#config.llmobs.DD_LLMOBS_ENABLED || !LLMObsTagger.tagMap.has(span)) return
98
97
 
99
98
  try {
100
99
  this.#setGenAiApmTags(span)
@@ -118,8 +118,8 @@ class LLMObsTagger {
118
118
  integration,
119
119
  _decorator,
120
120
  } = {}) {
121
- if (!this.#config.llmobs.DD_LLMOBS_ENABLED) return
122
- if (!kind) return // do not register it in the map if it doesn't have an llmobs span kind
121
+ // Do not register it in the map if it does not have an LLMObs span kind.
122
+ if (!this.#config.llmobs.DD_LLMOBS_ENABLED || !kind) return
123
123
 
124
124
  const spanMlApp =
125
125
  mlApp ||
@@ -260,8 +260,7 @@ function findArgumentsBounds (str) {
260
260
 
261
261
  const memo = new WeakMap()
262
262
  function getFunctionArguments (fn, args = []) {
263
- if (!fn) return
264
- if (!args.length) return
263
+ if (!fn || !args.length) return
265
264
  if (args.length === 1) return args[0]
266
265
 
267
266
  try {
@@ -23,13 +23,12 @@ class LLMObsBuffer {
23
23
  /**
24
24
  * @param {{
25
25
  * events: Record<string, unknown>[],
26
- * size: number,
27
26
  * routing?: { apiKey?: string, site?: string }
28
27
  * }} options
29
28
  */
30
- constructor ({ events, size, routing = {} }) {
29
+ constructor ({ events, routing = {} }) {
31
30
  this.events = events
32
- this.size = size
31
+ this.size = 0
33
32
  this.routing = routing
34
33
  }
35
34
 
@@ -53,7 +52,7 @@ class BaseLLMObsWriter {
53
52
  this._eventType = eventType
54
53
 
55
54
  /** @type {LLMObsBuffer} */
56
- this._buffer = new LLMObsBuffer({ events: [], size: 0 })
55
+ this._buffer = new LLMObsBuffer({ events: [] })
57
56
 
58
57
  /** @type {import('../../config/config-base')} */
59
58
  this._config = config
@@ -92,7 +91,7 @@ class BaseLLMObsWriter {
92
91
  const apiKey = routing.apiKey
93
92
  let buffer = this.#multiTenantBuffers.get(apiKey)
94
93
  if (!buffer) {
95
- buffer = new LLMObsBuffer({ events: [], size: 0, routing })
94
+ buffer = new LLMObsBuffer({ events: [], routing })
96
95
  this.#multiTenantBuffers.set(apiKey, buffer)
97
96
  }
98
97
  return buffer
@@ -17,11 +17,7 @@ class Log {
17
17
  get formatted () {
18
18
  const { message, args } = this
19
19
 
20
- let formatted = message
21
- if (message && args && args.length) {
22
- formatted = format(message, ...args)
23
- }
24
- return formatted
20
+ return message && args && args.length ? format(message, ...args) : message
25
21
  }
26
22
 
27
23
  static parse (...args) {
@@ -0,0 +1,51 @@
1
+ 'use strict'
2
+
3
+ const PARAMETER = 'dd_evaluation_telemetry_opt_out'
4
+
5
+ /** Gates an owned finally-only telemetry hook without changing evaluation or exposure handling. */
6
+ class EvaluationTelemetryOptOutHook {
7
+ #delegate
8
+ #decisions = new WeakMap()
9
+
10
+ /** @param {import('@openfeature/server-sdk').Hook} delegate */
11
+ constructor (delegate) {
12
+ this.#delegate = delegate
13
+ }
14
+
15
+ /** @param {import('@openfeature/core').HookContext} hookContext */
16
+ before (hookContext) {
17
+ // Hook context identity is stable across stages, including each MultiProvider branch.
18
+ this.#decisions.set(hookContext, isOptedOut(hookContext.context))
19
+ }
20
+
21
+ /** @param {import('@openfeature/core').HookContext} hookContext */
22
+ error (hookContext) {
23
+ // A customer before hook can fail before this hook runs. Capture opt-out before customer error hooks change it.
24
+ if (!this.#decisions.has(hookContext)) {
25
+ this.#decisions.set(hookContext, isOptedOut(hookContext.context))
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @param {import('@openfeature/core').HookContext} hookContext
31
+ * @param {import('@openfeature/core').EvaluationDetails<import('@openfeature/core').FlagValue>} evaluationDetails
32
+ * @param {import('@openfeature/core').HookHints} [hints]
33
+ */
34
+ finally (hookContext, evaluationDetails, hints) {
35
+ const optedOut = this.#decisions.get(hookContext) === true || isOptedOut(hookContext.context)
36
+ this.#decisions.delete(hookContext)
37
+ if (!optedOut) return this.#delegate.finally?.(hookContext, evaluationDetails, hints)
38
+ }
39
+ }
40
+
41
+ /** @param {import('@openfeature/core').EvaluationContext} context */
42
+ function isOptedOut (context) {
43
+ try {
44
+ return context?.[PARAMETER] === true
45
+ } catch {
46
+ // An unreadable attribute is not boolean true and must not change the evaluation result.
47
+ return false
48
+ }
49
+ }
50
+
51
+ module.exports = EvaluationTelemetryOptOutHook
@@ -9,6 +9,7 @@ const configurationSource = require('./configuration_source')
9
9
  const { EXPOSURE_CHANNEL } = require('./constants/constants')
10
10
  const DebugLoggingHook = require('./debug-logging-hook')
11
11
  const EvalMetricsHook = require('./eval-metrics-hook')
12
+ const EvaluationTelemetryOptOutHook = require('./evaluation-telemetry-opt-out-hook')
12
13
  const SpanEnrichmentHook = require('./span-enrichment-hook')
13
14
  const FlagEvalEVPHook = require('./writers/flag-eval-evp-hook')
14
15
 
@@ -17,6 +18,9 @@ const FlagEvalEVPHook = require('./writers/flag-eval-evp-hook')
17
18
  * Extends DatadogNodeServerProvider to add tracer integration and configuration management.
18
19
  */
19
20
  class FlaggingProvider extends DatadogNodeServerProvider {
21
+ // Track this wrapper's logging lifecycle without reading the upstream provider's private state.
22
+ #waitingForConfiguration = false
23
+
20
24
  /** @type {SpanEnrichmentHook | undefined} */
21
25
  #spanEnrichmentHook
22
26
 
@@ -36,8 +40,13 @@ class FlaggingProvider extends DatadogNodeServerProvider {
36
40
  initializationTimeoutMs: config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS,
37
41
  })
38
42
 
43
+ const enableOptOut =
44
+ config.featureFlags.DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED === true
45
+ /** @param {import('@openfeature/server-sdk').Hook} hook */
46
+ const telemetryHook = hook => enableOptOut ? new EvaluationTelemetryOptOutHook(hook) : hook
47
+
39
48
  if (config.DD_METRICS_OTEL_ENABLED === true) {
40
- this.hooks.push(new EvalMetricsHook(config))
49
+ this.hooks.push(telemetryHook(new EvalMetricsHook(config)))
41
50
  } else {
42
51
  log.debug('Feature Flags: evaluation metrics disabled; set %s=true to enable', 'DD_METRICS_OTEL_ENABLED')
43
52
  }
@@ -49,7 +58,7 @@ class FlaggingProvider extends DatadogNodeServerProvider {
49
58
  if (config.featureFlags.DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED) {
50
59
  this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
51
60
  // @ts-expect-error The upstream constructor always initializes its optional hooks property.
52
- this.hooks.push(this.#spanEnrichmentHook)
61
+ this.hooks.push(telemetryHook(this.#spanEnrichmentHook))
53
62
  log.info('%s span enrichment enabled', this.constructor.name)
54
63
  } else {
55
64
  log.info('%s span enrichment disabled', this.constructor.name)
@@ -60,7 +69,7 @@ class FlaggingProvider extends DatadogNodeServerProvider {
60
69
 
61
70
  if (config.featureFlags?.DD_FLAGGING_EVALUATION_COUNTS_ENABLED !== false) {
62
71
  this.#flagEvalEVPHook = new FlagEvalEVPHook(config)
63
- this.hooks.push(this.#flagEvalEVPHook)
72
+ this.hooks.push(telemetryHook(this.#flagEvalEVPHook))
64
73
  }
65
74
 
66
75
  this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
@@ -74,6 +83,7 @@ class FlaggingProvider extends DatadogNodeServerProvider {
74
83
  initialize (context) {
75
84
  log.debug('Feature Flags: waiting for provider initialization...')
76
85
 
86
+ this.#waitingForConfiguration = !this.getConfiguration()
77
87
  const promise = super.initialize(context)
78
88
 
79
89
  // `DatadogNodeServerProvider#initialize` starts a timer that is never unref'd, which would
@@ -82,28 +92,45 @@ class FlaggingProvider extends DatadogNodeServerProvider {
82
92
  // TODO: remove once `@datadog/openfeature-node-server` unrefs this timer itself.
83
93
  this.initController?.timeoutId?.unref?.()
84
94
 
85
- // Only observes the outcome for logging; `promise` itself is returned unmodified below.
86
- // Guarded so a logger failure can never leave the derived promise unhandled.
87
- promise.then(
88
- () => {
89
- try {
90
- log.debug('Feature Flags: provider initialized successfully')
91
- } catch { /* logging failure must not crash the process */ }
92
- },
93
- (error) => {
94
- try {
95
- // errorWithoutTelemetry avoids inflating telemetry volume for every routine init timeout.
96
- log.errorWithoutTelemetry(
97
- 'Feature Flags: provider failed to initialize: %s',
98
- error instanceof Error ? error.message : String(error)
99
- )
100
- } catch { /* logging failure must not crash the process */ }
101
- }
102
- )
95
+ if (this.getConfiguration()) this.#logInitialConfiguration()
103
96
 
104
97
  return promise
105
98
  }
106
99
 
100
+ /**
101
+ * @param {Parameters<DatadogNodeServerProvider['setConfiguration']>[0]} configuration
102
+ */
103
+ setConfiguration (configuration) {
104
+ const wasWaiting = this.#waitingForConfiguration
105
+ super.setConfiguration(configuration)
106
+ this.#waitingForConfiguration = false
107
+ if (wasWaiting && configuration != null) this.#logInitialConfiguration()
108
+ }
109
+
110
+ /**
111
+ * @param {unknown} error
112
+ */
113
+ setError (error) {
114
+ const wasWaiting = this.#waitingForConfiguration
115
+ super.setError(error)
116
+ this.#waitingForConfiguration = false
117
+ if (!wasWaiting) return
118
+
119
+ try {
120
+ // Avoid inflating telemetry volume for routine initialization timeouts.
121
+ log.errorWithoutTelemetry(
122
+ 'Feature Flags: provider failed to initialize: %s',
123
+ error instanceof Error ? error.message : String(error)
124
+ )
125
+ } catch { /* logging failure must not affect initialization */ }
126
+ }
127
+
128
+ #logInitialConfiguration () {
129
+ try {
130
+ log.debug('Feature Flags: initial configuration available')
131
+ } catch { /* logging failure must not affect initialization */ }
132
+ }
133
+
107
134
  /**
108
135
  * Called when the provider is shut down.
109
136
  * Cleans up resources including channel subscriptions.
@@ -52,7 +52,7 @@ function encodeHrTime (hrTime) {
52
52
  * OtlpTransformer transforms log records to OTLP format.
53
53
  *
54
54
  * This implementation follows the OTLP Logs v1.7.0 Data Model specification:
55
- * https://opentelemetry.io/docs/specs/otlp/#log-data-model
55
+ * https://github.com/open-telemetry/opentelemetry-proto/blob/v1.7.0/opentelemetry/proto/logs/v1/logs.proto
56
56
  *
57
57
  * @class OtlpTransformer
58
58
  * @augments OtlpTransformerBase
@@ -18,7 +18,7 @@ const AGGREGATION_TEMPORALITY_CUMULATIVE = protoAggregationTemporality.values.AG
18
18
  * OtlpTransformer transforms metrics to OTLP format.
19
19
  *
20
20
  * This implementation follows the OTLP Metrics v1.7.0 Data Model specification:
21
- * https://opentelemetry.io/docs/specs/otlp/#metrics-data-model
21
+ * https://github.com/open-telemetry/opentelemetry-proto/blob/v1.7.0/opentelemetry/proto/metrics/v1/metrics.proto
22
22
  *
23
23
  * @class OtlpTransformer
24
24
  * @augments OtlpTransformerBase
@@ -162,8 +162,7 @@ class PeriodicMetricReader {
162
162
  addBatchObservableCallback (callback, observables) {
163
163
  if (typeof callback !== 'function') return
164
164
  const instruments = new Set(observables?.filter(isObservableInstrument))
165
- if (instruments.size === 0) return
166
- if (this.#findBatchCallback(callback, instruments) !== -1) return
165
+ if (instruments.size === 0 || this.#findBatchCallback(callback, instruments) !== -1) return
167
166
  this.#batchCallbacks.push({ callback, instruments })
168
167
  }
169
168
 
@@ -1,4 +1,4 @@
1
- // Vendored from: https://github.com/open-telemetry/opentelemetry-proto/blob/v1.7.0/opentelemetry/proto/logs/v1/common.proto
1
+ // Vendored from: https://github.com/open-telemetry/opentelemetry-proto/blob/v1.7.0/opentelemetry/proto/common/v1/common.proto
2
2
  // Copyright 2019, OpenTelemetry Authors
3
3
  //
4
4
  // Licensed under the Apache License, Version 2.0 (the "License");
@@ -84,7 +84,7 @@ const DD_ERROR_META_KEYS = new Set(['error.message'])
84
84
  * OtlpTraceTransformer transforms DD-formatted spans to OTLP trace JSON format.
85
85
  *
86
86
  * This implementation follows the OTLP trace data model:
87
- * https://opentelemetry.io/docs/specs/otlp/#trace-data-model
87
+ * https://github.com/open-telemetry/opentelemetry-proto/blob/v1.7.0/opentelemetry/proto/trace/v1/trace.proto
88
88
  *
89
89
  * It receives DD-formatted spans (from span_format.js) and produces
90
90
  * an ExportTraceServiceRequest serialized as JSON (http/json protocol only).
@@ -168,8 +168,8 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
168
168
 
169
169
  return {
170
170
  traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
171
- spanId: this.#idToBytes(span.span_id, 8),
172
- parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
171
+ spanId: this.#spanIdToHex(span.span_id),
172
+ parentSpanId: parentId && !parentId.equals(ZERO_ID) ? this.#spanIdToHex(parentId) : undefined,
173
173
  traceState: span.trace_state,
174
174
  name: span.resource,
175
175
  kind: this.#mapSpanKind(span.meta?.['span.kind']),
@@ -338,26 +338,9 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
338
338
  }
339
339
  }
340
340
 
341
- /**
342
- * Converts a DD Identifier object to a hex-encoded string of the specified byte length.
343
- * Pads with leading zeros if the identifier buffer is shorter than the target.
344
- * Per the OTLP http/json spec, trace-ids and span-ids must be hex-encoded strings.
345
- *
346
- * @param {Identifier} identifier - DD Identifier
347
- * @param {number} targetLength - Target byte length (16 for trace ID, 8 for span ID)
348
- */
349
- #idToBytes (identifier, targetLength) {
350
- const buffer = identifier.toBuffer()
351
- if (buffer.length === targetLength) {
352
- return Buffer.from(buffer).toString('hex')
353
- }
354
- if (buffer.length > targetLength) {
355
- return Buffer.from(buffer.slice(buffer.length - targetLength)).toString('hex')
356
- }
357
- // Pad with leading zeros to reach target length.
358
- const result = Buffer.alloc(targetLength)
359
- Buffer.from(buffer).copy(result, targetLength - buffer.length)
360
- return result.toString('hex')
341
+ /** @param {Identifier} identifier */
342
+ #spanIdToHex (identifier) {
343
+ return identifier.toString(16).slice(-16)
361
344
  }
362
345
 
363
346
  /**