dd-trace 5.133.0 → 5.134.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 (55) hide show
  1. package/ci/vitest-no-worker-init-runner-setup.mjs +27 -0
  2. package/index.d.ts +16 -5
  3. package/package.json +17 -16
  4. package/packages/datadog-instrumentations/index.js +2 -1
  5. package/packages/datadog-instrumentations/src/cucumber.js +17 -8
  6. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +27 -8
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/register.js +9 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/jest.js +12 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/vitest.js +8 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +5 -0
  13. package/packages/datadog-instrumentations/src/jest.js +69 -57
  14. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -2
  15. package/packages/datadog-instrumentations/src/nova-sonic/session.js +25 -16
  16. package/packages/datadog-instrumentations/src/playwright.js +2 -0
  17. package/packages/datadog-instrumentations/src/stripe.js +23 -12
  18. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -0
  19. package/packages/datadog-instrumentations/src/vitest-main.js +7 -3
  20. package/packages/datadog-instrumentations/src/vitest-util.js +5 -2
  21. package/packages/datadog-instrumentations/src/vitest.js +8 -0
  22. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +3 -0
  23. package/packages/datadog-plugin-cypress/src/support.js +2 -0
  24. package/packages/dd-trace/src/aiguard/evaluation.js +143 -1
  25. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -1
  26. package/packages/dd-trace/src/appsec/stack_trace.js +3 -2
  27. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +3 -2
  28. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  29. package/packages/dd-trace/src/config/index.js +45 -0
  30. package/packages/dd-trace/src/config/supported-configurations.json +9 -0
  31. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +6 -5
  32. package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -5
  33. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +4 -6
  34. package/packages/dd-trace/src/debugger/unsupported-runtime.js +16 -0
  35. package/packages/dd-trace/src/llmobs/agent-manifest.js +19 -2
  36. package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
  37. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +5 -0
  38. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +9 -1
  39. package/packages/dd-trace/src/llmobs/sdk.js +15 -4
  40. package/packages/dd-trace/src/llmobs/span_processor.js +12 -0
  41. package/packages/dd-trace/src/llmobs/tagger.js +18 -8
  42. package/packages/dd-trace/src/llmobs/util.js +10 -4
  43. package/packages/dd-trace/src/openfeature/index.js +6 -0
  44. package/packages/dd-trace/src/openfeature/writers/util.js +4 -4
  45. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +18 -1
  46. package/packages/dd-trace/src/plugin_manager.js +1 -2
  47. package/packages/dd-trace/src/plugins/tracing.js +0 -1
  48. package/packages/dd-trace/src/plugins/util/git.js +91 -35
  49. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -1
  50. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  51. package/packages/dd-trace/src/plugins/util/web.js +90 -20
  52. package/packages/dd-trace/src/span_stats.js +4 -1
  53. package/packages/dd-trace/src/telemetry/logs/log-collector.js +1 -1
  54. package/packages/dd-trace/src/util.js +8 -4
  55. package/vendor/dist/@apm-js-collab/code-transformer/index.js +8 -8
