dd-trace 6.15.0 → 6.16.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 (81) hide show
  1. package/README.md +15 -0
  2. package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
  3. package/ci/test-optimization-validation/package-check.js +4 -1
  4. package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
  5. package/index.d.ts +4 -0
  6. package/next.d.ts +27 -0
  7. package/next.js +3 -0
  8. package/package.json +15 -11
  9. package/packages/datadog-esbuild/index.js +14 -1
  10. package/packages/datadog-esbuild/src/resolver.js +284 -0
  11. package/packages/datadog-esbuild/src/utils.js +163 -119
  12. package/packages/datadog-instrumentations/src/ai.js +39 -97
  13. package/packages/datadog-instrumentations/src/anthropic.js +76 -134
  14. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
  15. package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
  16. package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
  17. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  18. package/packages/datadog-instrumentations/src/bullmq.js +1 -1
  19. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  20. package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
  21. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  22. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
  23. package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
  24. package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
  25. package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
  32. package/packages/datadog-instrumentations/src/langchain.js +1 -1
  33. package/packages/datadog-instrumentations/src/langgraph.js +1 -1
  34. package/packages/datadog-instrumentations/src/mercurius.js +1 -1
  35. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
  36. package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
  37. package/packages/datadog-instrumentations/src/openai.js +54 -76
  38. package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
  39. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
  40. package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
  41. package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
  42. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
  43. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
  44. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
  45. package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
  46. package/packages/datadog-turbopack/index.js +212 -0
  47. package/packages/datadog-turbopack/src/constants.js +5 -0
  48. package/packages/datadog-turbopack/src/loader.js +500 -0
  49. package/packages/dd-trace/src/aiguard/index.js +2 -1
  50. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
  51. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
  52. package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
  53. package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
  54. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
  55. package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
  56. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
  57. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
  58. package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
  59. package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
  60. package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
  61. package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
  62. package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
  63. package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
  64. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  65. package/packages/dd-trace/src/config/helper.js +10 -2
  66. package/packages/dd-trace/src/config/index.js +20 -5
  67. package/packages/dd-trace/src/config/supported-configurations.json +8 -0
  68. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
  69. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
  70. package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
  71. package/packages/dd-trace/src/evp_proxy/path.js +12 -3
  72. package/packages/dd-trace/src/exporters/common/docker.js +4 -1
  73. package/packages/dd-trace/src/exporters/common/proxy.js +3 -0
  74. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  75. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
  76. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
  77. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +7 -1
  78. package/packages/dd-trace/src/plugin_manager.js +9 -3
  79. package/packages/dd-trace/src/plugins/index.js +1 -0
  80. package/packages/dd-trace/src/plugins/util/test.js +4 -1
  81. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +0 -10
@@ -1,8 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const telemetryMetrics = require('../../telemetry/metrics')
4
-
5
- const appsecMetrics = telemetryMetrics.manager.namespace('appsec')
3
+ const { appsecMetrics } = require('./common')
6
4
 
