dd-trace 6.8.0 → 6.10.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 (209) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +140 -42
  44. package/index.d.ts +140 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +8 -5
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  55. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  56. package/packages/datadog-instrumentations/src/dns.js +21 -2
  57. package/packages/datadog-instrumentations/src/electron.js +1 -0
  58. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  59. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  60. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  61. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  62. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +629 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +199 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +62 -0
  68. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  69. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  70. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  71. package/packages/datadog-instrumentations/src/jest.js +980 -234
  72. package/packages/datadog-instrumentations/src/mocha/main.js +108 -10
  73. package/packages/datadog-instrumentations/src/mocha/utils.js +66 -50
  74. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  75. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  76. package/packages/datadog-instrumentations/src/mysql.js +46 -16
  77. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  78. package/packages/datadog-instrumentations/src/next.js +155 -23
  79. package/packages/datadog-instrumentations/src/openai.js +14 -0
  80. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  81. package/packages/datadog-instrumentations/src/pg.js +209 -11
  82. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  83. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  84. package/packages/datadog-instrumentations/src/promise.js +3 -3
  85. package/packages/datadog-instrumentations/src/router.js +195 -19
  86. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +85 -13
  87. package/packages/datadog-instrumentations/src/vitest-main.js +528 -72
  88. package/packages/datadog-instrumentations/src/vitest-util.js +106 -12
  89. package/packages/datadog-instrumentations/src/vitest-worker.js +382 -40
  90. package/packages/datadog-instrumentations/src/webdriverio.js +212 -22
  91. package/packages/datadog-instrumentations/src/when.js +3 -3
  92. package/packages/datadog-instrumentations/src/ws.js +5 -1
  93. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  94. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  95. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -5
  96. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  97. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  98. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  99. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +51 -47
  100. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  101. package/packages/datadog-plugin-electron/src/net.js +10 -4
  102. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  103. package/packages/datadog-plugin-http/src/client.js +1 -1
  104. package/packages/datadog-plugin-http2/src/client.js +1 -1
  105. package/packages/datadog-plugin-jest/src/index.js +6 -22
  106. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  107. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  108. package/packages/datadog-plugin-kafkajs/src/producer.js +5 -3
  109. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  110. package/packages/datadog-plugin-mocha/src/index.js +832 -4
  111. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  112. package/packages/datadog-plugin-next/src/index.js +51 -21
  113. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  114. package/packages/datadog-plugin-openai-agents/src/integration.js +278 -69
  115. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  116. package/packages/datadog-plugin-pg/src/index.js +66 -1
  117. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  118. package/packages/datadog-plugin-vitest/src/index.js +103 -27
  119. package/packages/datadog-plugin-ws/src/util.js +2 -1
  120. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  121. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  122. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  123. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  124. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  125. package/packages/dd-trace/src/appsec/handlers/http-request.js +228 -0
  126. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  127. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  128. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  129. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  130. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  131. package/packages/dd-trace/src/appsec/index.js +48 -476
  132. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  133. package/packages/dd-trace/src/carrier.js +356 -0
  134. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  135. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  136. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  137. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  138. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  139. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  140. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  141. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  142. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  143. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  144. package/packages/dd-trace/src/config/generated-config-types.d.ts +5 -0
  145. package/packages/dd-trace/src/config/helper.js +2 -0
  146. package/packages/dd-trace/src/config/index.js +16 -5
  147. package/packages/dd-trace/src/config/remote_config.js +1 -1
  148. package/packages/dd-trace/src/config/supported-configurations.json +18 -0
  149. package/packages/dd-trace/src/datastreams/context.js +6 -1
  150. package/packages/dd-trace/src/datastreams/manager.js +5 -1
  151. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  152. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  153. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  154. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  155. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  156. package/packages/dd-trace/src/guardrails/log.js +1 -10
  157. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  158. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  159. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  160. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  161. package/packages/dd-trace/src/llmobs/index.js +34 -2
  162. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  163. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  164. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  165. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  166. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  167. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  168. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  169. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  170. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  171. package/packages/dd-trace/src/llmobs/util.js +78 -0
  172. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  173. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  174. package/packages/dd-trace/src/log/channels.js +1 -9
  175. package/packages/dd-trace/src/log/levels.js +12 -0
  176. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  177. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  178. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  179. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  180. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  181. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  182. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  183. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  184. package/packages/dd-trace/src/plugin_manager.js +2 -0
  185. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  186. package/packages/dd-trace/src/plugins/index.js +1 -0
  187. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  188. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  189. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  190. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  191. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +3 -62
  192. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  193. package/packages/dd-trace/src/plugins/util/test.js +397 -74
  194. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  195. package/packages/dd-trace/src/plugins/util/web.js +76 -3
  196. package/packages/dd-trace/src/profiler.js +1 -1
  197. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  198. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  199. package/packages/dd-trace/src/proxy.js +1 -0
  200. package/packages/dd-trace/src/ritm.js +5 -0
  201. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  202. package/packages/dd-trace/src/sampling_rule.js +4 -2
  203. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  204. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  205. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  206. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  207. package/vendor/dist/pprof-format/index.js +1 -1
  208. package/version.js +10 -8
  209. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -3,7 +3,6 @@
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
5
  const { isError } = require('../util')