@@ -16,6 +16,8 @@ const {
16
16
  METADATA,
17
17
  COST_TAGS,
18
18
  AGENT_MANIFEST,
19
+ AGENT_VERSION,
20
+ AGENT_VERSION_TAG_KEY,
19
21
  TOOL_DEFINITIONS,
20
22
  EXPERIMENT_INPUT,
21
23
  EXPERIMENT_OUTPUT,
@@ -32,6 +34,7 @@ const {
32
34
  PARENT_ID_KEY,
33
35
  PARENT_AGENT_NAME,
34
36
  PARENT_AGENT_SPAN_ID,
37
+ PARENT_AGENT_VERSION,
35
38
  SESSION_ID,
36
39
  NAME,
37
40
  INPUT_PROMPT,
@@ -406,6 +409,15 @@ class LLMObsSpanProcessor {
406
409
  const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
407
410
  if (existingTags) tags = { ...tags, ...existingTags }
408
411
 
412
+ const mlObsTags = LLMObsTagger.tagMap.get(span)
413
+ // Resolved here because a span can become an agent after registration. An agent without its own version, and
414
+ // any other span, reports the version inherited at registration. Either wins over a user
415
+ // tag of the same name.
416
+ const agentVersion = mlObsTags?.[SPAN_KIND] === 'agent'
417
+ ? mlObsTags[AGENT_VERSION] || mlObsTags[PARENT_AGENT_VERSION]
418
+ : mlObsTags?.[PARENT_AGENT_VERSION]
419
+ if (agentVersion) tags[AGENT_VERSION_TAG_KEY] = agentVersion
420
+
409
421
  return tags
410
422
  }
411
423
 
@@ -19,6 +19,7 @@ const {
19
19
  METADATA,
20
20
  COST_TAGS,
21
21
  AGENT_MANIFEST,
22
+ AGENT_VERSION,
22
23
  METRICS,
23
24
  TOOL_DEFINITIONS,
24
25
  PARENT_ID_KEY,
@@ -28,6 +29,7 @@ const {
28
29
  NAME,
29
30
  PARENT_AGENT_NAME,
30
31
  PARENT_AGENT_SPAN_ID,
32
+ PARENT_AGENT_VERSION,
31
33
  PROPAGATED_PARENT_ID_KEY,
32
34
  PROPAGATED_PARENT_AGENT_ID_KEY,
33
35
  PROPAGATED_PARENT_AGENT_NAME_KEY,
@@ -116,6 +118,7 @@ class LLMObsTagger {
116
118
  kind,
117
119
  name,
118
120
  integration,
121
+ agentVersion,
119
122
  _decorator,
120
123
  } = {}) {
121
124
  // Do not register it in the map if it does not have an LLMObs span kind.
@@ -193,6 +196,8 @@ class LLMObsTagger {
193
196
  storage.getStore()?.agentDeclarations
194
197
  )
195
198
  if (agentDeclarations) this.#applyAgentDeclarations(span, agentDeclarations)
199
+ // The span's own version wins over the enclosing contexts.
200
+ if (agentVersion) this._setTag(span, AGENT_VERSION, agentVersion)
196
201
 
197
202
  // apply annotation context name
198
203
  const annotationContextName = annotationContext?.name
@@ -213,15 +218,16 @@ class LLMObsTagger {
213
218
 
214
219
  /**
215
220
  * Applies the agents declared by the enclosing annotation contexts, outermost first, to every span in the block.
216
- * The manifest is emitted only if the span is an agent when it finishes, since some integrations promote a span
217
- * to an agent after registration.
221
+ * Both are emitted only if the span is an agent when it finishes, since some integrations promote a span to an
222
+ * agent after registration.
218
223
  *
219
224
  * @param {import('../opentracing/span')} span
220
225
  * @param {import('./agent-manifest').AgentDeclaration[]} declarations
221
226
  */
222
227
  #applyAgentDeclarations (span, declarations) {
223
228
  for (const declaration of declarations) {
224
- this.#tagAgentManifestFields(span, declaration.manifest)
229
+ if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
230
+ if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
225
231
  }
226
232
  }
227
233
 
@@ -233,7 +239,9 @@ class LLMObsTagger {
233
239
  */
234
240
  tagAgent (span, agent) {
235
241
  const declaration = buildAgentDeclaration(agent)
236
- if (declaration) this.#tagAgentManifestFields(span, declaration.manifest)
242
+ if (!declaration) return
243
+ if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
244
+ if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
237
245
  }
238
246
 
239
247
  /**
@@ -276,8 +284,9 @@ class LLMObsTagger {
276
284
 
277
285
  /**
278
286
  * Store the nearest agent ancestor on the span so it can be surfaced as
279
- * `meta.agent_attribution` at finish. Resolved once here, at registration, so downstream
280
- * children inherit it with a single lookup rather than walking the ancestor chain.
287
+ * `meta.agent_attribution` at finish, along with that agent's version, which the agent panel reads
288
+ * off each span to attribute its spend. Resolved once here, at registration, so downstream children
289
+ * inherit it with a single lookup rather than walking the ancestor chain.
281
290
  *
282
291
  * @param {import('../opentracing/span')} span
283
292
  * @param {import('../opentracing/span')} [parent] the LLMObs parent span, if any
@@ -285,10 +294,10 @@ class LLMObsTagger {
285
294
  // TODO: spans whose kind changes after registration (e.g. claude-agent-sdk tools promoted to
286
295
  // sub-agents) will not retroactively update already-finished children's attribution. Follow up.
287
296
  #tagAgentAttribution (span, parent) {
288
- let name, spanId
297
+ let name, spanId, version
289
298
  if (registry.has(parent)) {
290
299
  // Local LLMObs parent: attribute to it if it is an agent, else inherit its resolution.
291
- ({ name, spanId } = resolveAgentAttribution(registry.get(parent), parent))
300
+ ({ name, spanId, version } = resolveAgentAttribution(registry.get(parent), parent))
292
301
  } else if (span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY]) {
293
302
  // Distributed LLMObs parent: inherit the nearest agent propagated from upstream. The
294
303
  // name may be absent when the upstream hop ran an older SDK or its name was not
@@ -300,6 +309,7 @@ class LLMObsTagger {
300
309
 
301
310
  if (name != null) this._setTag(span, PARENT_AGENT_NAME, name)
302
311
  if (spanId != null) this._setTag(span, PARENT_AGENT_SPAN_ID, spanId)
312
+ if (version) this._setTag(span, PARENT_AGENT_VERSION, version)
303
313
  }
304
314
 
305
315
  // TODO: similarly for the following `tag` methods,
@@ -8,8 +8,10 @@ const {
8
8
  SPAN_KINDS,
9
9
  SPAN_KIND,
10
10
  NAME,
11
+ AGENT_VERSION,
11
12
  PARENT_AGENT_NAME,
12
13
  PARENT_AGENT_SPAN_ID,
14
+ PARENT_AGENT_VERSION,
13
15
  } = require('./constants/tags')
14
16
 
15
17
  const DECIMAL_TRACE_ID_REGEX = /^\d+$/
@@ -325,14 +327,18 @@ function agentNameWireSafe (name) {
325
327
  *
326
328
  * @param {Record<string, unknown> | undefined} tags - Registry entry for the parent span.
327
329
  * @param {import('../opentracing/span')} [span] - The parent span itself (needed for span id / name).
328
- * @returns {{ name: string | undefined, spanId: string | undefined }}
330
+ * @returns {{ name: string | undefined, spanId: string | undefined, version: string | undefined }}
329
331
  */
330
332
  function resolveAgentAttribution (tags, span) {
331
- if (!tags) return { name: undefined, spanId: undefined }
333
+ if (!tags) return { name: undefined, spanId: undefined, version: undefined }
332
334
  if (tags[SPAN_KIND] === 'agent') {
333
- return { name: tags[NAME] || span._name, spanId: span.context().toSpanId() }
335
+ return {
336
+ name: tags[NAME] || span._name,
337
+ spanId: span.context().toSpanId(),
338
+ version: tags[AGENT_VERSION] || tags[PARENT_AGENT_VERSION],
339
+ }
334
340
  }
335
- return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID] }
341
+ return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID], version: tags[PARENT_AGENT_VERSION] }
336
342
  }
337
343
 
338
344
  function spanHasError (span) {
@@ -33,6 +33,12 @@ function _handleFlush () {
33
33
  * @param {import('../config')} config - Tracer configuration object
34
34
  */
35
35
  function enable (config) {
36
+ if (config.featureFlags?.DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED === false) {
37
+ log.debug('Feature Flags: Datadog exposure logging disabled; set %s=true to enable',
38
+ 'DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED')
39
+ return
40
+ }
41
+
36
42
  if (exposuresWriter) {
37
43
  log.warn('%s already enabled', exposuresWriter.constructor.name)
38
44
  return
@@ -31,14 +31,14 @@ const deliveryStrategies = new WeakMap()
31
31
  let missingRouteWarningLogged = false
32
32
 
33
33
  /**
34
- * Logs the unavailable exposure-delivery warning once.
34
+ * Logs the unavailable event-delivery warning once.
35
35
  *
36
36
  */
37
- function warnExposureDeliveryUnavailable () {
37
+ function warnEventDeliveryUnavailable () {
38
38
  if (missingRouteWarningLogged) return
39
39
  missingRouteWarningLogged = true
40
40
  logger.warn(
41
- 'Feature Flags exposure delivery is disabled because no compatible local EVP route or direct intake ' +
41
+ 'Feature Flags event delivery is disabled because no compatible local EVP route or direct intake ' +
42
42
  'credentials are available.'
43
43
  )
44
44
  }
@@ -169,7 +169,7 @@ function setAgentlessStrategy (config, setWriterEnabledValue) {
169
169
  if (error) {
170
170
  logger.debug('FFE Writer disabled - error getting local receiver info: %s', error.message)
171
171
  }
172
- warnExposureDeliveryUnavailable()
172
+ warnEventDeliveryUnavailable()
173
173
  markUnavailable()
174
174
  })
175
175
  }
@@ -95,6 +95,11 @@ const DD_ERROR_META_KEYS = new Set(['error.message'])
95
95
  class OtlpTraceTransformer extends OtlpTransformerBase {
96
96
  #otelTraceSemanticsEnabled
97
97
 
98
+ // Span meta entries with the same key and value as a resource attribute are already carried by
99
+ // the resource (e.g. DD_TAGS), so they are not repeated as span attributes.
100
+ /** @type {import('@opentelemetry/api').Attributes} */
101
+ #resourceAttributes
102
+
98
103
  /**
99
104
  * Creates a new OtlpTraceTransformer instance.
100
105
  *
@@ -104,6 +109,16 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
104
109
  constructor (resourceAttributes, otelTraceSemanticsEnabled) {
105
110
  super(resourceAttributes, 'http/json', 'traces')
106
111
  this.#otelTraceSemanticsEnabled = otelTraceSemanticsEnabled
112
+ this.#resourceAttributes = resourceAttributes
113
+ }
114
+
115
+ /**
116
+ * @override
117
+ * @param {import('@opentelemetry/api').Attributes} resourceAttributes - Resource attributes
118
+ */
119
+ updateResourceAttributes (resourceAttributes) {
120
+ super.updateResourceAttributes(resourceAttributes)
121
+ this.#resourceAttributes = resourceAttributes
107
122
  }
108
123
 
109
124
  /**
@@ -216,11 +231,13 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
216
231
  }
217
232
  }
218
233
 
219
- // Add meta string tags, skipping keys that map to dedicated OTLP fields
234
+ // Add meta string tags, skipping keys that map to dedicated OTLP fields and tags the
235
+ // resource already carries with the same value (e.g. global tags from DD_TAGS)
220
236
  if (span.meta) {
221
237
  for (const [key, value] of Object.entries(span.meta)) {
222
238
  if (EXCLUDED_META_KEYS.has(key)) continue
223
239
  if (this.#otelTraceSemanticsEnabled && DD_ERROR_META_KEYS.has(key)) continue
240
+ if (this.#resourceAttributes[key] === value) continue
224
241
  attributes.push({ key, value: { stringValue: value } })
225
242
  }
226
243
  }
@@ -102,8 +102,7 @@ module.exports = class PluginManager {
102
102
  loadPlugin (name) {
103
103
  const Plugin = pluginClasses[name]
104
104
 
105
- if (!Plugin) return
106
- if (!this._tracerConfig) return // TODO: don't wait for tracer to be initialized
105
+ if (!Plugin || !this._tracerConfig) return // TODO: don't wait for tracer to be initialized
107
106
 
108
107
  // Check if this is a Test Optimization plugin and Test Optimization is not enabled
109
108
  if (TEST_OPTIMIZATION_PLUGINS.has(name) && !this._tracerConfig.isCiVisibility) {
@@ -267,7 +267,6 @@ class TracingPlugin extends Plugin {
267
267
  analyticsSampler.sample(span, config.measured)
268
268
 
269
269
  // TODO: Remove this after migration to TracingChannel is done.
270
- // eslint-disable-next-line unicorn/no-unnecessary-boolean-comparison -- Context objects use the other branch.
271
270
  if (enterOrCtx === true) {
272
271
  legacyStorage.enterWith({ ...store, span })
273
272
  } else if (enterOrCtx) {
@@ -3,8 +3,10 @@
3
3
  const os = require('os')
4
4
  const path = require('path')
5
5
  const fs = require('fs')
6
+ const { execFileSync } = require('node:child_process')
6
7
 
7
8
  const log = require('../../log')
9
+ const { getEnvironmentVariables } = require('../../config/helper')
8
10
  const {
9
11
  incrementCountMetric,
10
12
  distributionMetric,
@@ -49,7 +51,8 @@ function sanitizedExec (
49
51
  operationMetric,
50
52
  durationMetric,
51
53
  errorMetric,
52
- shouldTrim = true
54
+ shouldTrim = true,
55
+ executionOptions
53
56
  ) {
54
57
  return legacyStorage.run({ noop: true }, () => {
55
58
  let startTime
@@ -60,7 +63,14 @@ function sanitizedExec (
60
63
  startTime = Date.now()
61
64
  }
62
65
  try {
63
- let result = cachedExec('git', flags, { stdio: 'pipe' }).toString()
66
+ const exec = executionOptions?.uncached ? execFileSync : cachedExec
67
+ /** @type {import('node:child_process').ExecFileSyncOptions} */
68
+ const options = { stdio: 'pipe' }
69
+ if (executionOptions?.timeout) options.timeout = executionOptions.timeout
70
+ if (executionOptions?.localOnly) {
71
+ options.env = { ...getEnvironmentVariables(), GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '' }
72
+ }
73
+ let result = exec('git', flags, options).toString()
64
74
 
65
75
  if (shouldTrim) {
66
76
  result = result.replaceAll(/(\r\n|\n|\r)/g, '')
@@ -79,6 +89,7 @@ function sanitizedExec (
79
89
  })
80
90
  }
81
91
  log.error('Git plugin error executing command', err)
92
+ if (executionOptions?.throwOnError) throw err
82
93
  return ''
83
94
  }
84
95
  })
@@ -103,12 +114,14 @@ function isGitAvailable () {
103
114
  }
104
115
  }