7
5
  function incrementMissingUserLogin (framework, eventType) {
8
6
  appsecMetrics.count('instrum.user_auth.missing_user_login', {
@@ -1,9 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const telemetryMetrics = require('../../telemetry/metrics')
4
- const { tags, getVersionsTags, DD_TELEMETRY_REQUEST_METRICS } = require('./common')
5
-
6
- const appsecMetrics = telemetryMetrics.manager.namespace('appsec')
3
+ const { appsecMetrics, tags, getVersionsTags, DD_TELEMETRY_REQUEST_METRICS } = require('./common')
7
4
 
8
5
  const DD_TELEMETRY_WAF_RESULT_TAGS = Symbol('_dd.appsec.telemetry.waf.result.tags')
9
6
 
@@ -126,21 +123,6 @@ function incrementWafRequests (store) {
126
123
  }
127
124
  }
128
125
 
129
- function incrementWafDurationMetrics (requestMetrics) {
130
- const { duration, durationExt, wafVersion, rulesVersion } = requestMetrics
131
- if (!duration && !durationExt) return
132
-
133
- const versionsTags = getVersionsTags(wafVersion, rulesVersion)
134
-
135
- if (duration) {
136
- appsecMetrics.distribution('waf.duration', versionsTags).track(duration)
137
- }
138
-
139
- if (durationExt) {
140
- appsecMetrics.distribution('waf.duration_ext', versionsTags).track(durationExt)
141
- }
142
- }
143
-
144
126
  function incrementTruncatedMetrics (metrics, truncationReason) {
145
127
  const truncationTags = { truncation_reason: truncationReason }
146
128
  appsecMetrics.count('waf.input_truncated', truncationTags).inc(1)
@@ -163,5 +145,4 @@ module.exports = {
163
145
  incrementWafUpdates,
164
146
  incrementWafConfigErrors,
165
147
  incrementWafRequests,
166
- incrementWafDurationMetrics,
167
148
  }
@@ -68,6 +68,7 @@ export interface GeneratedConfig {
68
68
  DD_AGENTLESS_ENABLED: boolean;
69
69
  DD_AGENTLESS_LOG_SUBMISSION_ENABLED: boolean;
70
70
  DD_AGENTLESS_LOG_SUBMISSION_URL: string | undefined;
71
+ DD_AI_GUARD_ANALYZE_STREAM_RESPONSES_ENABLED: boolean;
71
72
  DD_API_KEY: string | undefined;
72
73
  DD_APM_FLUSH_DEADLINE_MILLISECONDS: number;
73
74
  DD_APP_KEY: string | undefined;
@@ -615,6 +616,7 @@ export interface GeneratedEnvVarConfig {
615
616
  DD_AGENTLESS_ENABLED: boolean;
616
617
  DD_AGENTLESS_LOG_SUBMISSION_ENABLED: boolean;
617
618
  DD_AGENTLESS_LOG_SUBMISSION_URL: string | undefined;
619
+ DD_AI_GUARD_ANALYZE_STREAM_RESPONSES_ENABLED: boolean;
618
620
  DD_AI_GUARD_BLOCK: boolean;
619
621
  DD_AI_GUARD_ENABLED: boolean;
620
622
  DD_AI_GUARD_ENDPOINT: string | undefined;
@@ -143,10 +143,17 @@ function getEnvNameFromSource (name, source) {
143
143
  }
144
144
  }
145
145
 
146
+ /**
147
+ * @param {string} name
148
+ * @returns {boolean}
149
+ */
150
+ function isSupportedConfiguration (name) {
151
+ return supportedConfigurations[name] !== undefined || aliasToCanonical[name] !== undefined
152
+ }
153
+
146
154
  function validateAccess (name) {
147
155
  if ((name.startsWith('DD_') || name.startsWith('OTEL_')) &&
148
- !supportedConfigurations[name] &&
149
- !aliasToCanonical[name]) {
156
+ !isSupportedConfiguration(name)) {
150
157
  throw new Error(`Missing ${name} env/configuration in "supported-configurations.json" file.`)
151
158
  }
152
159
  }
@@ -284,6 +291,7 @@ function getValueFromEnvSources (name, skipDefault) {
284
291
 
285
292
  module.exports = {
286
293
  getValueFromEnvSources,
294
+ isSupportedConfiguration,
287
295
 
288
296
  /**
289
297
  * Expose raw stable config maps and warnings for consumers that need
@@ -24,6 +24,7 @@ const {
24
24
  } = require('../serverless')
25
25
  const { ORIGIN_KEY, DATADOG_MINI_AGENT_PATH } = require('../constants')
26
26
  const { appendRules } = require('../payload-tagging/config')
27
+ const { createSiteUrl } = require('../exporters/common/url')
27
28
  const ConfigBase = require('./config-base')
28
29
  const {
29
30
  getEnvironmentVariable,
@@ -495,9 +496,6 @@ class Config extends ConfigBase {
495
496
 
496
497
  const agentlessTracingEnabled = this.DD_AGENTLESS_ENABLED ||
497
498
  isTrue(getEnvironmentVariable('_DD_APM_TRACING_AGENTLESS_ENABLED'))
498
- if (agentlessTracingEnabled && !this.isCiVisibility) {
499
- setAndTrack(this, 'OTEL_TRACES_EXPORTER', 'none')
500
- }
501
499
 
502
500
  // Apply the OTel sampler when the user opted into OTel traces or explicitly set the sampler.
503
501
  // OTEL_TRACES_SAMPLER has `default: parentbased_always_on` (per OTel spec), so opt-in users
@@ -699,16 +697,33 @@ class Config extends ConfigBase {
699
697
 
700
698
  // TODO: This could likely be moved to the base class and allow easier GRPC handling
701
699
  // Default OTLP endpoints follow the configured agent host so users who point DD at a custom
702
- // agent (DD_AGENT_HOST / DD_TRACE_AGENT_URL) also reach OTLP on that host.
703
- const defaultOtlpBase = this.OTEL_EXPORTER_OTLP_ENDPOINT?.replace(/\/$/, '') ?? `http://${agentHostname}:4318`
700
+ // agent (DD_AGENT_HOST / DD_TRACE_AGENT_URL) also reach OTLP on that host. In agentless mode
701
+ // there is no agent to relay through, so OTLP goes straight to the per-site intake instead.
702
+ const otlpAgentlessOrigin = agentlessTracingEnabled && !this.OTEL_EXPORTER_OTLP_ENDPOINT
703
+ ? createSiteUrl(this.site, 'otlp')?.origin
704
+ : undefined
705
+ const defaultOtlpBase = otlpAgentlessOrigin ??
706
+ this.OTEL_EXPORTER_OTLP_ENDPOINT?.replace(/\/$/, '') ?? `http://${agentHostname}:4318`
707
+
708
+ const assignOtlpHeaderApiKey = (configName) => {
709
+ if (otlpAgentlessOrigin) {
710
+ this[configName] ??= { ...this.OTEL_EXPORTER_OTLP_HEADERS }
711
+ this[configName]['dd-api-key'] = this.DD_API_KEY
712
+ setAndTrack(this, configName, this[configName])
713
+ }
714
+ }
715
+
704
716
  if (!this.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
705
717
  setAndTrack(this, 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', `${defaultOtlpBase}/v1/logs`)
718
+ assignOtlpHeaderApiKey('OTEL_EXPORTER_OTLP_LOGS_HEADERS')
706
719
  }
707
720
  if (!this.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) {
708
721
  setAndTrack(this, 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', `${defaultOtlpBase}/v1/metrics`)
722
+ assignOtlpHeaderApiKey('OTEL_EXPORTER_OTLP_METRICS_HEADERS')
709
723
  }
710
724
  if (!this.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) {
711
725
  setAndTrack(this, 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', `${defaultOtlpBase}/v1/traces`)
726
+ assignOtlpHeaderApiKey('OTEL_EXPORTER_OTLP_TRACES_HEADERS')
712
727
  }
713
728
 
714
729
  const autoTraceMetrics = this.OTEL_TRACES_EXPORTER === 'otlp' && this.DD_METRICS_OTEL_ENABLED === true
@@ -53,6 +53,14 @@
53
53
  ]
54
54
  }
55
55
  ],
56
+ "DD_AI_GUARD_ANALYZE_STREAM_RESPONSES_ENABLED": [
57
+ {
58
+ "implementation": "A",
59
+ "type": "boolean",
60
+ "default": "false",
61
+ "description": "Buffer Vercel AI streamed responses for After Model evaluation before replaying them."
62
+ }
63
+ ],
56
64
  "DD_AI_GUARD_BLOCK": [
57
65
  {
58
66
  "implementation": "C",
@@ -6,7 +6,11 @@ const { getGeneratedPosition } = require('./source-maps')
6
6
  const session = require('./session')
7
7
  const { compile, compileSegments, templateRequiresEvaluation } = require('./condition')
8
8
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
9
- const { compileBreakpointCondition, getRemoveProbeExpression } = require('./probe_sampler')
9
+ const {
10
+ compileBreakpointCondition,
11
+ getRemoveProbeExpression,
12
+ isSnapshotProducingProbe,
13
+ } = require('./probe_sampler')
10
14
  const {
11
15
  DEFAULT_MAX_REFERENCE_DEPTH,
12
16
  DEFAULT_MAX_COLLECTION_SIZE,
@@ -76,12 +80,6 @@ async function addBreakpoint (probe) {
76
80
  }
77
81
  delete probe.segments
78
82
 
79
- // Optimize for fast calculations when probe is hit
80
- const snapshotsPerSecond = probe.sampling?.snapshotsPerSecond ?? (probe.captureSnapshot
81
- ? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE
82
- : MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE)
83
- probe.nsBetweenSampling = BigInt(Math.trunc(1 / snapshotsPerSecond * 1e9))
84
-
85
83
  // Warning: The code below relies on undocumented behavior of the inspector!
86
84
  // It expects that `await session.post('Debugger.enable')` will wait for all loaded scripts to be emitted as
87
85
  // `Debugger.scriptParsed` events. If this ever changes, we will have a race condition!
@@ -156,6 +154,15 @@ async function addBreakpoint (probe) {
156
154
  }
157
155
  }
158
156
 
157
+ // Must be calculated after `compiledCaptureExpressions` has been resolved, since capture-expression probes produce
158
+ // snapshots and therefore default to the snapshot rate.
159
+ //
160
+ // Optimize for fast calculations when probe is hit
161
+ const snapshotsPerSecond = probe.sampling?.snapshotsPerSecond ?? (isSnapshotProducingProbe(probe)
162
+ ? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE
163
+ : MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE)
164
+ probe.nsBetweenSampling = BigInt(Math.trunc(1 / snapshotsPerSecond * 1e9))
165
+
159
166
  const locationKey = generateLocationKey(scriptId, lineNumber, columnNumber)
160
167
  const breakpoint = locationToBreakpoint.get(locationKey)
161
168
 
@@ -8,6 +8,19 @@ const SAMPLER_EXPRESSION = `globalThis[Symbol.for(${JSON.stringify(DD_TRACE_SYMB
8
8
  module.exports = {
9
9
  compileBreakpointCondition,
10
10
  getRemoveProbeExpression,
11
+ isSnapshotProducingProbe,
12
+ }
13
+
14
+ /**
15
+ * Determine whether a probe produces snapshots. Probes that capture the full local state and probes that only capture
16
+ * specific expressions both emit snapshot payloads, so they share the snapshot sampling defaults and count towards the
17
+ * global snapshot rate limit.
18
+ *
19
+ * @param {{ captureSnapshot?: boolean, compiledCaptureExpressions?: object[] }} probe - The probe to inspect.
20
+ * @returns {boolean}
21
+ */
22
+ function isSnapshotProducingProbe (probe) {
23
+ return probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined
11
24
  }
12
25
 
13
26
  /**
@@ -66,7 +79,7 @@ function compileBreakpointCondition (probes) {
66
79
  */
67
80
  function compileProbeCondition (probe) {
68
81
  const sample = `$dd_sampler.makeSampleDecision(${probe.samplingIndex}, ${JSON.stringify(probe.id)}, ` +
69
- `${probe.nsBetweenSampling}n, ${probe.captureSnapshot === true || probe.compiledCaptureExpressions !== undefined})`
82
+ `${probe.nsBetweenSampling}n, ${isSnapshotProducingProbe(probe)})`
70
83
 
71
84
  if (probe.condition === undefined) {
72
85
  return `$dd_sampled = ${sample} || $dd_sampled`
@@ -2,8 +2,7 @@
2
2
 
3
3
  const { fetchAgentInfo } = require('../agent/info')
4
4
  const log = require('../log')
5
-
6
- const TRAILING_SLASHES = /\/+$/
5
+ const { stripTrailingSlashes } = require('./path')
7
6
 
8
7
  /**
9
8
  * Receiver discovery contract
@@ -81,14 +80,14 @@ function selectEVPProxyPath (agentInfo, { supportedPaths, requiredHeaders = [] }
81
80
  const advertisedPaths = new Set()
82
81
  for (const endpoint of agentInfo.endpoints) {
83
82
  if (typeof endpoint === 'string') {
84
- advertisedPaths.add(endpoint.replace(TRAILING_SLASHES, ''))
83
+ advertisedPaths.add(stripTrailingSlashes(endpoint))
85
84
  }
86
85
  }
87
86
 
88
87
  for (const supportedPath of supportedPaths) {
89
88
  if (typeof supportedPath !== 'string') continue
90
89
 
91
- const normalizedPath = supportedPath.replace(TRAILING_SLASHES, '')
90
+ const normalizedPath = stripTrailingSlashes(supportedPath)
92
91
  if (advertisedPaths.has(normalizedPath)) {
93
92
  return normalizedPath
94
93
  }
@@ -1,7 +1,16 @@
1
1
  'use strict'
2
2
 
3
3
  const LEADING_SLASHES = /^\/+/
4
- const TRAILING_SLASHES = /\/+$/
4
+
5
+ /**
6
+ * @param {string} value
7
+ * @returns {string}
8
+ */
9
+ function stripTrailingSlashes (value) {
10
+ let end = value.length
11
+ while (end > 0 && value[end - 1] === '/') end--
12
+ return end === value.length ? value : value.slice(0, end)
13
+ }
5
14
 
6
15
  /**
7
16
  * Joins a caller-supplied EVP proxy path and product endpoint.
@@ -13,10 +22,10 @@ const TRAILING_SLASHES = /\/+$/
13
22
  * @returns {string} Joined request path
14
23
  */
15
24
  function joinEVPProxyPath (basePath, endpoint) {
16
- const normalizedBasePath = basePath.replace(TRAILING_SLASHES, '')
25
+ const normalizedBasePath = stripTrailingSlashes(basePath)
17
26
  const normalizedEndpoint = endpoint.replace(LEADING_SLASHES, '')
18
27
 
19
28
  return `${normalizedBasePath}/${normalizedEndpoint}`
20
29
  }
21
30
 
22
- module.exports = { joinEVPProxyPath }
31
+ module.exports = { joinEVPProxyPath, stripTrailingSlashes }
@@ -12,7 +12,10 @@ const uuidSource =
12
12
  const containerSource = '[0-9a-f]{64}'
13
13
  const taskSource = String.raw`[0-9a-f]{32}-\d+`
14
14
  const lineReg = /^(\d+):([^:]*):(.+)$/m
15
- const entityReg = new RegExp(String.raw`.*(${uuidSource}|${containerSource}|${taskSource})(?:\.scope)?$`, 'm')
15
+ const entityReg = new RegExp(
16
+ String.raw`(?:^|/)[^/\r\n\u2028\u2029]*?(${uuidSource}|${containerSource}|${taskSource})(?:\.scope)?$`,
17
+ 'm'
18
+ )
16
19
 
17
20
  let inode = 0
18
21
  let cgroup = ''
@@ -44,6 +44,9 @@ function getHttpsProxyAgent (url, directAgent) {
44
44
  ? { keepAlive: directAgent.keepAlive, maxSockets: directAgent.maxSockets }
45
45
  : undefined
46
46
  agent = new HttpsProxyAgent(proxyUrl, options)
47
+ // AgentBase normally infers this from the node:https call stack. Interceptors such as MSW
48
+ // construct http.ClientRequest directly, so identify the target protocol explicitly.
49
+ agent.protocol = 'https:'
47
50
  agents.set(proxyUrl, agent)
48
51
  }
49
52
  return agent
@@ -66,7 +66,7 @@ function initializeOpenTelemetryMetrics (config) {
66
66
  metrics.setGlobalMeterProvider(meterProvider)
67
67
 
68
68
  // Include the final metric collection and export in lifecycle retention.
69
- registerTelemetryFlusher(done => meterProvider.forceFlush(done))
69
+ registerTelemetryFlusher(done => reader.forceFlush(done))
70
70
 
71
71
  registerResourceAttributeRefresh(exporter, () => buildGeneralResourceAttributes(config))
72
72
  }
@@ -51,11 +51,12 @@ class MeterProvider {
51
51
  }
52
52
 
53
53
  /**
54
- * @param {Function} [done] Called after the metric export completes
54
+ * @param {(error: Error | null) => void} [done] Called after shutdown completes
55
+ * @returns {void}
55
56
  */
56
- forceFlush (done) {
57
- if (this.reader) this.reader.forceFlush(done)
58
- else done?.()
57
+ shutdown (done) {
58
+ if (this.reader) this.reader.shutdown(done)
59
+ else done?.(null)
59
60
  }
60
61
  }
61
62
 
@@ -8,6 +8,19 @@ const {
8
8
  const { ObservableInstrument } = require('./instruments')
9
9
  const { nowUnixNano } = require('./time')
10
10
 
11
+ /**
12
+ * @param {(error: Error | null) => void} [done]
13
+ * @param {Error | null} [error]
14
+ */
15
+ function callDone (done, error) {
16
+ if (!done) return
17
+ try {
18
+ done(error ?? null)
19
+ } catch (callbackError) {
20
+ log.error('Error completing OTLP metrics lifecycle callback:', callbackError)
21
+ }
22
+ }
23
+
11
24
  /**
12
25
  * @typedef {import('@opentelemetry/api').Attributes} Attributes
13
26
  * @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
@@ -105,6 +118,10 @@ class PeriodicMetricReader {
105
118
  #exportInterval
106
119
  #aggregator
107
120
  #batchCallbacks = []
121
+ #exportQueue = []
122
+ #isExporting = false
123
+ #shutdownCallbacks = []
124
+ #shutdownComplete = false
108
125
 
109
126
  /**
110
127
  * Creates a new PeriodicMetricReader instance.
@@ -197,47 +214,36 @@ class PeriodicMetricReader {
197
214
 
198
215
  /**
199
216
  * Forces an immediate collection and export of all metrics.
200
- * @param {Function} [done] Called after the metric export completes
217
+ * @param {(error: Error | null) => void} [done] Called after the metric export completes
218
+ * @returns {void}
201
219
  */
202
220
  forceFlush (done) {
203
221
  if (this.#isShutdown) {
204
222
  log.warn('PeriodicMetricReader is shutdown. %d measurement(s) were dropped', this.#droppedCount)
205
- done?.()
223
+ if (this.#shutdownComplete) callDone(done)
224
+ else if (done) this.#shutdownCallbacks.push(done)
206
225
  return
207
226
  }
208
- let pending = 2
209
- const complete = () => {
210
- if (--pending === 0) done?.()
211
- }
212
-
213
- // Snapshot requests already active before starting this flush's export.
214
- try {
215
- if (typeof this.exporter.flush === 'function') this.exporter.flush(complete)
216
- else complete()
217
- } catch (error) {
218
- log.error('Error flushing OTLP metrics:', error)
219
- complete()
220
- }
221
- try {
222
- this.#collectAndExport(complete)
223
- } catch (error) {
224
- log.error('Error exporting OTLP metrics:', error)
225
- complete()
226
- }
227
+ this.#enqueueExport(true, done)
227
228
  }
228
229
 
229
230
  /**
230
231
  * Shuts down the reader and stops periodic collection.
232
+ * @param {(error: Error | null) => void} [done] Called after the final export and exporter shutdown complete
231
233
  * @returns {void}
232
234
  */
233
- shutdown () {
235
+ shutdown (done) {
234
236
  if (this.#isShutdown) {
235
237
  log.warn('PeriodicMetricReader is already shutdown')
238
+ if (this.#shutdownComplete) callDone(done)
239
+ else if (done) this.#shutdownCallbacks.push(done)
236
240
  return
237
241
  }
242
+ if (done) this.#shutdownCallbacks.push(done)
243
+
238
244
  this.#isShutdown = true
239
245
  this.#clearTimer()
240
- this.forceFlush()
246
+ this.#enqueueExport(true, error => this.#shutdownExporter(error))
241
247
  }
242
248
 
243
249
  /**
@@ -248,7 +254,7 @@ class PeriodicMetricReader {
248
254
  if (this.#timer) return
249
255
 
250
256
  this.#timer = setInterval(() => {
251
- this.#collectAndExport()
257
+ if (!this.#isExporting && this.#exportQueue.length === 0) this.#enqueueExport(false)
252
258
  }, this.#exportInterval)
253
259
  this.#timer.unref?.()
254
260
  }
@@ -264,6 +270,75 @@ class PeriodicMetricReader {
264
270
  }
265
271
  }
266
272
 
273
+ /**
274
+ * @param {boolean} flushExporter Whether to flush the exporter after export
275
+ * @param {(error: Error | null) => void} [done] Called when the queued export completes
276
+ * @returns {void}
277
+ */
278
+ #enqueueExport (flushExporter, done) {
279
+ this.#exportQueue.push({ flushExporter, done })
280
+ this.#drainExportQueue()
281
+ }
282
+
283
+ /** @returns {void} */
284
+ #drainExportQueue () {
285
+ if (this.#isExporting || this.#exportQueue.length === 0) return
286
+
287
+ this.#isExporting = true
288
+ const { flushExporter, done } = this.#exportQueue.shift()
289
+ let completed = false
290
+ const complete = error => {
291
+ if (completed) return
292
+ completed = true
293
+ this.#isExporting = false
294
+ queueMicrotask(() => this.#drainExportQueue())
295
+ callDone(done, error)
296
+ }
297
+ let exportCompleted = false
298
+ const afterExport = error => {
299
+ if (exportCompleted) return
300
+ exportCompleted = true
301
+ if (!flushExporter || typeof this.exporter.flush !== 'function') return complete(error)
302
+
303
+ try {
304
+ this.exporter.flush(flushError => complete(error || flushError))
305
+ } catch (flushError) {
306
+ log.error('Error flushing OTLP metrics:', flushError)
307
+ complete(error || flushError)
308
+ }
309
+ }
310
+
311
+ try {
312
+ this.#collectAndExport(afterExport)
313
+ } catch (error) {
314
+ log.error('Error exporting OTLP metrics:', error)
315
+ afterExport(error)
316
+ }
317
+ }
318
+
319
+ /**
320
+ * @param {Error} [exportError] Error from the final export
321
+ * @returns {void}
322
+ */
323
+ #shutdownExporter (exportError) {
324
+ let completed = false
325
+ const complete = shutdownError => {
326
+ if (completed) return
327
+ completed = true
328
+ this.#shutdownComplete = true
329
+ const callbacks = this.#shutdownCallbacks.splice(0)
330
+ for (const callback of callbacks) callDone(callback, exportError || shutdownError)
331
+ }
332
+
333
+ try {
334
+ if (typeof this.exporter.shutdown === 'function') this.exporter.shutdown(complete)
335
+ else complete()
336
+ } catch (error) {
337
+ log.error('Error shutting down OTLP metrics exporter:', error)
338
+ complete(error)
339
+ }
340
+ }
341
+
267
342
  /**
268
343
  * Collects measurements and exports metrics.
269
344
  *
@@ -322,7 +397,13 @@ class PeriodicMetricReader {
322
397
  this.#lastExportedState
323
398
  )
324
399
 
325
- this.exporter.export(metrics, callback)
400
+ this.exporter.export(metrics, result => {
401
+ if (result?.code === 1) {
402
+ callback?.(result.error || new Error('OTLP metrics export failed'))
403
+ } else {
404
+ callback?.()
405
+ }
406
+ })
326
407
  }
327
408
  }
328
409
 
@@ -177,7 +177,13 @@ class OtlpHttpExporterBase {
177
177
  this.telemetryTags[0] = `protocol:${this.#transport === https ? 'https' : 'http'}`
178
178
  }
179
179
 
180
- shutdown () {}
180
+ /**
181
+ * @param {() => void} [done]
182
+ * @returns {void}
183
+ */
184
+ shutdown (done) {
185
+ done?.()
186
+ }
181
187
  }
182
188
 
183
189
  module.exports = OtlpHttpExporterBase
@@ -2,7 +2,11 @@
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
- const { getEnvironmentVariable, getValueFromEnvSources } = require('./config/helper')
5
+ const {
6
+ getEnvironmentVariable,
7
+ getValueFromEnvSources,
8
+ isSupportedConfiguration,
9
+ } = require('./config/helper')
6
10
  const { isFalse, isTrue, normalizePluginEnvName } = require('./util')
7
11
  const plugins = require('./plugins')
8
12
  const log = require('./log')
@@ -59,12 +63,14 @@ function maybeEnable (Plugin) {
59
63
  }
60
64
 
61
65
  function getEnabled (Plugin) {
62
- const envName = `DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`
66
+ const envName = normalizePluginEnvName(`DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`)
67
+ if (!isSupportedConfiguration(envName)) return
68
+
63
69
  // skipDefault: only an explicitly configured value should drive enablement here. A registered
64
70
  // default of `false` (e.g. an opt-in plugin like `nats`) must not be read as an explicit
65
71
  // "disabled via configuration option" — that path both logs a misleading line and nulls the
66
72
  // plugin class, bypassing the opt-in handled by `loadPlugin`.
67
- return getValueFromEnvSources(normalizePluginEnvName(envName), true)
73
+ return getValueFromEnvSources(envName, true)
68
74
  }
69
75
 
70
76
  // TODO this must always be a singleton.
@@ -19,6 +19,7 @@ const plugins = {
19
19
  get '@google-cloud/pubsub' () { return require('../../../datadog-plugin-google-cloud-pubsub/src') },
20
20
  get '@google-cloud/vertexai' () { return require('../../../datadog-plugin-google-cloud-vertexai/src') },
21
21
  get '@google/genai' () { return require('../../../datadog-plugin-google-genai/src') },
22
+ get '@graphql-tools/executor' () { return require('../../../datadog-plugin-graphql/src') },
22
23
  get '@grpc/grpc-js' () { return require('../../../datadog-plugin-grpc/src') },
23
24
  get '@hapi/hapi' () { return require('../../../datadog-plugin-hapi/src') },
24
25
  get '@happy-dom/jest-environment' () { return require('../../../datadog-plugin-jest/src') },
@@ -1135,7 +1135,10 @@ function getCodeOwnersPatternRegex (pattern) {
1135
1135
  }
1136
1136
 
1137
1137
  const directoryOnly = pattern.endsWith('/')
1138
- const normalizedPattern = pattern.replace(/^\/+/, '').replace(/\/+$/, '')
1138
+ let normalizedPattern = pattern.replace(/^\/+/, '')
1139
+ let normalizedPatternEnd = normalizedPattern.length
1140
+ while (normalizedPatternEnd > 0 && normalizedPattern[normalizedPatternEnd - 1] === '/') normalizedPatternEnd--
1141
+ normalizedPattern = normalizedPattern.slice(0, normalizedPatternEnd)
1139
1142
  const anchored = pattern.startsWith('/') || normalizedPattern.includes('/')
1140
1143
 
1141
1144
  if (!normalizedPattern) {
@@ -1,10 +0,0 @@
1
- 'use strict'
2
-
3
- function splitModel (model) {
4
- if (!model) return { modelName: undefined, modelProvider: 'anthropic' }
5
- const idx = model.indexOf('/')
6
- if (idx === -1) return { modelName: model, modelProvider: 'anthropic' }
7
- return { modelName: model.slice(idx + 1), modelProvider: model.slice(0, idx) }
8
- }
9
-
10
- module.exports = { splitModel }