dd-trace 6.8.0 → 6.9.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 (179) 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 +30 -34
  44. package/index.d.ts +98 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +5 -3
  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/cucumber.js +124 -47
  55. package/packages/datadog-instrumentations/src/dns.js +21 -2
  56. package/packages/datadog-instrumentations/src/electron.js +1 -0
  57. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  58. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  59. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  60. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  61. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +33 -9
  62. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +118 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  66. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  67. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  68. package/packages/datadog-instrumentations/src/jest.js +979 -233
  69. package/packages/datadog-instrumentations/src/mocha/main.js +107 -9
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +64 -42
  71. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  72. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  73. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  74. package/packages/datadog-instrumentations/src/next.js +155 -23
  75. package/packages/datadog-instrumentations/src/openai.js +14 -0
  76. package/packages/datadog-instrumentations/src/pg.js +180 -6
  77. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  78. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  79. package/packages/datadog-instrumentations/src/promise.js +3 -3
  80. package/packages/datadog-instrumentations/src/router.js +195 -19
  81. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -10
  82. package/packages/datadog-instrumentations/src/vitest-main.js +360 -56
  83. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  84. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  85. package/packages/datadog-instrumentations/src/webdriverio.js +194 -18
  86. package/packages/datadog-instrumentations/src/when.js +3 -3
  87. package/packages/datadog-instrumentations/src/ws.js +5 -1
  88. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  90. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  92. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  93. package/packages/datadog-plugin-electron/src/net.js +10 -4
  94. package/packages/datadog-plugin-http/src/client.js +1 -1
  95. package/packages/datadog-plugin-http2/src/client.js +1 -1
  96. package/packages/datadog-plugin-jest/src/index.js +1 -18
  97. package/packages/datadog-plugin-jest/src/util.js +2 -0
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  99. package/packages/datadog-plugin-mocha/src/index.js +318 -1
  100. package/packages/datadog-plugin-next/src/index.js +57 -9
  101. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  102. package/packages/datadog-plugin-pg/src/index.js +65 -1
  103. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  104. package/packages/datadog-plugin-vitest/src/index.js +95 -7
  105. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  106. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  107. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  108. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  109. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  110. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  111. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  112. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  113. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  114. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  115. package/packages/dd-trace/src/appsec/index.js +48 -476
  116. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  117. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  118. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  120. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  122. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  123. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  124. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  127. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  128. package/packages/dd-trace/src/config/helper.js +2 -0
  129. package/packages/dd-trace/src/config/index.js +16 -5
  130. package/packages/dd-trace/src/config/remote_config.js +1 -1
  131. package/packages/dd-trace/src/config/supported-configurations.json +8 -0
  132. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  133. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  134. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  135. package/packages/dd-trace/src/guardrails/log.js +1 -10
  136. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  137. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  138. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  139. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  140. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  141. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  142. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  143. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  144. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  145. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  146. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  147. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  148. package/packages/dd-trace/src/log/channels.js +1 -9
  149. package/packages/dd-trace/src/log/levels.js +12 -0
  150. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  151. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  152. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  153. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  154. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  155. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  156. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  157. package/packages/dd-trace/src/plugins/index.js +1 -0
  158. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  159. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  160. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  161. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  162. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  163. package/packages/dd-trace/src/plugins/util/test.js +319 -75
  164. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  165. package/packages/dd-trace/src/plugins/util/web.js +23 -1
  166. package/packages/dd-trace/src/profiler.js +1 -1
  167. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  168. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  169. package/packages/dd-trace/src/proxy.js +1 -0
  170. package/packages/dd-trace/src/ritm.js +5 -0
  171. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  172. package/packages/dd-trace/src/sampling_rule.js +4 -2
  173. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  174. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  175. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  176. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  177. package/vendor/dist/pprof-format/index.js +1 -1
  178. package/version.js +10 -8
  179. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -579,6 +579,7 @@ export interface GeneratedConfig {
579
579
  DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
580
580
  DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
581
581
  DD_TEST_MANAGEMENT_ENABLED: boolean;
582
+ DD_TEST_MANAGEMENT_REPORT_ENABLED: boolean | undefined;
582
583
  };
583
584
  traceId128BitGenerationEnabled: boolean;