105
116
 
106
- function isShallowRepository () {
117
+ function isShallowRepository (uncached = false) {
107
118
  return sanitizedExec(
108
119
  ['rev-parse', '--is-shallow-repository'],
109
120
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'check_shallow' } },
110
121
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'check_shallow' } },
111
- { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'check_shallow' } }
122
+ { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'check_shallow' } },
123
+ true,
124
+ { uncached }
112
125
  ) === 'true'
113
126
  }
114
127
 
@@ -223,6 +236,13 @@ function getLatestCommits () {
223
236
  }
224
237
 
225
238
  function getGitDiff (baseCommit, targetCommit) {
239
+ // Fetch the required snapshots explicitly; a depth-1 metadata fetch does not provide their ancestry.
240
+ for (const commit of targetCommit ? [baseCommit, targetCommit] : [baseCommit]) {
241
+ if (!hasLocalGitObject(commit)) {
242
+ // Use the current checkout state so a stale cache cannot shorten a complete clone.
243
+ fetchHeadCommitSha(commit, isShallowRepository(true))
244
+ }
245
+ }
226
246
  const flags = ['diff', '-U0', '--word-diff=porcelain', baseCommit]
227
247
  if (targetCommit) {
228
248
  flags.push(targetCommit)
@@ -232,7 +252,8 @@ function getGitDiff (baseCommit, targetCommit) {
232
252
  { name: TELEMETRY_GIT_COMMAND, tags: { command: 'diff' } },
233
253
  { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'diff' } },
234
254
  { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'diff' } },
235
- false // important not to trim or we'll lose the line breaks which we need to detect impacted tests
255
+ false, // important not to trim or we'll lose the line breaks which we need to detect impacted tests
256
+ { uncached: true }
236
257
  )
