dd-trace 5.99.1 → 5.101.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 (101) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/index.d.ts +14 -0
  3. package/package.json +8 -8
  4. package/packages/datadog-instrumentations/src/cucumber.js +69 -5
  5. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  6. package/packages/datadog-instrumentations/src/express.js +3 -2
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  8. package/packages/datadog-instrumentations/src/hono.js +15 -4
  9. package/packages/datadog-instrumentations/src/http/client.js +20 -3
  10. package/packages/datadog-instrumentations/src/jest.js +146 -90
  11. package/packages/datadog-instrumentations/src/mocha/common.js +4 -1
  12. package/packages/datadog-instrumentations/src/mocha/main.js +43 -26
  13. package/packages/datadog-instrumentations/src/mocha/utils.js +114 -96
  14. package/packages/datadog-instrumentations/src/mocha/worker.js +7 -4
  15. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  16. package/packages/datadog-instrumentations/src/path-to-regexp.js +44 -0
  17. package/packages/datadog-instrumentations/src/playwright.js +108 -18
  18. package/packages/datadog-instrumentations/src/router.js +53 -33
  19. package/packages/datadog-instrumentations/src/vitest.js +76 -30
  20. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -1
  21. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  22. package/packages/datadog-plugin-bullmq/src/consumer.js +5 -4
  23. package/packages/datadog-plugin-bullmq/src/producer.js +37 -29
  24. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +49 -9
  25. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  26. package/packages/datadog-plugin-cypress/src/support.js +22 -21
  27. package/packages/datadog-plugin-grpc/src/client.js +1 -1
  28. package/packages/datadog-plugin-grpc/src/server.js +1 -1
  29. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  30. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  31. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -3
  32. package/packages/datadog-plugin-playwright/src/index.js +6 -0
  33. package/packages/datadog-plugin-router/src/index.js +13 -0
  34. package/packages/dd-trace/index.js +4 -3
  35. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  36. package/packages/dd-trace/src/appsec/reporter.js +4 -1
  37. package/packages/dd-trace/src/baggage.js +10 -0
  38. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  39. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  40. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  41. package/packages/dd-trace/src/config/generated-config-types.d.ts +17 -41
  42. package/packages/dd-trace/src/config/index.js +7 -60
  43. package/packages/dd-trace/src/config/normalize-service.js +31 -0
  44. package/packages/dd-trace/src/config/supported-configurations.json +15 -32
  45. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  46. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  47. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  48. package/packages/dd-trace/src/datastreams/processor.js +17 -15
  49. package/packages/dd-trace/src/datastreams/size.js +6 -2
  50. package/packages/dd-trace/src/debugger/config.js +6 -3
  51. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  52. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  53. package/packages/dd-trace/src/dogstatsd.js +10 -7
  54. package/packages/dd-trace/src/encode/0.4.js +3 -3
  55. package/packages/dd-trace/src/encode/0.5.js +2 -2
  56. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  57. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  58. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  59. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  60. package/packages/dd-trace/src/git_metadata.js +66 -0
  61. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  62. package/packages/dd-trace/src/heap_snapshots.js +4 -4
  63. package/packages/dd-trace/src/id.js +15 -26
  64. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  65. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  66. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  67. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +30 -13
  68. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  69. package/packages/dd-trace/src/llmobs/sdk.js +5 -1
  70. package/packages/dd-trace/src/llmobs/span_processor.js +28 -2
  71. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  72. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  73. package/packages/dd-trace/src/llmobs/util.js +80 -5
  74. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +2 -2
  75. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  76. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  77. package/packages/dd-trace/src/opentelemetry/context_manager.js +22 -10
  78. package/packages/dd-trace/src/opentelemetry/span-helpers.js +308 -0
  79. package/packages/dd-trace/src/opentelemetry/span.js +42 -108
  80. package/packages/dd-trace/src/opentelemetry/tracer.js +11 -36
  81. package/packages/dd-trace/src/opentracing/propagation/text_map.js +95 -36
  82. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +98 -32
  83. package/packages/dd-trace/src/opentracing/span.js +58 -49
  84. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  85. package/packages/dd-trace/src/plugins/util/ci.js +119 -32
  86. package/packages/dd-trace/src/plugins/util/test.js +293 -27
  87. package/packages/dd-trace/src/priority_sampler.js +6 -4
  88. package/packages/dd-trace/src/profiling/config.js +5 -4
  89. package/packages/dd-trace/src/profiling/ssi-heuristics.js +2 -2
  90. package/packages/dd-trace/src/propagation-hash/index.js +1 -1
  91. package/packages/dd-trace/src/proxy.js +3 -3
  92. package/packages/dd-trace/src/remote_config/index.js +5 -3
  93. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  94. package/packages/dd-trace/src/span_format.js +52 -5
  95. package/packages/dd-trace/src/span_processor.js +1 -5
  96. package/packages/dd-trace/src/spanleak.js +0 -1
  97. package/packages/dd-trace/src/telemetry/telemetry.js +7 -5
  98. package/packages/dd-trace/src/tracer_metadata.js +1 -1
  99. package/packages/dd-trace/src/util.js +17 -0
  100. package/vendor/dist/path-to-regexp/LICENSE +0 -21
  101. package/vendor/dist/path-to-regexp/index.js +0 -1