6
- const tracerVersion = require('../../../../package.json').version
7
6
  const logger = require('../log')
8
7
  const { getValueFromEnvSources } = require('../config/helper')
9
8
  const Span = require('../opentracing/span')
@@ -13,6 +12,17 @@ const {
13
12
  INPUT_VALUE,
14
13
  TRACE_ID,
15
14
  } = require('./constants/tags')
15
+ const {
16
+ FEEDBACK_METRIC_TYPES,
17
+ FEEDBACK_TARGET_KEYS,
18
+ buildMetricTags,
19
+ validateAssessment,
20
+ validateLabel,
21
+ validateMetricType,
22
+ validateMetricValue,
23
+ validateReasoning,
24
+ validateTimestamp,
25
+ } = require('./eval-metric')
16
26
  const {
17
27
  getFunctionArguments,
18
28
  validateKind,
@@ -392,92 +402,35 @@ class LLMObs extends NoopLLMObs {
392
402
  }
393
403
 
394
404
  const timestampMs = options.timestampMs || Date.now()
395
- if (typeof timestampMs !== 'number' || timestampMs < 0) {
396
- err = 'invalid_timestamp'
397
- throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
398
- }
405
+ validateTimestamp(timestampMs, 'evaluation')
399
406
 
400
407
  const { label, value, tags, reasoning, assessment, metadata } = options
401
408
  const metricType = options.metricType?.toLowerCase()
402
- if (!label) {
403
- err = 'invalid_metric_label'
404
- throw new Error('label must be the specified name of the evaluation metric')
405
- }
406
- if (!metricType || !['categorical', 'score', 'boolean', 'json'].includes(metricType)) {
407
- err = 'invalid_metric_type'
408
- throw new Error('metricType must be one of "categorical", "score", "boolean" or "json"')
409
- }
410
- if (metricType === 'categorical' && typeof value !== 'string') {
411
- err = 'invalid_metric_value'
412
- throw new Error('value must be a string for a categorical metric.')
413
- }
414
- if (metricType === 'score' && typeof value !== 'number') {
415
- err = 'invalid_metric_value'
416
- throw new Error('value must be a number for a score metric.')
417
- }
418
- if (metricType === 'boolean' && typeof value !== 'boolean') {
419
- err = 'invalid_metric_value'
420
- throw new Error('value must be a boolean for a boolean metric')
421
- }
422
- if (metricType === 'json' && (typeof value !== 'object' || value == null || Array.isArray(value))) {
423
- err = 'invalid_metric_value'
424
- throw new Error('value must be a JSON object for a json metric')
425
- }
426
- if (assessment != null && assessment !== 'pass' && assessment !== 'fail') {
427
- err = 'invalid_assessment'
428
- throw new Error('assessment must be pass or fail')
429
- }
430
- if (reasoning != null && typeof reasoning !== 'string') {
431
- err = 'invalid_reasoning'
432
- throw new Error('reasoning must be a string')
433
- }
409
+ const labelValue = validateLabel(label, 'evaluation')
410
+ validateMetricType(metricType, 'evaluation')
411
+ validateMetricValue(metricType, value)
412
+ validateAssessment(assessment)
413
+ validateReasoning(reasoning)
434
414
  if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
435
415
  err = 'invalid_metadata'
436
416
  throw new Error('metadata must be a JSON object')
437
417
  }
438
418
 