237
258
  }
238
259
 
@@ -464,6 +485,7 @@ function getGitMetadata (ciMetadata) {
464
485
  authorEmail: ciAuthorEmail,
465
486
  ciWorkspacePath,
466
487
  headCommitSha,
488
+ pullRequestBaseBranchSha,
467
489
  } = ciMetadata
468
490
 
469
491
  // With stdio: 'pipe', errors in this command will not be output to the parent process,
@@ -486,8 +508,25 @@ function getGitMetadata (ciMetadata) {
486
508
  }
487
509
 
488
510
  if (headCommitSha) {
489
- if (isShallowRepository()) {
490
- fetchHeadCommitSha(headCommitSha)
511
+ const shallow = isShallowRepository(true)
512
+ const fetchedAtDepthOne = shallow && fetchHeadCommitSha(headCommitSha)
513
+ const headFlags = [
514
+ 'show',
515
+ '-s',
516
+ '--format=\'%H","%aI","%an","%ae","%cI","%cn","%ce","%B\'',
517
+ headCommitSha,
518
+ ]
519
+ // Only explicit, bounded fetches may contact the remote during head metadata recovery.
520
+ const headOptions = { uncached: true, localOnly: true }
521
+ let headInfo = sanitizedExec(headFlags, null, null, null, false, headOptions)
522
+ if (!headInfo && (fetchedAtDepthOne || !shallow)) {
523
+ fetchHeadCommitSha(headCommitSha, false)
524
+ headInfo = sanitizedExec(headFlags, null, null, null, false, headOptions)
525
+ }
526
+
527
+ // Prepare a known diff base while the remote is available, including when Git upload is disabled.
528
+ if (shallow && pullRequestBaseBranchSha && !hasLocalGitObject(pullRequestBaseBranchSha)) {
529
+ fetchHeadCommitSha(pullRequestBaseBranchSha)
491
530
  }
492
531
 
493
532
  const [
@@ -499,18 +538,7 @@ function getGitMetadata (ciMetadata) {
499
538
  headCommitterName,
500
539
  headCommitterEmail,
501
540
  headCommitMessage,
502
- ] = sanitizedExec(
503
- [
504
- 'show',
505
- '-s',
506
- '--format=\'%H","%aI","%an","%ae","%cI","%cn","%ce","%B\'',
507
- headCommitSha,
508
- ],
509
- null,
510
- null,
511
- null,
512
- false
513
- ).split('","')
541
+ ] = headInfo.split('","')
514
542
 
515
543
  if (gitHeadCommitSha) {
516
544
  tags[GIT_COMMIT_HEAD_AUTHOR_DATE] = headAuthorDate
@@ -558,23 +586,51 @@ function getGitInformationDiscrepancy () {
558
586
  return { gitRepositoryUrl, gitCommitSHA }
559
587
  }
560
588
 
561
- function fetchHeadCommitSha (headSha) {
562
- const remoteName = getGitRemoteName()
589
+ /** @param {string} sha */
590
+ function hasLocalGitObject (sha) {
591
+ return legacyStorage.run({ noop: true }, () => {
592
+ try {
593
+ execFileSync('git', ['cat-file', '-e', sha], {
594
+ stdio: 'pipe',
595
+ // Older Git versions ignore GIT_NO_LAZY_FETCH, so also disallow transports for this local-only probe.
596
+ env: { ...getEnvironmentVariables(), GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '' },
597
+ })
598
+ return true
599
+ } catch {
600
+ return false
601
+ }
602
+ })
603
+ }
563
604
 
564
- sanitizedExec(
565
- [
566
- 'fetch',
567
- '--update-shallow',
568
- '--filter=blob:none',
569
- '--recurse-submodules=no',
570
- '--no-write-fetch-head',
571
- remoteName,
572
- headSha,
573
- ],
574
- { name: TELEMETRY_GIT_COMMAND, tags: { command: 'fetch_head_commit_sha' } },
575
- { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'fetch_head_commit_sha' } },
576
- { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'fetch_head_commit_sha' } }
577
- )
605
+ function fetchHeadCommitSha (headSha, useDepth = true) {
606
+ const remoteName = getGitRemoteName()
607
+ // Existing heads retain the original repair/history behavior; a failed bounded fetch also retries that path.
608
+ const depths = useDepth && !hasLocalGitObject(headSha) ? [true, false] : [false]
609
+ for (const shallow of depths) {
610
+ try {
611
+ sanitizedExec(
612
+ [
613
+ 'fetch',
614
+ ...(shallow ? ['--depth=1'] : []),
615
+ '--update-shallow',
616
+ '--filter=blob:none',
617
+ '--recurse-submodules=no',
618
+ '--no-write-fetch-head',
619
+ remoteName,
620
+ headSha,
621
+ ],
622
+ { name: TELEMETRY_GIT_COMMAND, tags: { command: 'fetch_head_commit_sha' } },
623
+ { name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'fetch_head_commit_sha' } },
624
+ { name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'fetch_head_commit_sha' } },
625
+ true,
626
+ { uncached: true, throwOnError: true, timeout: 5000 }
627
+ )
628
+ return shallow
629
+ } catch {
630
+ // sanitizedExec records the failure; retry without a depth limit if the bounded fetch failed.
631
+ }
632
+ }
633
+ return false
578
634
  }