@@ -7,18 +7,24 @@ const crypto = require('crypto')
7
7
  const { LRUCache } = require('../../../../vendor/dist/lru-cache')
8
8
  const log = require('../log')
9
9
  const pick = require('../../../datadog-core/src/utils/src/pick')
10
- const { encodeVarint, decodeVarint } = require('./encoding')
10
+ const { encodeVarintInto, decodeVarint } = require('./encoding')
11
11
 
12
12
  const cache = new LRUCache({ max: 500 })
13
13
 
14
14
  const CONTEXT_PROPAGATION_KEY = 'dd-pathway-ctx'
15
15
  const CONTEXT_PROPAGATION_KEY_BASE64 = 'dd-pathway-ctx-base64'
16
16
 
17
+ const PATHWAY_CONTEXT_BYTES = 20
18
+
19
+ // Reused across `encodePathwayContext` calls; the buffer is fully rewritten before each
20
+ // `Buffer.from(...)` copy-out so callers never observe mutation between checkpoints.
21
+ const pathwayScratch = Buffer.allocUnsafe(PATHWAY_CONTEXT_BYTES)
22
+
17
23
  const logKeys = [CONTEXT_PROPAGATION_KEY, CONTEXT_PROPAGATION_KEY_BASE64]
18
24
 
19
25
  function shaHash (checkpointString) {
20
- const hash = crypto.createHash('sha256').update(checkpointString).digest('hex').slice(0, 16)
21
- return Buffer.from(hash, 'hex')
26
+ // Copy out of the 32-byte digest so the LRU cache doesn't retain it.
27
+ return Buffer.from(crypto.createHash('sha256').update(checkpointString).digest().subarray(0, 8))
22
28
  }
23
29
 
24
30
  /**
@@ -30,30 +36,25 @@ function shaHash (checkpointString) {
30
36
  */