584
585
  traceId128BitLoggingEnabled: boolean;
@@ -821,6 +822,7 @@ export interface GeneratedEnvVarConfig {
821
822
  DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
822
823
  DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
823
824
  DD_TEST_MANAGEMENT_ENABLED: boolean;
825
+ DD_TEST_MANAGEMENT_REPORT_ENABLED: boolean | undefined;
824
826
  DD_TEST_SESSION_NAME: string | undefined;
825
827
  DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: boolean;
826
828
  DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED: boolean;
@@ -307,6 +307,8 @@ module.exports = {
307
307
  *
308
308
  * This should only be called once in config.js to avoid copying the object frequently.
309
309
  *
310
+ * @param {typeof process.env} [source] The environment to read from
311
+ * @param {boolean} [internalOnly] Only return configurations marked as internal
310
312
  * @returns {TracerEnv} The environment variables
311
313
  */
312
314
  getEnvironmentVariables (source = process.env, internalOnly = false) {
@@ -4,6 +4,7 @@ const fs = require('node:fs')
4
4
  const os = require('node:os')
5
5
  const { URL, format } = require('node:url')
6
6
 
7
+ const exporters = require('../../../../ext/exporters')
7
8
  const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
8
9
  const uuid = require('../../../../vendor/dist/crypto-randomuuid') // we need to keep the old uuid dep because of cypress
9
10
  const set = require('../../../datadog-core/src/utils/src/set')
@@ -41,6 +42,13 @@ const { normalizeService } = require('./normalize-service')
41
42
  const { programmaticTypeCoercions, transformers } = require('./parsers')
42
43
 
43
44
  const RUNTIME_ID = uuid()
45
+ const TEST_OPTIMIZATION_WORKER_EXPORTERS = new Set([
46
+ exporters.CUCUMBER_WORKER,
47
+ exporters.JEST_WORKER,
48
+ exporters.MOCHA_WORKER,
49
+ exporters.PLAYWRIGHT_WORKER,
50
+ exporters.VITEST_WORKER,
51
+ ])
44
52
 
45
53
  const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
46
54
 
@@ -194,8 +202,11 @@ class Config extends ConfigBase {
194
202
  this.debug = log.configure(options)
195
203
 
196
204
  // Process stable config warnings, if any
197
- for (const warning of this.stableConfig?.warnings ?? []) {
198
- log.warn(warning)
205
+ const stableConfigWarnings = this.stableConfig?.warnings
206
+ if (stableConfigWarnings) {
207
+ for (const warning of stableConfigWarnings) {
208
+ log.warn(warning)
209
+ }
199
210
  }
200
211
 
201
212
  this.#applyDefaults()
@@ -593,9 +604,9 @@ class Config extends ConfigBase {
593
604
  setAndTrack(this, 'remoteConfig.DD_REMOTE_CONFIGURATION_ENABLED', false)
594
605
  }
595
606
 
596
- // TODO: Should this unconditionally be disabled?
597
- if (getEnvironmentVariable('JEST_WORKER_ID') &&
598
- !trackedConfigOrigins.has('telemetry.DD_INSTRUMENTATION_TELEMETRY_ENABLED')) {
607
+ const isTestOptimizationWorker = this.isCiVisibility &&
608
+ TEST_OPTIMIZATION_WORKER_EXPORTERS.has(this.experimental.exporter)
609
+ if (isTestOptimizationWorker) {
599
610
  setAndTrack(this, 'telemetry.DD_INSTRUMENTATION_TELEMETRY_ENABLED', false)
600
611
  }
601
612
 
@@ -236,7 +236,7 @@ const optionLookupTable = {
236
236
 
237
237
  const transformers = {
238
238
  tracing_sampling_rules (samplingRules) {
239
- for (const rule of (samplingRules || [])) {
239
+ for (const rule of samplingRules) {
240
240
  if (rule.tags) {
241
241
  const reformattedTags = {}
242
242
  for (const tag of rule.tags) {
@@ -1941,6 +1941,14 @@
1941
1941
  "namespace": "testOptimization"
1942
1942
  }
1943
1943
  ],
1944
+ "DD_TEST_MANAGEMENT_REPORT_ENABLED": [
1945
+ {
1946
+ "implementation": "A",
1947
+ "type": "boolean",
1948
+ "default": null,
1949
+ "namespace": "testOptimization"
1950
+ }
1951
+ ],
1944
1952
  "DD_TEST_SESSION_NAME": [
1945
1953
  {
1946
1954
  "implementation": "A",
@@ -32,7 +32,7 @@ const STATUSES = {
32
32
  INSTALLED: 'INSTALLED',
33
33
  EMITTING: 'EMITTING',
34
34
  ERROR: 'ERROR',
35
- BLOCKED: 'BLOCKED', // TODO: Implement once support for allow list, deny list or max probe limit has been added
35
+ // TODO: Add BLOCKED once support for allow list, deny list or max probe limit has been added
36
36
  }
37
37
 
38
38
  function ackReceived ({ id: probeId, version }) {
@@ -110,7 +110,6 @@ class CoverageCIVisibilityEncoder extends AgentEncoder {
110
110
  _encodePayloadStart (bytes) {
111
111
  const payload = {
112
112
  version: COVERAGE_PAYLOAD_VERSION,
113
- coverages: [],
114
113
  }
115
114
  bytes.writeMapPrefix(COVERAGE_KEYS_LENGTH)
116
115
  this._encodeString(bytes, 'version')
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ // In PM2 cluster mode, per-app env vars arrive as a `pm2_env`
4
+ // JSON string after preloads have run, so extract them first.
5
+ var pm2EnvStr = process.env.pm2_env
6
+ if (typeof pm2EnvStr === 'string') {
7
+ try {
8
+ var pm2Config = JSON.parse(pm2EnvStr)
9
+ var pm2Keys = Object.keys(pm2Config)
10
+ for (var i = 0; i < pm2Keys.length; i++) {
11
+ var key = pm2Keys[i]
12
+ var value = pm2Config[key]
13
+ process.env[key] = String(value)
14
+ }
15
+ } catch (error) {}
16
+ }
@@ -2,21 +2,12 @@
2
2
 
3
3
  /* eslint-disable no-console */
4
4
 
5
+ var logLevels = require('../log/levels')
5
6
  var isTrue = require('./util').isTrue
6
7
 
7
8
  var DD_TRACE_DEBUG = process.env.DD_TRACE_DEBUG
8
9
  var DD_TRACE_LOG_LEVEL = process.env.DD_TRACE_LOG_LEVEL
9
10
 
10
- var logLevels = {
11
- trace: 20,
12
- debug: 20,
13
- info: 30,
14
- warn: 40,
15
- error: 50,
16
- critical: 50,
17
- off: 100
18
- }
19
-
20
11
  var logLevel = isTrue(DD_TRACE_DEBUG)
21
12
  ? Number(DD_TRACE_LOG_LEVEL) || logLevels.debug
22
13
  : logLevels.off
@@ -0,0 +1,249 @@
1
+ 'use strict'
2
+
3
+ const tracerVersion = require('../../../../package.json').version
4
+
5
+ /**
6
+ * Kind of event submitted through the evaluation metrics intake. Both kinds share the
7
+ * writer, the endpoint and most of their validation, and are told apart on the wire by
8
+ * the `event_kind` field.
9
+ * @typedef {'evaluation' | 'feedback'} MetricEventKind
10
+ */
11
+
12
+ /**
13
+ * Tag value the intake accepts: a string, a nullish value, or anything the SDK can coerce
14
+ * through `toString()`.
15
+ * @typedef {string | null | undefined | { toString?: unknown }} MetricTagValue
16
+ */
17
+
18
+ /** @type {string[]} */
19
+ const EVALUATION_METRIC_TYPES = ['categorical', 'score', 'boolean', 'json']
20
+
21
+ /**
22
+ * End-user feedback additionally accepts free-form text values.
23
+ * @type {string[]}
24
+ */
25
+ const FEEDBACK_METRIC_TYPES = [...EVALUATION_METRIC_TYPES, 'text']
26
+
27
+ /** @type {Record<MetricEventKind, string[]>} */
28
+ const METRIC_TYPES = {
29
+ evaluation: EVALUATION_METRIC_TYPES,
30
+ feedback: FEEDBACK_METRIC_TYPES,
31
+ }
32
+
33
+ /**
34
+ * Feedback target option accepted by the SDK, mapped to the payload key it is sent as. `span` and
35
+ * `spanId` are two ways of naming the same target.
36
+ * @type {Record<string, string>}
37
+ */
38
+ const FEEDBACK_TARGET_KEYS = {
39
+ span: 'span_id',
40
+ spanId: 'span_id',
41
+ traceId: 'trace_id',
42
+ sessionId: 'session_id',
43
+ feedbackJoinKey: 'feedback_join_key',
44
+ }
45
+
46
+ // The validation is shared but the user-facing wording is per-kind. Keeping the terms in one
47
+ // table avoids threading three near-identical strings through every validator.
48
+ /** @type {Record<MetricEventKind, { metric: string, metrics: string, data: string }>} */
49
+ const TERMS = {
50
+ evaluation: {
51
+ metric: 'evaluation metric',
52
+ metrics: 'evaluation metrics',
53
+ data: 'Evaluation metric data',
54
+ },
55
+ feedback: {
56
+ metric: 'feedback metric',
57
+ metrics: 'feedback metrics',
58
+ data: 'Feedback data',
59
+ },
60
+ }
61
+
62
+ /**
63
+ * @param {string[]} metricTypes
64
+ * @returns {string} the quoted list, e.g. `"categorical", "score" or "json"`
65
+ */
66
+ function formatMetricTypes (metricTypes) {
67
+ const quoted = metricTypes.map(metricType => `"${metricType}"`)
68
+ return `${quoted.slice(0, -1).join(', ')} or ${quoted.at(-1)}`
69
+ }
70
+
71
+ // Built once at load time: only ever used to build an error message.
72
+ /** @type {Record<MetricEventKind, string>} */
73
+ const METRIC_TYPES_MESSAGE = {
74
+ evaluation: formatMetricTypes(EVALUATION_METRIC_TYPES),
75
+ feedback: formatMetricTypes(FEEDBACK_METRIC_TYPES),
76
+ }
77
+
78
+ /**
79
+ * Builds an error carrying the telemetry error tag the SDK reports for the failed submission.
80
+ * @param {string} errorTag - Telemetry `error_type` tag value.
81
+ * @param {string} message - User-facing message.
82
+ * @returns {Error} the tagged error, to be thrown by the caller.
83
+ */
84
+ function taggedError (errorTag, message) {
85
+ const error = new Error(message)
86
+ // Same shape as the tagger's failures, which the SDK already reads back as `e.ddErrorTag`.
87
+ Object.defineProperty(error, 'ddErrorTag', { get () { return errorTag } })
88
+ return error
89
+ }
90
+
91
+ /**
92
+ * @param {MetricEventKind} kind
93
+ * @returns {Error} the tagged error, to be thrown by the caller.
94
+ */
95
+ function invalidTagsError (kind) {
96
+ return taggedError('invalid_tags', `Failed to parse tags. Tags for ${TERMS[kind].metrics} must be strings`)
97
+ }
98
+
99
+ /**
100
+ * @param {unknown} timestampMs
101
+ * @param {MetricEventKind} kind
102
+ * @returns {void}
103
+ */
104
+ function validateTimestamp (timestampMs, kind) {
105
+ if (typeof timestampMs !== 'number' || timestampMs < 0) {
106
+ throw taggedError(
107
+ 'invalid_timestamp',
108
+ `timestampMs must be a non-negative integer. ${TERMS[kind].data} will not be sent`
109
+ )
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Validates the label and returns it as sent on the wire.
115
+ * @param {unknown} label
116
+ * @param {MetricEventKind} kind
117
+ * @returns {unknown} the label, coerced to a string for feedback and left untouched for evaluations.
118
+ */
119
+ function validateLabel (label, kind) {
120
+ if (!label) {
121
+ throw taggedError('invalid_metric_label', `label must be the specified name of the ${TERMS[kind].metric}`)
122
+ }
123
+
124
+ // Evaluations keep the looser check they shipped with: a dotted or non-string label reaches the
125
+ // intake as-is. Aligning them with feedback rejects calls that succeed today, so it is deferred.
126
+ if (kind === 'evaluation') return label
127
+
128
+ // A dot makes the label unusable as a facet key.
129
+ const labelValue = String(label)
130
+ if (labelValue.includes('.')) {
131
+ throw taggedError('invalid_label_value', 'label value must not contain a "."')
132
+ }
133
+
134
+ return labelValue
135
+ }
136
+
137
+ /**
138
+ * @param {string | undefined} metricType - Already lower-cased metric type.
139
+ * @param {MetricEventKind} kind
140
+ * @returns {void}
141
+ */
142
+ function validateMetricType (metricType, kind) {
143
+ if (!metricType || !METRIC_TYPES[kind].includes(metricType)) {
144
+ throw taggedError('invalid_metric_type', `metricType must be one of ${METRIC_TYPES_MESSAGE[kind]}`)
145
+ }
146
+ }
147
+
148
+ /**
149
+ * @param {string | undefined} metricType - Metric type already validated for the submitted kind.
150
+ * @param {unknown} value
151
+ * @returns {void}
152
+ */
153
+ function validateMetricValue (metricType, value) {
154
+ if (metricType === 'categorical' && typeof value !== 'string') {
155
+ throw taggedError('invalid_metric_value', 'value must be a string for a categorical metric.')
156
+ }
157
+ if (metricType === 'score' && typeof value !== 'number') {
158
+ throw taggedError('invalid_metric_value', 'value must be a number for a score metric.')
159
+ }
160
+ if (metricType === 'boolean' && typeof value !== 'boolean') {
161
+ throw taggedError('invalid_metric_value', 'value must be a boolean for a boolean metric')
162
+ }
163
+ if (metricType === 'json' && (typeof value !== 'object' || value == null || Array.isArray(value))) {
164
+ throw taggedError('invalid_metric_value', 'value must be a JSON object for a json metric')
165
+ }
166
+ if (metricType === 'text' && typeof value !== 'string') {
167
+ throw taggedError('invalid_metric_value', 'value must be a string for a text metric')
168
+ }
169
+ }
170
+
171
+ /**
172
+ * @param {unknown} assessment
173
+ * @returns {void}
174
+ */
175
+ function validateAssessment (assessment) {
176
+ if (assessment != null && assessment !== 'pass' && assessment !== 'fail') {
177
+ throw taggedError('invalid_assessment', 'assessment must be pass or fail')
178
+ }
179
+ }
180
+
181
+ /**
182
+ * @param {unknown} reasoning
183
+ * @returns {void}
184
+ */
185
+ function validateReasoning (reasoning) {
186
+ if (reasoning != null && typeof reasoning !== 'string') {
187
+ throw taggedError('invalid_reasoning', 'reasoning must be a string')
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Serializes the user-provided tags into the `key:value` list expected by the intake, along
193
+ * with the tags every submission carries.
194
+ * @param {Record<string, MetricTagValue> | undefined} tags - User-provided tags, coerced when not strings.
195
+ * @param {string} mlApp - Resolved ML app name.
196
+ * @param {MetricEventKind} kind
197
+ * @param {boolean} [otelEnabled] - Adds `source:otel` so the backend waits for OTel span conversion.
198
+ * @returns {string[]} the serialized tag list.
199
+ */
200
+ function buildMetricTags (tags, mlApp, kind, otelEnabled = false) {
201
+ // An array or a string would otherwise be walked by index and produce tags named after
202
+ // their offsets, which the intake would happily store.
203
+ if (tags != null && (typeof tags !== 'object' || Array.isArray(tags))) {
204
+ throw invalidTagsError(kind)
205
+ }
206
+
207
+ const metricTags = {
208
+ 'ddtrace.version': tracerVersion,
209
+ ml_app: mlApp,
210
+ }
211
+
212
+ if (tags) {
213
+ for (const key of Object.keys(tags)) {
214
+ const tag = tags[key]
215
+ if (typeof tag === 'string') {
216
+ metricTags[key] = tag
217
+ } else if (tag == null) {
218
+ // A nullish value can be intentional, so it is serialized rather than rejected. Checked
219
+ // before the `toString` lookup below, which would throw a raw TypeError on it.
220
+ metricTags[key] = String(tag)
221
+ } else if (typeof tag.toString === 'function') {
222
+ metricTags[key] = tag.toString()
223
+ } else {
224
+ // Every other value carries a `toString`, either its own or the one inherited from
225
+ // `Object.prototype`, so this is only reached for a null-prototype object.
226
+ throw invalidTagsError(kind)
227
+ }
228
+ }
229
+ }
230
+
231
+ if (otelEnabled) {
232
+ metricTags.source = 'otel'
233
+ }
234
+
235
+ return Object.entries(metricTags).map(([key, value]) => `${key}:${value}`)
236
+ }
237
+
238
+ module.exports = {
239
+ EVALUATION_METRIC_TYPES,
240
+ FEEDBACK_METRIC_TYPES,
241
+ FEEDBACK_TARGET_KEYS,
242
+ buildMetricTags,
243
+ validateAssessment,
244
+ validateLabel,
245
+ validateMetricType,
246
+ validateMetricValue,
247
+ validateReasoning,
248
+ validateTimestamp,
249
+ }
@@ -47,15 +47,17 @@ class Experiments {
47
47
  : (descriptionOrOptions ?? {})
48
48
  const dataset = new Dataset(this.#client, name, options.description ?? '')
49
49
  const recordIds = new Set()
50
- for (const record of options.records ?? []) {
51
- if (record.id !== undefined && (typeof record.id !== 'string' || record.id.length === 0)) {
52
- throw new Error('record id must be a non-empty string')
53
- }
54
- if (record.id !== undefined) {
55
- if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
56
- recordIds.add(record.id)
50
+ if ((options.records) != null) {
51
+ for (const record of options.records) {
52
+ if (record.id !== undefined && (typeof record.id !== 'string' || record.id.length === 0)) {
53
+ throw new Error('record id must be a non-empty string')
54
+ }
55
+ if (record.id !== undefined) {
56
+ if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
57
+ recordIds.add(record.id)
58
+ }
59
+ dataset.addRecord(new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id))
57
60
  }
58
- dataset.addRecord(new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id))
59
61
  }
60
62
  return dataset
61
63
  }
@@ -82,13 +84,16 @@ class Experiments {
82
84
  'GET',
83
85
  `${API_BASE_PATH}/${projectId}/datasets?filter[name]=${encodeURIComponent(name)}`
84
86
  )
85
- for (const item of listed?.data ?? []) {
86
- if (item?.attributes?.name === name) {
87
- datasetId = String(item?.id ?? '')
88
- description = String(item?.attributes?.description ?? '')
89
- latestVersion = item?.attributes?.current_version ?? null
90
- datasetVersion = version ?? latestVersion
91
- break
87
+ const datasets = listed?.data
88
+ if (datasets) {
89
+ for (const item of datasets) {
90
+ if (item?.attributes?.name === name) {
91
+ datasetId = String(item?.id ?? '')
92
+ description = String(item?.attributes?.description ?? '')
93
+ latestVersion = item?.attributes?.current_version ?? null
94
+ datasetVersion = version ?? latestVersion
95
+ break
96
+ }
92
97
  }
93
98
  }
94
99
  if (datasetId === null) return false
@@ -107,16 +112,19 @@ class Experiments {
107
112
  'GET',
108
113
  `${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records?${query.toString()}`
109
114
  )
110
- for (const item of resp?.data ?? []) {
111
- const attrs = item?.attributes ?? item
112
- const recordId = String(item?.id ?? attrs?.id ?? '')
113
- recs.push(new DatasetRecord(
114
- attrs?.input ?? null,
115
- attrs?.expected_output ?? null,
116
- attrs?.metadata ?? {},
117
- recordId === '' ? null : recordId
118
- ))
119
- ids.push(recordId)
115
+ const recordData = resp?.data
116
+ if (recordData) {
117
+ for (const item of recordData) {
118
+ const attrs = item?.attributes ?? item
119
+ const recordId = String(item?.id ?? attrs?.id ?? '')
120
+ recs.push(new DatasetRecord(
121
+ attrs?.input ?? null,
122
+ attrs?.expected_output ?? null,
123
+ attrs?.metadata ?? {},
124
+ recordId === '' ? null : recordId
125
+ ))
126
+ ids.push(recordId)
127
+ }
120
128
  }
121
129
  cursor = resp?.meta?.after ?? ''
122
130
  if (!cursor) break
@@ -166,8 +166,10 @@ function stringify (value) {
166
166
  */
167
167
  function buildTags (userTags, autoTags) {
168
168
  const tags = new Map()
169
- for (const [key, value] of Object.entries(userTags ?? {})) {
170
- tags.set(key, `${key}:${value}`)
169
+ if ((userTags) != null) {
170
+ for (const [key, value] of Object.entries(userTags)) {
171
+ tags.set(key, `${key}:${value}`)
172
+ }
171
173
  }
172
174
  for (const [key, value] of Object.entries(autoTags)) {
173
175
  if (value !== undefined && value !== null && value !== '') tags.set(key, `${key}:${value}`)
@@ -80,6 +80,8 @@ class NoopLLMObs {
80
80
 
81
81
  submitEvaluation (llmobsSpanContext, options) {}
82
82
 
83
+ submitFeedback (options) {}
84
+
83
85
  flush () {}
84
86
 
85
87
  registerProcessor (processor) {}
@@ -118,7 +118,7 @@ class DdTelemetryPlugin extends BaseLLMObsPlugin {
118
118
  * @returns {string | undefined}
119
119
  */
120
120
  findToolName (toolName, toolDescription) {
121
- if (Number.isNaN(Number.parseInt(toolName))) return toolName
121
+ if (Number.isNaN(Number.parseInt(toolName, 10))) return toolName
122
122
 
123
123
  for (const availableTool of this.#availableTools) {
124
124
  const description = availableTool.description
@@ -322,7 +322,7 @@ function getToolNameFromTags (tags) {
322
322
  const toolName = tags['ai.toolCall.name']
323
323
  if (!toolName) return null
324
324
 
325
- const parsedToolName = Number.parseInt(toolName)
325
+ const parsedToolName = Number.parseInt(toolName, 10)
326
326
  if (!Number.isNaN(parsedToolName)) return null
327
327
 
328
328
  return toolName
@@ -72,10 +72,10 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
72
72
  const cacheWriteTokenCount = headers['x-amzn-bedrock-cache-write-input-token-count']
73
73
 
74
74
  pendingTokenHeaders.set(requestId, {
75
- inputTokensFromHeaders: inputTokenCount && Number.parseInt(inputTokenCount),
76
- outputTokensFromHeaders: outputTokenCount && Number.parseInt(outputTokenCount),
77
- cacheReadTokensFromHeaders: cacheReadTokenCount && Number.parseInt(cacheReadTokenCount),
78
- cacheWriteTokensFromHeaders: cacheWriteTokenCount && Number.parseInt(cacheWriteTokenCount),
75
+ inputTokensFromHeaders: inputTokenCount && Number.parseInt(inputTokenCount, 10),
76
+ outputTokensFromHeaders: outputTokenCount && Number.parseInt(outputTokenCount, 10),
77
+ cacheReadTokensFromHeaders: cacheReadTokenCount && Number.parseInt(cacheReadTokenCount, 10),
78
+ cacheWriteTokensFromHeaders: cacheWriteTokenCount && Number.parseInt(cacheWriteTokenCount, 10),
79
79
  })
80
80
  })
81
81
 
@@ -138,7 +138,7 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
138
138
  #tagCommon ({ span, requestParams, textAndResponseReason, tokensFromHeaders }) {
139
139
  this._tagger.tagMetadata(span, {
140
140
  temperature: Number.parseFloat(requestParams.temperature) || 0,
141
- max_tokens: Number.parseInt(requestParams.maxTokens) || 0,
141
+ max_tokens: Number.parseInt(requestParams.maxTokens, 10) || 0,
142
142
  })
143
143
  this._tagger.tagLLMIO(span, requestParams.prompt, textAndResponseReason.messages)
144
144
  this._tagger.tagMetrics(span, extractTokens({
@@ -113,7 +113,7 @@ class BaseLangChainLLMObsPlugin extends LLMObsPlugin {
113
113
  }
114
114
 
115
115
  if (maxTokens) {
116
- metadata.maxTokens = Number.parseInt(maxTokens)
116
+ metadata.maxTokens = Number.parseInt(maxTokens, 10)
117
117
  }
118
118
 
119
119
  this._tagger.tagMetadata(span, metadata)