579
635
 
580
636
  module.exports = {
@@ -12,8 +12,9 @@ const NODE_MODULES_PATTERN_START = `node_modules${sep}`
12
12
  * current file path ends with the expected path structure from the repo root.
13
13
  * This is needed for local and CI where dd-trace-js is not in node_modules.
14
14
  * In production, these frames are already filtered by isNodeModulesFrame.
15
+ * Bundled into ESM output, `__filename` is not defined, and dd-trace is not running from its repo.
15
16
  */
16
- const SHOULD_FILTER_DD_TRACE_INSTRUMENTAION = __filename.endsWith(
17
+ const SHOULD_FILTER_DD_TRACE_INSTRUMENTAION = typeof __filename === 'string' && __filename.endsWith(
17
18
  join('packages', 'dd-trace', 'src', 'plugins', 'util', 'stacktrace.js')
18
19
  )
19
20
 
@@ -799,6 +799,8 @@ function getTestEnvironmentMetadata (testFramework, config, shouldSkipGitMetadat
799
799
  commitMessage,
800
800
  ciWorkspacePath,
801
801
  headCommitSha,
802
+ pullRequestBaseBranchSha: userProvidedGitMetadata[GIT_PULL_REQUEST_BASE_BRANCH_SHA] ||
803
+ ciMetadata[GIT_PULL_REQUEST_BASE_BRANCH_SHA],
802
804
  })
803
805
  }