31
37
  function computeHash (service, env, edgeTags, parentHash, propagationHashBigInt = null) {
32
38
  edgeTags.sort()
33
- const hashableEdgeTags = edgeTags.filter(item => item !== 'manual_checkpoint:true')
34
-
35
- // Cache key includes parentHash to handle fan-in/fan-out scenarios where the same
36
- // service+env+tags+propagationHash can have different parents. This ensures we cache
37
- // the complete pathway context, not just the current node's identity.
38
- const propagationPart = propagationHashBigInt ? `:${propagationHashBigInt.toString(16)}` : ''
39
- const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}${propagationPart}`
39
+ const hashableEdgeTags = edgeTags.includes('manual_checkpoint:true')
40
+ ? edgeTags.filter(item => item !== 'manual_checkpoint:true')
41
+ : edgeTags
42
+
43
+ // The cache key includes parentHash so a fan-in node with different parents
44
+ // gets distinct cache entries; the hash input below excludes parentHash and
45
+ // gets combined with it via a second sha pass to produce the final hash.
46
+ const joinedEdgeTags = hashableEdgeTags.join('')
47
+ const propagationHex = propagationHashBigInt ? propagationHashBigInt.toString(16) : ''
48
+ const propagationPart = propagationHex ? `:${propagationHex}` : ''
49
+ const key = `${service}${env}${joinedEdgeTags}${parentHash}${propagationPart}`
40
50
 
41
51
  let value = cache.get(key)
42
52
  if (value) {
43
53
  return value
44
54
  }
45
55
 
46
- // Key vs hashInput distinction:
47
- // - 'key' (above) is used for caching and includes parentHash to differentiate pathways
48
- // with the same node but different parents (e.g., multiple queues feeding one consumer)
49
- // - 'hashInput' (below) excludes parentHash to compute only the current node's identity hash,
50
- // which is then XORed with parentHash (line 54) to build the complete pathway hash
51
- // This two-step approach (hash current node independently, then combine with parent) is
52
- // required for proper pathway construction in the DSM protocol.
53
- const baseString = `${service}${env}` + hashableEdgeTags.join('')
54
- const hashInput = propagationHashBigInt
55
- ? `${baseString}:${propagationHashBigInt.toString(16)}`
56
- : baseString
56
+ const baseString = `${service}${env}${joinedEdgeTags}`
57
+ const hashInput = propagationHex ? `${baseString}:${propagationHex}` : baseString
57
58
 
58
59
  const currentHash = shaHash(hashInput)
59
60
  const buf = Buffer.concat([currentHash, parentHash], 16)
@@ -70,11 +71,12 @@ function computeHash (service, env, edgeTags, parentHash, propagationHashBigInt
70
71
  * @returns {Buffer}
71
72
  */
72
73
  function encodePathwayContext (dataStreamsContext) {
73
- return Buffer.concat([
74
- dataStreamsContext.hash,
75
- Buffer.from(encodeVarint(Math.round(dataStreamsContext.pathwayStartNs / 1e6))),
76
- Buffer.from(encodeVarint(Math.round(dataStreamsContext.edgeStartNs / 1e6))),
77
- ], 20)
74
+ let offset = dataStreamsContext.hash.copy(pathwayScratch, 0)
75
+ offset = encodeVarintInto(pathwayScratch, offset, Math.round(dataStreamsContext.pathwayStartNs / 1e6))
76
+ offset = encodeVarintInto(pathwayScratch, offset, Math.round(dataStreamsContext.edgeStartNs / 1e6))
77
+ // No-op when offset >= PATHWAY_CONTEXT_BYTES; otherwise pads stale bytes from a previous call.
78
+ pathwayScratch.fill(0, offset, PATHWAY_CONTEXT_BYTES)
79
+ return Buffer.from(pathwayScratch.subarray(0, PATHWAY_CONTEXT_BYTES))
78
80
  }
79
81
 
80
82
  /**
@@ -178,6 +180,7 @@ const DsmPathwayCodec = {
178
180
  }
179
181
 
180
182
  module.exports = {
183
+ CONTEXT_PROPAGATION_KEY_BASE64,
181
184
  computePathwayHash: computeHash,
182
185
  encodePathwayContext,
183
186
  decodePathwayContext,
@@ -8,15 +8,21 @@ const { PATHWAY_HASH, DSM_TRANSACTION_ID, DSM_TRANSACTION_CHECKPOINT } = require
8
8
  const log = require('../log')
9
9
  const processTags = require('../process-tags')
10
10
  const propagationHash = require('../propagation-hash')
11
- const { DsmPathwayCodec } = require('./pathway')
11
+ const { CONTEXT_PROPAGATION_KEY_BASE64, computePathwayHash } = require('./pathway')
12
12
  const { DataStreamsWriter } = require('./writer')
13
- const { computePathwayHash } = require('./pathway')
14
13
  const { getAmqpMessageSize, getHeadersSize, getMessageSize, getSizeOrZero } = require('./size')
15
14
  const { SchemaBuilder } = require('./schemas/schema_builder')
16
15
  const { SchemaSampler } = require('./schemas/schema_sampler')
17
16
 
18
17
  const ENTRY_PARENT_HASH = Buffer.from('0000000000000000', 'hex')
19
18
 
19
+ // A direction:out checkpoint estimates the size cost of the header the
20
+ // producer plugin will inject. The pathway context is always 20 binary
21
+ // bytes, encoded as 28 base64 chars; together with the header key and
22
+ // JSON framing (matching the prior `JSON.stringify({key: value})` byte
23
+ // count minus 1), this is a fixed value.
24
+ const PATHWAY_HEADER_BYTES = CONTEXT_PROPAGATION_KEY_BASE64.length + 28 + 6
25
+
20
26
  class StatsPoint {
21
27
  constructor (hash, parentHash, edgeTags) {
22
28
  this.hash = hash.readBigUInt64LE()
@@ -271,19 +277,19 @@ class DataStreamsProcessor {
271
277
 
272
278
  recordCheckpoint (checkpoint, span = null) {
273
279
  if (!this.enabled) return
274
- this.bucketFromTimestamp(checkpoint.currentTimestamp)
275
- .forCheckpoint(checkpoint)
276
- .addLatencies(checkpoint)
277
- // set DSM pathway hash on span to enable related traces feature on DSM tab, convert from buffer to uint64
280
+ const statsPoint = this.bucketFromTimestamp(checkpoint.currentTimestamp).forCheckpoint(checkpoint)
281
+ statsPoint.addLatencies(checkpoint)
278
282
  if (span) {
279
- span.setTag(PATHWAY_HASH, checkpoint.hash.readBigUInt64LE(0).toString())
283
+ // StatsPoint already converted the 8-byte Buffer hash to a uint64 BigInt.
284
+ span.setTag(PATHWAY_HASH, statsPoint.hash.toString())
280
285
  }
281
286
  }
282
287
 
283
- setCheckpoint (edgeTags, span, ctx = null, payloadSize = 0) {
284
- if (!this.enabled) return null
288
+ setCheckpoint (edgeTags, span, ctx, payloadSize = 0) {
289
+ if (!this.enabled) return
285
290
  const nowNs = Date.now() * 1e6
286
- const direction = edgeTags.find(t => t.startsWith('direction:'))
291
+ // Callers must place the direction tag at index 0.
292
+ const direction = edgeTags[0]
287
293
  let pathwayStartNs = nowNs
288
294
  let edgeStartNs = nowNs
289
295
  let parentHash = ENTRY_PARENT_HASH
@@ -334,11 +340,7 @@ class DataStreamsProcessor {
334
340
  closestOppositeDirectionEdgeStart,
335
341
  }
336
342
  if (direction === 'direction:out') {
337
- // Add the header for this now, as the callee doesn't have access to context when producing
338
- // - 1 to account for extra byte for {
339
- const ddInfoContinued = {}
340
- DsmPathwayCodec.encode(dataStreamsContext, ddInfoContinued)
341
- payloadSize += getSizeOrZero(JSON.stringify(ddInfoContinued)) - 1
343
+ payloadSize += PATHWAY_HEADER_BYTES
342
344
  }
343
345
  const checkpoint = {
344
346
  currentTimestamp: nowNs,
@@ -4,7 +4,7 @@ const { types } = require('util')
4
4
 
5
5
  function getSizeOrZero (obj) {
6
6
  if (typeof obj === 'string') {
7
- return Buffer.from(obj, 'utf8').length
7
+ return Buffer.byteLength(obj, 'utf8')
8
8
  }
9
9
  if (types.isArrayBuffer(obj)) {
10
10
  return obj.byteLength
@@ -32,7 +32,11 @@ function getSizeOrZero (obj) {
32
32
 
33
33
  function getHeadersSize (headers) {
34
34
  if (headers === undefined) return 0
35
- return Object.entries(headers).reduce((prev, [key, val]) => getSizeOrZero(key) + getSizeOrZero(val) + prev, 0)
35
+ let size = 0
36
+ for (const key of Object.keys(headers)) {
37
+ size += Buffer.byteLength(key, 'utf8') + getSizeOrZero(headers[key])
38
+ }
39
+ return size
36
40
  }
37
41
 
38
42
  function getMessageSize (message) {
@@ -1,16 +1,19 @@
1
1
  'use strict'
2
2
 
3
+ const getGitMetadata = require('../git_metadata')
4
+
3
5
  module.exports = function getDebuggerConfig (config, inputPath) {
6
+ const { commitSHA, repositoryUrl } = getGitMetadata(config)
4
7
  return {
5
- commitSHA: config.commitSHA,
8
+ commitSHA,
6
9
  debug: config.debug,
7
10
  dynamicInstrumentation: config.dynamicInstrumentation,
8
11
  env: config.env,
9
12
  hostname: config.hostname,
10
13
  logLevel: config.logLevel,
11
14
  port: config.port,
12
- propagateProcessTags: config.propagateProcessTags,
13
- repositoryUrl: config.repositoryUrl,
15
+ propagateProcessTags: { enabled: config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED },
16
+ repositoryUrl,
14
17
  runtimeId: config.tags['runtime-id'],
15
18
  service: config.service,
16
19
  url: config.url?.toString(),
@@ -247,10 +247,6 @@ session.on('Debugger.paused', async ({ params }) => {
247
247
  language: 'javascript',
248
248
  }
249
249
 
250
- if (config.propagateProcessTags.enabled) {
251
- snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject
252
- }
253
-
254
250
  if (probe.captureSnapshot) {
255
251
  if (fatalSnapshotErrors && fatalSnapshotErrors.length > 0) {
256
252
  // There was an error collecting the snapshot for this probe, let's not try again
@@ -327,7 +323,8 @@ session.on('Debugger.paused', async ({ params }) => {
327
323
 
328
324
  ackEmitting(probe)
329
325
 
330
- send(message, logger, dd, snapshot)
326
+ send(message, logger, dd, snapshot,
327
+ config.propagateProcessTags.enabled ? processTags.serialized : undefined)
331
328
  }
332
329
  })
333
330
 
@@ -40,7 +40,7 @@ const jsonBuffer = new JSONBuffer({
40
40
  onFlush,
41
41
  })
42
42
 
43
- function send (message, logger, dd, snapshot) {
43
+ function send (message, logger, dd, snapshot, processTags) {
44
44
  const payload = {
45
45
  ddsource,
46
46
  hostname,
@@ -50,6 +50,7 @@ function send (message, logger, dd, snapshot) {
50
50
  : message,
51
51
  logger,
52
52
  dd,
53
+ process_tags: processTags,
53
54
  debugger: { snapshot },
54
55
  }
55
56
 
@@ -22,6 +22,7 @@ const TYPE_HISTOGRAM = 'h'
22
22
  */
23
23
  class DogStatsDClient {
24
24
  #lookup
25
+ #tagsPrefix
25
26
  constructor (options) {
26
27
  this.#lookup = options.lookup
27
28
  if (options.metricsProxyUrl) {
@@ -36,6 +37,7 @@ class DogStatsDClient {
36
37
  this._family = isIP(this._host)
37
38
  this._port = options.port
38
39
  this._tags = options.tags
40
+ this.#tagsPrefix = this._tags?.length ? `|#${this._tags.join(',')}` : ''
39
41
  this._queue = []