439
- const evaluationTags = {
440
- 'ddtrace.version': tracerVersion,
441
- ml_app: mlApp,
442
- }
443
-
444
- if (tags) {
445
- for (const key in tags) {
446
- const tag = tags[key]
447
- if (typeof tag === 'string') {
448
- evaluationTags[key] = tag
449
- } else if (typeof tag.toString === 'function') {
450
- evaluationTags[key] = tag.toString()
451
- } else if (tag == null) {
452
- evaluationTags[key] = Object.prototype.toString.call(tag)
453
- } else {
454
- // should be a rare case
455
- // every object in JS has a toString, otherwise every primitive has its own toString
456
- // null and undefined are handled above
457
- err = 'invalid_tags'
458
- throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
459
- }
460
- }
461
- }
462
-
463
- // When OTel tracing is enabled, add source:otel tag to allow backend to wait for OTel span conversion
464
- if (this._config.DD_TRACE_OTEL_ENABLED) {
465
- evaluationTags.source = 'otel'
466
- }
467
-
468
419
  const payload = {
420
+ event_kind: 'evaluation',
469
421
  join_on: {
470
422
  span: {
471
423
  span_id: spanId,
472
424
  trace_id: traceId,
473
425
  },
474
426
  },
475
- label,
427
+ label: labelValue,
476
428
  metric_type: metricType,
477
429
  ml_app: mlApp,
478
430
  [`${metricType}_value`]: value,
479
431
  timestamp_ms: timestampMs,
480
- tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`),
432
+ // When OTel tracing is enabled, `source:otel` lets the backend wait for OTel span conversion
433
+ tags: buildMetricTags(tags, mlApp, 'evaluation', this._config.DD_TRACE_OTEL_ENABLED),
481
434
  }
482
435
  if (reasoning != null) {
483
436
  payload.reasoning = reasoning
@@ -491,11 +444,139 @@ class LLMObs extends NoopLLMObs {
491
444
  const currentStore = storage.getStore()
492
445
  const routing = currentStore?.routingContext
493
446
  evalMetricAppendCh.publish({ payload, routing })
447
+ } catch (e) {
448
+ if (e.ddErrorTag) err = e.ddErrorTag
449
+ throw e
494
450
  } finally {
495
451
  telemetry.recordSubmitEvaluation(options, err)
496
452
  }
497
453
  }
498
454
 
455
+ /**
456
+ * Submits end-user feedback for a span, trace, session, or customer-defined entity.
457
+ *
458
+ * Exactly one target must be provided: `span` (as returned by `llmobs.exportSpan()`) or
459
+ * `spanId` to target a span, `traceId` a trace, `sessionId` a session, or `feedbackJoinKey`
460
+ * a customer-defined entity.
461
+ * `label`, `metricType`, `value` and `submitter` are required, and are validated at call time.
462
+ * @param {object} [options] - The feedback options.
463
+ * @param {string} [options.label] - The name of the feedback metric.
464
+ * @param {'categorical' | 'score' | 'boolean' | 'json' | 'text'} [options.metricType] - The value type.
465
+ * @param {string | number | boolean | Record<string, unknown>} [options.value] - The feedback value,
466
+ * matching `metricType`.
467
+ * @param {{ id: string, type?: string }} [options.submitter] - Who submitted the feedback.
468
+ * @param {{ traceId: string, spanId: string }} [options.span] - Span context to attach the feedback to.
469
+ * @param {string} [options.spanId] - ID of the span to attach the feedback to.
470
+ * @param {string} [options.traceId] - ID of the trace to attach the feedback to.
471
+ * @param {string} [options.sessionId] - ID of the session to attach the feedback to.
472
+ * @param {string} [options.feedbackJoinKey] - Customer-defined key to attach the feedback to.
473
+ * @param {Record<string, string>} [options.tags] - Tags to attach to the feedback.
474
+ * @param {string} [options.mlApp] - The ML app name. Defaults to the configured one.
475
+ * @param {number} [options.timestampMs] - When the feedback was generated. Defaults to now.
476
+ * @param {'pass' | 'fail'} [options.assessment] - Assessment of the feedback.
477
+ * @param {string} [options.reasoning] - Explanation of the feedback.
478
+ * @returns {void}
479
+ */
480
+ submitFeedback (options = {}) {
481
+ if (!this.enabled) return
482
+
483
+ let err = ''
484
+ let targetType = 'other'
485
+ let metricTypeTag = 'other'
486
+ try {
487
+ const { span, spanId, traceId, sessionId, feedbackJoinKey, submitter } = options
488
+
489
+ // Resolved before any validation so telemetry still reports the metric type of a
490
+ // submission that fails on an unrelated field.
491
+ const metricType = options.metricType?.toLowerCase()
492
+ if (FEEDBACK_METRIC_TYPES.includes(metricType)) metricTypeTag = metricType
493
+
494
+ // The intake keys feedback off a single top-level identifier, so more than one target
495
+ // would be ambiguous and none would leave the feedback unattached. `span` also carries a
496
+ // traceId, but passing it is wire-equivalent to passing its `spanId` directly.
497
+ let targetName, targetValue
498
+ let targetCount = 0
499
+ if (span != null) { targetName = 'span'; targetValue = span.spanId; targetCount++ }
500
+ if (spanId != null) { targetName = 'spanId'; targetValue = spanId; targetCount++ }
501
+ if (traceId != null) { targetName = 'traceId'; targetValue = traceId; targetCount++ }
502
+ if (sessionId != null) { targetName = 'sessionId'; targetValue = sessionId; targetCount++ }
503
+ if (feedbackJoinKey != null) { targetName = 'feedbackJoinKey'; targetValue = feedbackJoinKey; targetCount++ }
504
+
505
+ if (targetCount !== 1) {
506
+ err = 'invalid_target_count'
507
+ throw new Error(
508
+ 'Exactly one of `span`, `spanId`, `traceId`, `sessionId` or `feedbackJoinKey` ' +
509
+ 'must be specified to submit feedback.'
510
+ )
511
+ }
512
+
513
+ targetType = FEEDBACK_TARGET_KEYS[targetName]
514
+ if (typeof targetValue !== 'string' || !targetValue) {
515
+ if (targetName === 'span') {
516
+ err = 'invalid_span'
517
+ throw new TypeError(
518
+ '`span` must be an object containing a non-empty string spanId. ' +
519
+ '`llmobs.exportSpan()` can be used to generate this object from a given span.'
520
+ )
521
+ }
522
+ err = `invalid_${targetType}`
523
+ throw new TypeError(`\`${targetName}\` must be a non-empty string`)
524
+ }
525
+
526
+ if (typeof submitter?.id !== 'string' || !submitter.id) {
527
+ err = 'invalid_submitter'
528
+ throw new TypeError('submitter must be an object containing a non-empty string id')
529
+ }
530
+ if (submitter.type != null && typeof submitter.type !== 'string') {
531
+ err = 'invalid_submitter'
532
+ throw new TypeError('submitter.type must be a string')
533
+ }
534
+
535
+ const mlApp = options.mlApp || this._config.llmobs.mlApp
536
+ if (!mlApp) {
537
+ err = 'missing_ml_app'
538
+ throw new Error('ML App name is required for sending feedback. Feedback data will not be sent.')
539
+ }
540
+
541
+ const timestampMs = options.timestampMs || Date.now()
542
+ validateTimestamp(timestampMs, 'feedback')
543
+
544
+ const { label, value, tags, reasoning, assessment } = options
545
+ const labelValue = validateLabel(label, 'feedback')
546
+ validateMetricType(metricType, 'feedback')
547
+ validateMetricValue(metricType, value)
548
+ validateAssessment(assessment)
549
+ validateReasoning(reasoning)
550
+
551
+ const payload = {
552
+ event_kind: 'feedback',
553
+ [targetType]: targetValue,
554
+ label: labelValue,
555
+ metric_type: metricType,
556
+ ml_app: mlApp,
557
+ [`${metricType}_value`]: value,
558
+ timestamp_ms: timestampMs,
559
+ tags: buildMetricTags(tags, mlApp, 'feedback'),
560
+ submitter: submitter.type == null ? { id: submitter.id } : { id: submitter.id, type: submitter.type },
561
+ }
562
+ if (reasoning != null) {
563
+ payload.reasoning = reasoning
564
+ }
565
+ if (assessment != null) {
566
+ payload.assessment = assessment
567
+ }
568
+
569
+ const currentStore = storage.getStore()
570
+ const routing = currentStore?.routingContext
571
+ evalMetricAppendCh.publish({ payload, routing })
572
+ } catch (e) {
573
+ if (e.ddErrorTag) err = e.ddErrorTag
574
+ throw e
575
+ } finally {
576
+ telemetry.recordSubmitFeedback(metricTypeTag, targetType, err)
577
+ }
578
+ }
579
+
499
580
  annotationContext (options, fn) {
500
581
  if (!this.enabled) return fn()
501
582
 
@@ -27,6 +27,8 @@ const {
27
27
  ML_APP,
28
28
  TAGS,
29
29
  PARENT_ID_KEY,
30
+ PARENT_AGENT_NAME,
31
+ PARENT_AGENT_SPAN_ID,
30
32
  SESSION_ID,
31
33
  NAME,
32
34
  INPUT_PROMPT,
@@ -157,6 +159,20 @@ class LLMObsSpanProcessor {
157
159
  this.#addObject(mlObsTags[TOOL_DEFINITIONS], meta.tool_definitions)
158
160
  }
159
161
 
162
+ // Surface the agent attribution resolved at registration, but only on spans that actually
163
+ // have an agent ancestor. The id is always present in that case; the name may be missing when
164
+ // it arrived id-only over distributed propagation (older/unsafe upstream). Emit the name as
165
+ // explicit `null` then, matching the cross-language wire shape (dd-trace-py sends null, not an
166
+ // absent key) so the shared backend and system-tests see the same payload.
167
+ const parentAgentName = mlObsTags[PARENT_AGENT_NAME]
168
+ const parentAgentSpanId = mlObsTags[PARENT_AGENT_SPAN_ID]
169
+ if (parentAgentName != null || parentAgentSpanId != null) {
170
+ meta.agent_attribution = {
171
+ pagent_name: parentAgentName ?? null,
172
+ pagent_span_id: parentAgentSpanId,
173
+ }
174
+ }
175
+
160
176
  const llmObsSpan = new LLMObservabilitySpan(spanKind)
161
177
 
162
178
  if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
@@ -23,7 +23,11 @@ const {
23
23
  OUTPUT_MESSAGES,
24
24
  TAGS,
25
25
  NAME,
26
+ PARENT_AGENT_NAME,
27
+ PARENT_AGENT_SPAN_ID,
26
28
  PROPAGATED_PARENT_ID_KEY,
29
+ PROPAGATED_PARENT_AGENT_ID_KEY,
30
+ PROPAGATED_PARENT_AGENT_NAME_KEY,
27
31
  ROOT_PARENT_ID,
28
32
  CACHE_READ_INPUT_TOKENS_METRIC_KEY,
29
33
  CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
@@ -57,6 +61,7 @@ const {
57
61
  const { storage } = require('./storage')
58
62
  const {
59
63
  findGenAIAncestorSpanId,
64
+ resolveAgentAttribution,
60
65
  validateCostTags,
61
66
  writeBridgeTags,
62
67
  validateToolDefinitions,
@@ -173,6 +178,7 @@ class LLMObsTagger {
173
178
  this._setTag(span, PARENT_ID_KEY, parentId)
174
179
 
175
180
  this.#tagSamplingDecision(span, parent)
181
+ this.#tagAgentAttribution(span, parent)
176
182
 
177
183
  // apply annotation context
178
184
  const annotationContext = storage.getStore()?.annotationContext
@@ -229,6 +235,34 @@ class LLMObsTagger {
229
235
  if (samplingDecision != null) this._setTag(span, SAMPLING_DECISION, samplingDecision)
230
236
  }
231
237
 
238
+ /**
239
+ * Store the nearest agent ancestor on the span so it can be surfaced as
240
+ * `meta.agent_attribution` at finish. Resolved once here, at registration, so downstream
241
+ * children inherit it with a single lookup rather than walking the ancestor chain.
242
+ *
243
+ * @param {import('../opentracing/span')} span
244
+ * @param {import('../opentracing/span')} [parent] the LLMObs parent span, if any
245
+ */
246
+ // TODO: spans whose kind changes after registration (e.g. claude-agent-sdk tools promoted to
247
+ // sub-agents) will not retroactively update already-finished children's attribution. Follow up.
248
+ #tagAgentAttribution (span, parent) {
249
+ let name, spanId
250
+ if (registry.has(parent)) {
251
+ // Local LLMObs parent: attribute to it if it is an agent, else inherit its resolution.
252
+ ({ name, spanId } = resolveAgentAttribution(registry.get(parent), parent))
253
+ } else if (span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY]) {
254
+ // Distributed LLMObs parent: inherit the nearest agent propagated from upstream. The
255
+ // name may be absent when the upstream hop ran an older SDK or its name was not
256
+ // wire-safe; the id-only case is expected and the backend resolves the name by span id.
257
+ const traceTags = span.context()._trace.tags
258
+ name = traceTags[PROPAGATED_PARENT_AGENT_NAME_KEY]
259
+ spanId = traceTags[PROPAGATED_PARENT_AGENT_ID_KEY]
260
+ }
261
+
262
+ if (name != null) this._setTag(span, PARENT_AGENT_NAME, name)
263
+ if (spanId != null) this._setTag(span, PARENT_AGENT_SPAN_ID, spanId)
264
+ }
265
+
232
266
  // TODO: similarly for the following `tag` methods,
233
267
  // how can we transition from a span weakmap to core API functionality
234
268
  tagLLMIO (span, inputData, outputData) {
@@ -696,6 +730,59 @@ class LLMObsTagger {
696
730
  return filteredAudioParts
697
731
  }
698
732
 
733
+ // Image counterpart of #filterAudioParts, with the same wire shape and validation.
734
+ #filterImageParts (imageParts) {
735
+ if (!Array.isArray(imageParts)) {
736
+ imageParts = [imageParts]
737
+ }
738
+
739
+ const filteredImageParts = []
740
+ for (const imagePart of imageParts) {
741
+ if (imagePart == null || typeof imagePart !== 'object') {
742
+ this.#handleFailure('Image part must be an object.', 'invalid_io_messages')
743
+ continue
744
+ }
745
+
746
+ const { mimeType, content, attachmentKey } = imagePart
747
+
748
+ if (typeof mimeType !== 'string' || !mimeType) {
749
+ this.#handleFailure('Image part mimeType must be a non-empty string.', 'invalid_io_messages')
750
+ continue
751
+ }
752
+
753
+ if (content == null && attachmentKey == null) {
754
+ this.#handleFailure("Image part must have either 'content' or 'attachmentKey'.", 'invalid_io_messages')
755
+ continue
756
+ }
757
+
758
+ if (content != null && attachmentKey != null) {
759
+ this.#handleFailure(
760
+ "Image part must have only one of 'content' or 'attachmentKey', not both.", 'invalid_io_messages'
761
+ )
762
+ continue
763
+ }
764
+
765
+ const imagePartObj = { mime_type: mimeType }
766
+
767
+ if (content == null) {
768
+ if (typeof attachmentKey !== 'string') {
769
+ this.#handleFailure('Image part attachmentKey must be a string.', 'invalid_io_messages')
770
+ continue
771
+ }
772
+ imagePartObj.attachment_key = attachmentKey
773
+ } else {
774
+ if (typeof content !== 'string') {
775
+ this.#handleFailure('Image part content must be a base64-encoded string.', 'invalid_io_messages')
776
+ continue
777
+ }
778
+ imagePartObj.content = content
779
+ }
780
+
781
+ filteredImageParts.push(imagePartObj)
782
+ }
783
+ return filteredImageParts
784
+ }
785
+
699
786
  #tagMessages (span, data, key) {
700
787
  if (!data) {
701
788
  return
@@ -723,6 +810,7 @@ class LLMObsTagger {
723
810
  toolResults,
724
811
  toolId,
725
812
  audioParts,
813
+ imageParts,
726
814
  } = message
727
815
  const messageObj = {}
728
816
 
@@ -764,6 +852,14 @@ class LLMObsTagger {
764
852
  }
765
853
  }
766
854
 
855
+ if (imageParts != null) {
856
+ const filteredImageParts = this.#filterImageParts(imageParts)
857
+
858
+ if (filteredImageParts.length) {
859
+ messageObj.image_parts = filteredImageParts
860
+ }
861
+ }
862
+
767
863
  if (toolId) {
768
864
  if (role === 'tool') {
769
865
  condition = this.#tagConditionalString(toolId, 'Tool ID', messageObj, 'tool_id') && condition
@@ -183,6 +183,23 @@ function recordSubmitEvaluation (options, err, value = 1) {
183
183
  llmobsMetrics.count('evals_submitted', tags).inc(value)
184
184
  }
185
185
 
186
+ /**
187
+ * @param {string} metricType - The submitted metric type, `'other'` if unrecognized.
188
+ * @param {string} targetType - The payload key the feedback was attached to, `'other'` if unresolved.
189
+ * @param {string} err - The telemetry error tag, empty when the submission succeeded.
190
+ * @param {number} [value]
191
+ * @returns {void}
192
+ */
193
+ function recordSubmitFeedback (metricType, targetType, err, value = 1) {
194
+ const tags = {
195
+ error: Number(!!err),
196
+ metric_type: metricType,
197
+ target_type: targetType,
198
+ }
199
+ if (err) tags.error_type = err
200
+ llmobsMetrics.count('feedback_submitted', tags).inc(value)
201
+ }
202
+
186
203
  function recordLLMObsUserProcessorCalled (error, value = 1) {
187
204
  const tags = { error: error ? 1 : 0 }
188
205
  llmobsMetrics.count('user_processor_called', tags).inc(value)
@@ -201,5 +218,6 @@ module.exports = {
201
218
  recordUserFlush,
202
219
  recordExportSpan,
203
220
  recordSubmitEvaluation,
221
+ recordSubmitFeedback,
204
222
  recordLLMObsUserProcessorCalled,
205
223
  }
@@ -6,6 +6,10 @@ const {
6
6
  LLMOBS_PARENT_ID_BRIDGE_KEY,
7
7
  LLMOBS_TRACE_ID_BRIDGE_KEY,
8
8
  SPAN_KINDS,
9
+ SPAN_KIND,
10
+ NAME,
11
+ PARENT_AGENT_NAME,
12
+ PARENT_AGENT_SPAN_ID,
9
13
  } = require('./constants/tags')
10
14
 
11
15
  const DECIMAL_TRACE_ID_REGEX = /^\d+$/
@@ -294,6 +298,76 @@ function getFunctionArguments (fn, args = []) {
294
298
  }
295
299
  }
296
300
 
301
+ // The `x-datadog-tags` tagset encoder rejects commas (the entry delimiter) and any byte
302
+ // outside 0x20-0x7E, and a raise there drops the ENTIRE header (taking ml_app,
303
+ // llmobs_trace_id, parent_id, sampling with it). An agent name is arbitrary user text, so it
304
+ // must be skipped rather than sanitized when unsafe: only the digit-safe id then propagates and
305
+ // the backend resolves the real name by span id. `=` is legal in tagset values (illegal only in
306
+ // keys), so a name containing `=` is safe and must not be dropped.
307
+ /**
308
+ * @param {string} name
309
+ * @returns {boolean}
310
+ */
311
+ function agentNameWireSafe (name) {
312
+ // Conservative slice of the 512B shared tagset budget, mirroring dd-trace-py.
313
+ if (Buffer.byteLength(name, 'utf8') > 256) return false
314
+ for (let index = 0; index < name.length; index++) {
315
+ const code = name.charCodeAt(index)
316
+ if (code < 0x20 || code > 0x7E || code === 0x2C /* comma */) return false
317
+ }
318
+ return true
319
+ }
320
+
321
+ /**
322
+ * Resolve the nearest agent that a *child* of `span` should be attributed to.
323
+ *
324
+ * If `span` is itself an agent, the child attributes directly to `span`. Otherwise `span`
325
+ * already resolved its own nearest agent at registration, so the child inherits that with a
326
+ * single lookup rather than walking the ancestor chain. An agent never attributes itself.
327
+ *
328
+ * @param {Record<string, unknown> | undefined} tags - Registry entry for the parent span.
329
+ * @param {import('../opentracing/span')} [span] - The parent span itself (needed for span id / name).
330
+ * @returns {{ name: string | undefined, spanId: string | undefined }}
331
+ */
332
+ function resolveAgentAttribution (tags, span) {
333
+ if (!tags) return { name: undefined, spanId: undefined }
334
+ if (tags[SPAN_KIND] === 'agent') {
335
+ return { name: tags[NAME] || span._name, spanId: span.context().toSpanId() }
336
+ }
337
+ return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID] }
338
+ }
339
+
340
+ /**
341
+ * Removes all `key=value` entries for the given key from a comma-separated tagset string.
342
+ *
343
+ * @param {string} tags - Existing tagset string (may be empty).
344
+ * @param {string} key
345
+ * @returns {string}
346
+ */
347
+ function stripTagsetEntry (tags, key) {
348
+ if (!tags.includes(key)) return tags
349
+ return tags.split(',').filter(entry => !entry.startsWith(`${key}=`)).join(',')
350
+ }
351
+
352
+ /**
353
+ * Appends `key=value` to the tagset string with a comma separator, but only when `value` is
354
+ * truthy, passes the optional `safeguard` predicate, and fits within `maxTagSetLength`. Returns
355
+ * the original `tags` unchanged when the value is absent, unsafe, or would overflow the budget.
356
+ *
357
+ * @param {string} tags - Existing tagset string (may be empty).
358
+ * @param {string} key
359
+ * @param {string | undefined} value
360
+ * @param {((v: string) => boolean) | null} [safeguard]
361
+ * @param {number} [maxTagSetLength]
362
+ * @returns {string}
363
+ */
364
+ function appendOptionalPropagatedTag (tags, key, value, safeguard, maxTagSetLength) {
365
+ if (!value || (safeguard && !safeguard(value))) return tags
366
+ const entry = `${tags ? ',' : ''}${key}=${value}`
367
+ if (maxTagSetLength != null && tags.length + entry.length > maxTagSetLength) return tags
368
+ return `${tags}${entry}`
369
+ }
370
+
297
371
  function spanHasError (span) {
298
372
  const spanContext = span.context()
299
373
  return !!(spanContext.getTag('error') || spanContext.getTag('error.type'))
@@ -443,6 +517,8 @@ function formatAudioPart (data, mimeType) {
443
517
  }
444
518
 
445
519
  module.exports = {
520
+ agentNameWireSafe,
521
+ appendOptionalPropagatedTag,
446
522
  audioMimeTypeFromFormat,
447
523
  encodeUnicode,
448
524
  findGenAIAncestorSpanId,
@@ -450,6 +526,8 @@ module.exports = {
450
526
  llmObsTraceIdToWire,
451
527
  normalizeLlmObsTraceId,
452
528
  formatAudioPart,
529
+ resolveAgentAttribution,
530
+ stripTagsetEntry,
453
531
  validateCostTags,
454
532
  validateKind,
455
533
  getFunctionArguments,
@@ -88,6 +88,9 @@ class BaseLLMObsWriter {
88
88
  }
89
89
 
90
90
  /**
91
+ * @param {Record<string, unknown>} event
92
+ * @param {{ apiKey?: string, site?: string }} [routing]
93
+ * @param {number} [byteLength]
91
94
  * @returns {boolean} `true` if the event was buffered, `false` if it was dropped
92
95
  * (e.g. the per-routing buffer was full). Callers that depend on the event
93
96
  * actually being submitted should check this value.
@@ -7,6 +7,8 @@ const { fetchAgentInfo } = require('../../agent/info')
7
7
 
8
8
  /**
9
9
  * @param {import('../../config/config-base')} config
10
+ * @param {(agentless: boolean) => void} setWritersAgentlessValue
11
+ * @returns {void}
10
12
  */
11
13
  function setAgentStrategy (config, setWritersAgentlessValue) {
12
14
  const agentlessEnabled = config.llmobs.agentlessEnabled
@@ -2,15 +2,7 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
- const Level = {
6
- trace: 10,
7
- debug: 20,
8
- info: 30,
9
- warn: 40,
10
- error: 50,
11
- critical: 50,
12
- off: 100,
13
- }
5
+ const Level = require('./levels')
14
6
 
15
7
  const traceChannel = channel('datadog:log:trace')
16
8
  const debugChannel = channel('datadog:log:debug')
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ // The guardrails logger requires this file, so it has to parse on Node.js 0.8.
4
+ module.exports = {
5
+ trace: 10,
6
+ debug: 20,
7
+ info: 30,
8
+ warn: 40,
9
+ error: 50,
10
+ critical: 50,
11
+ off: 100
12
+ }
@@ -63,7 +63,7 @@ class ExposuresWriter extends BaseFFEWriter {
63
63
  * @param {import('../../config/config-base')} config - Tracer configuration object
64
64
  */
65
65
  constructor (config) {
66
- const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/+$/, '')
66
+ const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/$/, '')
67
67
  const endpoint = EXPOSURES_ENDPOINT.replace(/^\/+/, '')
68
68
  const fullEndpoint = `${basePath}/${endpoint}`
69
69
 
@@ -18,7 +18,7 @@ function setAgentStrategy (config, setWriterEnabledValue) {
18
18
  }
19
19
 
20
20
  const endpoints = agentInfo.endpoints
21
- const normalizedPath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/+$/, '')
21
+ const normalizedPath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/$/, '')
22
22
  const hasEndpoint = Array.isArray(endpoints) &&
23
23
  endpoints.includes(normalizedPath) || endpoints.includes(normalizedPath + '/')
24
24
 
@@ -22,7 +22,7 @@ class BatchLogRecordProcessor {
22
22
  /**
23
23
  * Creates a new BatchLogRecordProcessor instance.
24
24
  *
25
- * @param {OtlpHttpLogExporter} exporter - Log processor for exporting batches to Datadog Agent
25
+ * @param {import('./otlp_http_log_exporter')} exporter - Log processor for exporting batches to Datadog Agent
26
26
  * @param {number} batchTimeout - Timeout in milliseconds for batch processing
27
27
  * @param {number} maxExportBatchSize - Maximum number of log records per batch
28
28
  */