804
806
 
@@ -321,20 +321,16 @@ const web = {
321
321
  const writeHead = res.writeHead
322
322
 
323
323
  return function (statusCode, statusMessage, headers) {
324
- // CORS preflight tagging only matters for OPTIONS requests. Skip the
325
- // getHeaders() spread + isOriginAllowed work entirely for the common
326
- // GET / POST / etc. case. Node's http module passes `req.method`
327
- // through unchanged, so all standard methods are uppercase; the
328
- // `toLowerCase` fallback covers any non-standard caller.
329
324
  if (req.method === 'OPTIONS' || req.method.toLowerCase() === 'options') {
330
- headers = typeof statusMessage === 'string' ? headers : statusMessage
331
- headers = { ...res.getHeaders(), ...headers }
332
-
333
- if (isOriginAllowed(req, headers)) {
334
- addAllowHeaders(req, res, headers)
325
+ const headerIndex = typeof statusMessage === 'string' || headers != null ? 2 : 1
326
+ const updatedHeaders = addAllowHeaders(req, res, arguments[headerIndex])
327
+ if (updatedHeaders) {
328
+ arguments[headerIndex] = updatedHeaders
329
+ const result = writeHead.apply(this, arguments)
330
+ if (context.config.headers.length > 0) context.responseHeaders = updatedHeaders
331
+ return result
335
332
  }
336
333
  }
337
-
338
334
  return writeHead.apply(this, arguments)
339
335
  }
340
336
  },
@@ -362,8 +358,28 @@ function normalizeHeadersCarrier (headers) {
362
358
  return carrier
363
359
  }
364
360
 
361
+ /**
362
+ * @param {import('node:http').IncomingMessage} req
363
+ * @param {import('node:http').ServerResponse | import('node:http2').Http2ServerResponse} res
364
+ * @param {ResponseHeaders} headers
365
+ */
365
366
  function addAllowHeaders (req, res, headers) {
366
- const allowHeaders = splitHeader(headers['access-control-allow-headers'])
367
+ if (!req.headers.origin || !req.headers['access-control-request-headers'] ||
368
+ res.headersSent || res.stream?.destroyed || res.stream?.closed) return
369
+
370
+ const originKey = findHeader(headers, 'access-control-allow-origin')
371
+ const allowOrigin = originKey === undefined
372
+ ? res.getHeader('access-control-allow-origin')
373
+ : getHeaderValue(headers, originKey)
374
+ if (allowOrigin !== '*' && allowOrigin !== req.headers.origin) return
375
+
376
+ const allowKey = findHeader(headers, 'access-control-allow-headers')
377
+ const existing = allowKey === undefined
378
+ ? res.getHeader('access-control-allow-headers')
379
+ : getHeaderValue(headers, allowKey)
380
+ if (allowKey !== undefined && existing == null) return
381
+
382
+ const allowHeaders = splitHeader(existing)
367
383
  const requestHeaders = splitHeader(req.headers['access-control-request-headers'])
368
384
  const contextHeaders = [
369
385
  'baggage',
@@ -384,19 +400,69 @@ function addAllowHeaders (req, res, headers) {
384
400
  }
385
401
 
386
402
  if (allowHeaders.length > 0) {
387
- res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(','))
403
+ const value = uniq(allowHeaders).join(',')
404
+ if (headers) return setAllowHeaders(headers, allowKey, value)
405
+ res.setHeader('access-control-allow-headers', value)
406
+ }
407
+ }
408
+
409
+ /** @typedef {import('node:http').OutgoingHttpHeader} HeaderValue */
410
+ /** @typedef {import('node:http').OutgoingHttpHeaders | Array<HeaderValue | [string, HeaderValue]>} ResponseHeaders */
411
+
412
+ /**
413
+ * @param {ResponseHeaders} headers
414
+ * @param {string} name
415
+ */
416
+ function findHeader (headers, name) {
417
+ if (!headers) return
418
+ if (Array.isArray(headers)) {
419
+ const tuples = Array.isArray(headers[0])
420
+ for (let i = headers.length - (tuples ? 1 : 2); i >= 0; i -= tuples ? 1 : 2) {
421
+ const key = tuples ? headers[i][0] : headers[i]
422
+ if (typeof key === 'string' && key.toLowerCase() === name) return tuples ? i : i + 1
423
+ }
424
+ } else {
425
+ const keys = Object.keys(headers)
426
+ for (let i = keys.length - 1; i >= 0; i--) {
427
+ if (keys[i].toLowerCase() === name) return keys[i]
428
+ }
388
429
  }
389
430
  }
390
431
 
391
- function isOriginAllowed (req, headers) {
392
- const origin = req.headers.origin
393
- const allowOrigin = headers['access-control-allow-origin']
432
+ /**
433
+ * @param {ResponseHeaders} headers
434
+ * @param {string | number} key
435
+ */
436
+ function getHeaderValue (headers, key) {
437
+ return Array.isArray(headers) && Array.isArray(headers[0]) ? headers[key][1] : headers[key]
438
+ }
394
439
 
395
- return origin && (allowOrigin === '*' || allowOrigin === origin)
440
+ /**
441
+ * @param {ResponseHeaders} headers
442
+ * @param {string | number | undefined} key
443
+ * @param {string} value
444
+ */
445
+ function setAllowHeaders (headers, key, value) {
446
+ if (Array.isArray(headers)) {
447
+ const result = []
448
+ const tuples = Array.isArray(headers[0])
449
+ // Native writeHead ignores custom array iterators.
450
+ for (let i = 0; i < headers.length; i++) result.push(headers[i])
451
+ if (key !== undefined) {
452
+ result[key] = tuples ? [headers[key][0], value] : value
453
+ } else if (tuples) {
454
+ result.push(['access-control-allow-headers', value])
455
+ } else {
456
+ result.push('access-control-allow-headers', value)
457
+ }
458
+ return result
459
+ }
460
+ return { ...headers, [key ?? 'access-control-allow-headers']: value }
396
461
  }
397
462
 
398
463
  function splitHeader (str) {
399
- return typeof str === 'string' ? str.split(',').map((header) => header.trim()) : []
464
+ if (Array.isArray(str)) str = str.join(',')
465
+ return str === undefined ? [] : String(str).split(',').map((header) => header.trim())
400
466
  }
401
467
 
402
468
  function addRequestTags (context, spanType) {
@@ -527,10 +593,14 @@ function addRequestHeaders (context) {
527
593
  }
528
594
 
529
595
  function addResponseHeaders (context) {
530
- const { res, config, span, inferredProxySpan } = context
596
+ const { res, config, span, inferredProxySpan, responseHeaders } = context
531
597
 
532
598
  for (const [key, tag] of config.headers) {
533
- const resHeader = res.getHeader(key)
599
+ let resHeader = res.getHeader(key)
600
+ if (resHeader === undefined && responseHeaders) {
601
+ const headerKey = findHeader(responseHeaders, key)
602
+ if (headerKey !== undefined) resHeader = getHeaderValue(responseHeaders, headerKey)
603
+ }
534
604
  if (resHeader) {
535
605
  const tagName = tag || `${HTTP_RESPONSE_HEADERS}.${key}`
536
606
  span.setTag(tagName, resHeader)
@@ -208,6 +208,7 @@ class SpanStatsProcessor {
208
208
  env,
209
209
  tags,
210
210
  version: appVersion,
211
+ reportHostname,
211
212
  _DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL: flushIntervalMs,
212
213
  } = config
213
214
  if (!otlpExporter) {
@@ -217,7 +218,9 @@ class SpanStatsProcessor {
217
218
  this.interval = intervalMs / 1e3
218
219
  this.bucketSizeNs = intervalMs * 1e6
219
220
  this.buckets = new TimeBuckets(Boolean(otlpExporter))
220
- this.hostname = os.hostname()
221
+ // A non-empty client hostname takes precedence over the agent's own hostname for the resulting
222
+ // metrics, so only send it when explicitly requested.
223
+ this.hostname = reportHostname ? os.hostname() : ''
221
224
  this.enabled = enabled
222
225
  this.otlpExporter = otlpExporter || null
223
226
  this.env = env