40
42
  this._buffer = ''
41
43
  this._offset = 0
@@ -66,9 +68,9 @@ class DogStatsDClient {
66
68
  flush () {
67
69
  const queue = this._enqueue()
68
70
 
69
- log.debug('Flushing %s metrics via', queue.length, this._httpOptions ? 'HTTP' : 'UDP')
71
+ if (queue.length === 0) return
70
72
 
71
- if (this._queue.length === 0) return
73
+ log.debug('Flushing %s metrics via %s', queue.length, this._httpOptions ? 'HTTP' : 'UDP')
72
74
 
73
75
  this._queue = []
74
76
 
@@ -119,11 +121,12 @@ class DogStatsDClient {
119
121
  _add (stat, value, type, tags) {
120
122
  let message = `${stat}:${value}|${type}`
121
123
 
122
- // Don't manipulate this._tags as it is still used
123
- tags = tags ? [...this._tags, ...tags] : this._tags
124
-
125
- if (tags.length > 0) {
126
- message += `|#${tags.join(',')}`
124
+ if (tags?.length) {
125
+ message += this.#tagsPrefix
126
+ ? `${this.#tagsPrefix},${tags.join(',')}`
127
+ : `|#${tags.join(',')}`
128
+ } else {
129
+ message += this.#tagsPrefix
127
130
  }
128
131
 
129
132
  if (entityId) {
@@ -3,15 +3,15 @@
3
3
  const getConfig = require('../config')
4
4
  const { MsgpackChunk, MsgpackEncoder } = require('../msgpack')
5
5
  const log = require('../log')
6
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
6
+ const { normalizeSpan } = require('./tags-processors')
7
7
 
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
9
9
 
10
10
  function formatSpan (span, config) {
11
- span = normalizeSpan(truncateSpan(span, false))
11
+ span = normalizeSpan(span)
12
12
  if (span.span_events) {
13
13
  // ensure span events are encoded as tags if agent doesn't support native top level span events
14
- if (config.trace.nativeSpanEvents) {
14
+ if (config.DD_TRACE_NATIVE_SPAN_EVENTS) {
15
15
  formatSpanEvents(span)
16
16
  } else {
17
17
  span.meta.events = JSON.stringify(span.span_events)
@@ -1,13 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
+ const { normalizeSpan } = require('./tags-processors')
4
4
  const { AgentEncoder: BaseEncoder } = require('./0.4')
5
5
 
6
6
  const ARRAY_OF_TWO = 0x92
7
7
  const ARRAY_OF_TWELVE = 0x9C
8
8
 
9
9
  function formatSpan (span) {
10
- span = normalizeSpan(truncateSpan(span, false))
10
+ span = normalizeSpan(span)
11
11
  // ensure span events are encoded as tags
12
12
  if (span.span_events) {
13
13
  span.meta.events = JSON.stringify(span.span_events)
@@ -2,7 +2,7 @@
2
2
 
3
3
  const log = require('../log')
4
4
  const { TOP_LEVEL_KEY } = require('../constants')
5
- const { truncateSpan, normalizeSpan } = require('./tags-processors')
5
+ const { normalizeSpan } = require('./tags-processors')
6
6
 
7
7
  // Soft limit for estimated payload size. Triggers an early flush to stay under intake request size limits.
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
@@ -14,7 +14,7 @@ const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
14
14
  * @returns {object} The formatted span
15
15
  */
16
16
  function formatSpan (span, isFirstSpan) {
17
- span = normalizeSpan(truncateSpan(span, false))
17
+ span = normalizeSpan(span)
18
18
 
19
19
  // Remove _dd.p.tid (the upper 64 bits of a 128-bit trace ID) since trace_id is truncated to lower 64 bits
20
20
  delete span.meta['_dd.p.tid']
@@ -25,35 +25,10 @@ const MAX_SERVICE_LENGTH = 100
25
25
  // MAX_TYPE_LENGTH the maximum length a span type can have
26
26
  const MAX_TYPE_LENGTH = 100
27
27
 
28
- // TODO (bengl) Pretty much everything in this file should happen in
29
- // `format.js`, so that we're not iterating over all the spans and modifying
30
- // them yet again.
31
-
32
- // normally the agent truncates the resource and parses it in certain scenarios (e.g. SQL Queries)
33
- function truncateSpan (span, shouldTruncateResourceName = true) {
34
- if (shouldTruncateResourceName && span.resource && span.resource.length > MAX_RESOURCE_NAME_LENGTH) {
28
+ function truncateSpan (span) {
29
+ if (span.resource && span.resource.length > MAX_RESOURCE_NAME_LENGTH) {
35
30
  span.resource = `${span.resource.slice(0, MAX_RESOURCE_NAME_LENGTH)}...`
36
31
  }
37
- for (let metaKey in span.meta) {
38
- const val = span.meta[metaKey]
39
- if (metaKey.length > MAX_META_KEY_LENGTH) {
40
- delete span.meta[metaKey]
41
- metaKey = `${metaKey.slice(0, MAX_META_KEY_LENGTH)}...`
42
- span.metrics[metaKey] = val
43
- }
44
- if (val && val.length > MAX_META_VALUE_LENGTH) {
45
- span.meta[metaKey] = `${val.slice(0, MAX_META_VALUE_LENGTH)}...`
46
- }
47
- }
48
- for (let metricsKey in span.metrics) {
49
- const val = span.metrics[metricsKey]
50
- if (metricsKey.length > MAX_METRIC_KEY_LENGTH) {
51
- delete span.metrics[metricsKey]
52
- metricsKey = `${metricsKey.slice(0, MAX_METRIC_KEY_LENGTH)}...`
53
- span.metrics[metricsKey] = val
54
- }
55
- }
56
-
57
32
  return span
58
33
  }
59
34
 
@@ -13,6 +13,12 @@ const log = require('../../log')
13
13
  const { urlToHttpOptions } = require('./url-to-http-options-polyfill')
14
14
  const docker = require('./docker')
15
15
  const { httpAgent, httpsAgent } = require('./agents')
16
+ const {
17
+ getMaxAttempts,
18
+ getRetryDelay,
19
+ isRetriableNetworkError,
20
+ markEndpointReached,
21
+ } = require('./retry')
16
22
 
17
23
  const maxActiveBufferSize = 1024 * 1024 * 64
18
24
 
@@ -92,6 +98,8 @@ function request (data, options, callback) {
92
98
  options.agent = isSecure ? httpsAgent : httpAgent
93
99
 
94
100
  const onResponse = (res, finalize) => {
101
+ markEndpointReached(options)
102
+
95
103
  const chunks = []
96
104
 
97
105
  res.setTimeout(timeout)
@@ -142,7 +150,10 @@ function request (data, options, callback) {
142
150
  })
143
151
  }
144
152
 
145
- const makeRequest = onError => {
153
+ // Retries always run via setTimeout so the AsyncLocalStorage store survives
154
+ // the gap before socket.connect(); ALS.run() does not call ALS.enterWith()
155
+ // outside AsyncContextFrame, so a synchronous re-entry would lose the store.
156
+ const attempt = attemptIndex => {
146
157
  if (!request.writable) {
147
158
  log.debug('Maximum number of active requests reached: payload is discarded.')
148
159
  return callback(null)
@@ -163,9 +174,16 @@ function request (data, options, callback) {
163
174
  req.once('close', finalize)
164
175
  req.once('timeout', finalize)
165
176
 
166
- req.once('error', err => {
177
+ req.once('error', error => {
167
178
  finalize()
168
- onError(err)
179
+ if (attemptIndex < getMaxAttempts(options) && isRetriableNetworkError(error)) {
180
+ // Unref so a pending retry never keeps the host process alive past
181
+ // its natural exit point; long-running apps still retry because the
182
+ // event loop is held open by their own work.
183
+ setTimeout(attempt, getRetryDelay(options, attemptIndex), attemptIndex + 1).unref()
184
+ } else {
185
+ callback(error)
186
+ }
169
187
  })
170
188
 
171
189
  req.setTimeout(timeout, () => {
@@ -185,14 +203,7 @@ function request (data, options, callback) {
185
203
  })
186
204
  }
187
205
 
188
- // The setTimeout is needed to avoid losing the async context in the retry
189
- // request before socket.connect() is called. This is a workaround for the
190
- // issue that the AsyncLocalStorage.run() method does not call the
191
- // AsyncLocalStorage.enterWith() method when not using AsyncContextFrame.
192
- //
193
- // TODO: Test that this doesn't trace itself on retry when the diagnostics
194
- // channel events are available in the agent exporter.
195
- makeRequest(() => setTimeout(() => makeRequest(callback)))
206
+ attempt(1)
196
207
  }
197
208
 
198
209
  function byteLength (data) {
@@ -0,0 +1,104 @@
1
+ 'use strict'
2
+
3
+ const RATE_LIMIT_MAX_WAIT_MS = 30_000
4
+
5
+ const SINGLE_RETRY_BASE_MS = 5000
6
+ const SINGLE_RETRY_JITTER_MS = 2500
7
+
8
+ const STARTUP_GRACE_MS = 30_000
9
+ const STARTUP_BACKOFF_BASE_MS = 1000
10
+ const STARTUP_BACKOFF_MAX_MS = 8000
11
+ const STARTUP_BACKOFF_JITTER_MS = 500
12
+ const STARTUP_MAX_ATTEMPTS = 5
13
+ const POST_STARTUP_MAX_ATTEMPTS = 2
14
+
15
+ // `ECONNREFUSED` and `ENOENT` cover the agent-not-yet-listening cases (TCP and
16
+ // UDS). `EAI_AGAIN` covers transient DNS in agentless intake. `ENOTFOUND` is
17
+ // excluded because it usually means a misconfigured host, not a transient state.
18
+ const RETRIABLE_NETWORK_CODES = new Set([
19
+ 'EAI_AGAIN',
20
+ 'ECONNREFUSED',
21
+ 'ECONNRESET',
22
+ 'ENOENT',
23
+ 'EPIPE',
24
+ 'ETIMEDOUT',
25
+ ])
26
+
27
+ const startedAtMs = Date.now()
28
+ const reachedEndpoints = new Set()
29
+
30
+ /**
31
+ * @typedef {object} EndpointOptions
32
+ * @property {string} [socketPath]
33
+ * @property {string} [hostname]
34
+ * @property {string} [host]
35
+ * @property {string|number} [port]
36
+ */
37
+
38
+ /**
39
+ * @param {Error & { code?: string }} error
40
+ */
41
+ function isRetriableNetworkError (error) {
42
+ return error?.code !== undefined && RETRIABLE_NETWORK_CODES.has(error.code)
43
+ }
44
+
45
+ function singleJitteredDelay () {
46
+ return SINGLE_RETRY_BASE_MS + Math.random() * SINGLE_RETRY_JITTER_MS
47
+ }
48
+
49
+ /**
50
+ * Stable key identifying the destination so the startup-phase gate is scoped
51
+ * per endpoint. UDS path beats host:port because both can coexist on the same
52
+ * options object after `parseUrl` runs.
53
+ *
54
+ * @param {EndpointOptions} options
55
+ */
56
+ function getEndpointKey (options) {
57
+ if (options.socketPath) return options.socketPath
58
+ return `${options.hostname || options.host || ''}:${options.port || ''}`
59
+ }
60
+
61
+ /**
62
+ * @param {EndpointOptions} options
63
+ */
64
+ function inStartupPhase (options) {
65
+ if ((Date.now() - startedAtMs) >= STARTUP_GRACE_MS) return false
66
+ return !reachedEndpoints.has(getEndpointKey(options))
67
+ }
68
+
69
+ /**
70
+ * Wait time before the next attempt when the previous one just failed. Bounded
71
+ * exponential backoff with small jitter inside the startup grace window;
72
+ * single 5–7.5 s jittered retry afterwards.
73
+ *
74
+ * @param {EndpointOptions} options
75
+ * @param {number} previousAttempt 1-based index of the attempt that just failed.
76
+ */
77
+ function getRetryDelay (options, previousAttempt) {
78
+ if (!inStartupPhase(options)) return singleJitteredDelay()
79
+ const exp = Math.min(STARTUP_BACKOFF_MAX_MS, STARTUP_BACKOFF_BASE_MS << (previousAttempt - 1))
80
+ return exp + Math.random() * STARTUP_BACKOFF_JITTER_MS
81
+ }
82
+
83
+ /**
84
+ * @param {EndpointOptions} options
85
+ */
86
+ function getMaxAttempts (options) {
87
+ return inStartupPhase(options) ? STARTUP_MAX_ATTEMPTS : POST_STARTUP_MAX_ATTEMPTS
88
+ }
89
+
90
+ /**
91
+ * @param {EndpointOptions} options
92
+ */
93
+ function markEndpointReached (options) {
94
+ reachedEndpoints.add(getEndpointKey(options))
95
+ }
96
+
97
+ module.exports = {
98
+ RATE_LIMIT_MAX_WAIT_MS,
99
+ getMaxAttempts,
100
+ getRetryDelay,
101
+ isRetriableNetworkError,
102
+ markEndpointReached,
103
+ singleJitteredDelay,
104
+ }
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const log = require('./log')
7
+ const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('./plugins/util/tags')
8
+ const {
9
+ getGitMetadataFromGitProperties,
10
+ getRemoteOriginURL,
11
+ removeUserSensitiveInfo,
12
+ resolveGitHeadSHA,
13
+ } = require('./config/git_properties')
14
+
15
+ /** @type {{ commitSHA: string | undefined, repositoryUrl: string | undefined } | undefined} */
16
+ let cached
17
+
18
+ /**
19
+ * @param {import('./config/config-types').ConfigProperties} config
20
+ */
21
+ function getGitMetadata (config) {
22
+ if (cached) return cached
23
+
24
+ if (!config.DD_TRACE_GIT_METADATA_ENABLED) {
25
+ cached = { commitSHA: undefined, repositoryUrl: undefined }
26
+ return cached
27
+ }
28
+
29
+ let repositoryUrl = removeUserSensitiveInfo(config.DD_GIT_REPOSITORY_URL ?? config.tags[GIT_REPOSITORY_URL])
30
+ let commitSHA = config.DD_GIT_COMMIT_SHA ?? config.tags[GIT_COMMIT_SHA]
31
+
32
+ if (!repositoryUrl || !commitSHA) {
33
+ const propertiesFile = config.DD_GIT_PROPERTIES_FILE
34
+ const gitPropertiesFile = propertiesFile ?? `${process.cwd()}/git.properties`
35
+ try {
36
+ const fromProperties = getGitMetadataFromGitProperties(fs.readFileSync(gitPropertiesFile, 'utf8'))
37
+ commitSHA ??= fromProperties.commitSHA
38
+ repositoryUrl ??= fromProperties.repositoryUrl
39
+ } catch (error) {
40
+ if (propertiesFile) {
41
+ log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', gitPropertiesFile, error)
42
+ }
43
+ }
44
+ }
45
+
46
+ const folderPath = config.DD_GIT_FOLDER_PATH
47
+ const gitFolderPath = folderPath ?? path.join(process.cwd(), '.git')
48
+
49
+ if (!repositoryUrl) {
50
+ const gitConfigPath = path.join(gitFolderPath, 'config')
51
+ try {
52
+ repositoryUrl = getRemoteOriginURL(fs.readFileSync(gitConfigPath, 'utf8'))
53
+ } catch (error) {
54
+ if (folderPath) {
55
+ log.error('Error reading git config: %s', gitConfigPath, error)
56
+ }
57
+ }
58
+ }
59
+
60
+ commitSHA ??= resolveGitHeadSHA(gitFolderPath)
61
+
62
+ cached = { commitSHA, repositoryUrl }
63
+ return cached
64
+ }
65
+
66
+ module.exports = getGitMetadata
@@ -1,17 +1,25 @@
1
1
  'use strict'
2
2
 
3
3
  const { SCI_COMMIT_SHA, SCI_REPOSITORY_URL } = require('./constants')
4
+ const getGitMetadata = require('./git_metadata')
4
5
 
5
6
  class GitMetadataTagger {
7
+ #commitSHA
8
+ #repositoryUrl
9
+ #enabled
10
+
6
11
  constructor (config) {
7
- this._config = config
12
+ this.#enabled = config.DD_TRACE_GIT_METADATA_ENABLED
13
+ const { commitSHA, repositoryUrl } = getGitMetadata(config)
14
+ this.#commitSHA = commitSHA
15
+ this.#repositoryUrl = repositoryUrl
8
16
  }
9
17
 
10
18
  tagGitMetadata (spanContext) {
11
- if (this._config.DD_TRACE_GIT_METADATA_ENABLED) {
12
- // These tags are added only to the local root span
13
- spanContext._trace.tags[SCI_COMMIT_SHA] = this._config.commitSHA
14
- spanContext._trace.tags[SCI_REPOSITORY_URL] = this._config.repositoryUrl
19
+ if (this.#enabled) {
20
+ const tags = spanContext._trace.tags
21
+ tags[SCI_COMMIT_SHA] = this.#commitSHA
22
+ tags[SCI_REPOSITORY_URL] = this.#repositoryUrl
15
23
  }
16
24
  }
17
25
  }