dd-trace 6.1.0 → 6.3.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 (58) hide show
  1. package/README.md +1 -1
  2. package/index.d.ts +7 -0
  3. package/initialize.mjs +4 -2
  4. package/package.json +5 -5
  5. package/packages/datadog-instrumentations/src/cookie.js +7 -1
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -1
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +30 -54
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
  12. package/packages/datadog-instrumentations/src/http/server.js +27 -0
  13. package/packages/datadog-instrumentations/src/mercurius.js +11 -0
  14. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +248 -1
  15. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
  16. package/packages/datadog-instrumentations/src/playwright.js +40 -3
  17. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  18. package/packages/datadog-instrumentations/src/vitest-util.js +0 -3
  19. package/packages/datadog-instrumentations/src/vitest-worker.js +0 -1
  20. package/packages/datadog-plugin-azure-functions/src/index.js +2 -1
  21. package/packages/datadog-plugin-graphql/src/execute.js +4 -15
  22. package/packages/datadog-plugin-graphql/src/index.js +6 -0
  23. package/packages/datadog-plugin-graphql/src/request.js +104 -0
  24. package/packages/datadog-plugin-graphql/src/utils.js +177 -0
  25. package/packages/datadog-plugin-graphql/src/validate.js +24 -1
  26. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +144 -26
  27. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/utils.js +30 -0
  28. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  29. package/packages/datadog-plugin-vitest/src/index.js +0 -8
  30. package/packages/datadog-shimmer/src/shimmer.js +9 -2
  31. package/packages/dd-trace/src/appsec/channels.js +1 -0
  32. package/packages/dd-trace/src/appsec/index.js +6 -3
  33. package/packages/dd-trace/src/appsec/lambda.js +8 -8
  34. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  35. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +23 -16
  36. package/packages/dd-trace/src/config/config-types.d.ts +8 -0
  37. package/packages/dd-trace/src/config/defaults.js +20 -15
  38. package/packages/dd-trace/src/config/index.js +13 -3
  39. package/packages/dd-trace/src/config/parsers.js +79 -0
  40. package/packages/dd-trace/src/datastreams/writer.js +14 -2
  41. package/packages/dd-trace/src/encode/0.4.js +23 -2
  42. package/packages/dd-trace/src/encode/0.5.js +12 -1
  43. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
  44. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
  45. package/packages/dd-trace/src/exporters/common/writer.js +17 -1
  46. package/packages/dd-trace/src/guardrails/index.js +3 -1
  47. package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
  48. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  49. package/packages/dd-trace/src/llmobs/tagger.js +15 -1
  50. package/packages/dd-trace/src/msgpack/chunk.js +33 -1
  51. package/packages/dd-trace/src/msgpack/index.js +6 -1
  52. package/packages/dd-trace/src/opentelemetry/span_processor.js +7 -5
  53. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +32 -16
  54. package/packages/dd-trace/src/plugins/index.js +3 -0
  55. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -11
  56. package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
  57. package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
  58. package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
@@ -297,8 +297,87 @@ const parsers = {
297
297
  },
298
298
  }
299
299
 
300
+ const programmaticTypeCoercions = {
301
+ /**
302
+ * @param {unknown} value
303
+ */
304
+ BOOLEAN (value) {
305
+ if (typeof value === 'boolean') {
306
+ return value
307
+ }
308
+ if (typeof value === 'string') {
309
+ return parsers.BOOLEAN(value)
310
+ }
311
+ },
312
+ /**
313
+ * @param {unknown} value
314
+ */
315
+ INT (value) {
316
+ if (typeof value === 'number' || typeof value === 'string' && value.trim() !== '') {
317
+ return parsers.INT(value)
318
+ }
319
+ },
320
+ /**
321
+ * @param {unknown} value
322
+ */
323
+ DECIMAL (value) {
324
+ if (typeof value === 'number' || typeof value === 'string' && value.trim() !== '') {
325
+ return parsers.DECIMAL(value)
326
+ }
327
+ },
328
+ /**
329
+ * @param {unknown} value
330
+ */
331
+ STRING (value) {
332
+ if (typeof value === 'string') {
333
+ return value
334
+ }
335
+ if (typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value)) {
336
+ return String(value)
337
+ }
338
+ },
339
+ /**
340
+ * @param {unknown} value
341
+ */
342
+ FUNCTION (value) {
343
+ if (typeof value === 'function') {
344
+ return value
345
+ }
346
+ },
347
+ /**
348
+ * @param {unknown} value
349
+ */
350
+ ARRAY (value) {
351
+ if (Array.isArray(value)) {
352
+ for (const item of value) {
353
+ if (typeof item !== 'string' && typeof item !== 'number' && typeof item !== 'boolean') {
354
+ return
355
+ }
356
+ }
357
+ return value
358
+ }
359
+ },
360
+ /**
361
+ * @param {unknown} value
362
+ */
363
+ MAP (value) {
364
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
365
+ return value
366
+ }
367
+ },
368
+ /**
369
+ * @param {unknown} value
370
+ */
371
+ JSON (value) {
372
+ if (typeof value === 'object' && value !== null) {
373
+ return value
374
+ }
375
+ },
376
+ }
377
+
300
378
  module.exports = {
301
379
  parsers,
380
+ programmaticTypeCoercions,
302
381
  transformers,
303
382
  telemetryTransformers,
304
383
  setWarnInvalidValue,
@@ -4,7 +4,7 @@ const zlib = require('zlib')
4
4
  const pkg = require('../../../../package.json')
5
5
  const log = require('../log')
6
6
  const request = require('../exporters/common/request')
7
- const { encode: encodeMsgpack } = require('../msgpack')
7
+ const { encode: encodeMsgpack, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack')
8
8
 
9
9
  function makeRequest (data, url, cb) {
10
10
  const options = {
@@ -36,7 +36,19 @@ class DataStreamsWriter {
36
36
  log.debug('Maximum number of active requests reached. Payload discarded: %j', payload)
37
37
  return
38
38
  }
39
- const encodedPayload = encodeMsgpack(payload)
39
+
40
+ let encodedPayload
41
+ try {
42
+ encodedPayload = encodeMsgpack(payload)
43
+ } catch (error) {
44
+ if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
45
+ // The msgpack-encoded pipeline-stats payload exceeded the agent
46
+ // intake cap. Dropping it locally is safer than letting the
47
+ // RangeError crash the host process; the agent would reject the
48
+ // oversized payload at the network boundary anyway.
49
+ log.error('DataStreamsWriter dropped a payload that exceeded the %d byte chunk cap', MAX_CHUNK_SIZE)
50
+ return
51
+ }
40
52
 
41
53
  zlib.gzip(encodedPayload, { level: 1 }, (err, compressedData) => {
42
54
  if (err) {
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const getConfig = require('../config')
4
- const { MsgpackChunk } = require('../msgpack')
4
+ const { MsgpackChunk, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack')
5
5
  const log = require('../log')
6
6
  const { normalizeSpan, eventTimeNano } = require('./tags-processors')
7
7
 
@@ -265,7 +265,28 @@ class AgentEncoder {
265
265
 
266
266
  this._traceCount++
267
267
 
268
- this._encode(bytes, trace)
268
+ try {
269
+ this._encode(bytes, trace)
270
+ } catch (error) {
271
+ if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
272
+ // The trace, or the queued payload it joined, hit the chunk cap.
273
+ // Rolling back just the in-flight trace is unsafe: the string cache
274
+ // may already hold subarrays / indices pointing at bytes we'd
275
+ // discard, and the next encode would emit stale bytes against a
276
+ // smaller string table. Drop the whole queued payload so the next
277
+ // encode starts from a clean state. Emitting a partial buffer would
278
+ // corrupt the msgpack wire. The virtual `reset()` is called (not
279
+ // `_reset()`) so subclasses can clear their own per-payload state
280
+ // (e.g. `AgentlessCiVisibilityEncoder._eventCount`).
281
+ const dropped = this._traceCount
282
+ this.reset()
283
+ log.error(
284
+ 'Trace encoder reset after exceeding the %d byte chunk cap; dropped %d trace(s)',
285
+ MAX_CHUNK_SIZE,
286
+ dropped
287
+ )
288
+ return
289
+ }
269
290
 
270
291
  if (this.#debugEncoding) {
271
292
  const end = bytes.length
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { MAX_SIZE, OverflowError } = require('../msgpack')
3
4
  const { normalizeSpan } = require('./tags-processors')
4
5
  const { AgentEncoder: BaseEncoder, stringifySpanEvents } = require('./0.4')
5
6
 
@@ -30,7 +31,17 @@ class AgentEncoder extends BaseEncoder {
30
31
  const prefixSize = 1
31
32
  const stringSize = this._stringBytes.length + 5
32
33
  const traceSize = this._traceBytes.length + 5
33
- const buffer = Buffer.allocUnsafe(prefixSize + stringSize + traceSize)
34
+ const payloadSize = prefixSize + stringSize + traceSize
35
+
36
+ // The string table and the trace bytes are capped independently, so both
37
+ // can sit just under the cap while their concatenation crosses it. `encode`
38
+ // never sees this overflow — it only exists once the two are summed here —
39
+ // so the writer's flush-time catch drops the payload.
40
+ if (payloadSize > MAX_SIZE) {
41
+ throw new OverflowError(payloadSize)
42
+ }
43
+
44
+ const buffer = Buffer.allocUnsafe(payloadSize)
34
45
 
35
46
  buffer[0] = ARRAY_OF_TWO
36
47
 
@@ -7,7 +7,7 @@ const {
7
7
  TELEMETRY_ENDPOINT_PAYLOAD_SERIALIZATION_MS,
8
8
  TELEMETRY_ENDPOINT_PAYLOAD_EVENTS_COUNT,
9
9
  } = require('../ci-visibility/telemetry')
10
- const { MsgpackChunk } = require('../msgpack')
10
+ const { MsgpackChunk, MAX_SIZE, OverflowError } = require('../msgpack')
11
11
  const { AgentEncoder } = require('./0.4')
12
12
  const {
13
13
  truncateSpanTestOpt,
@@ -371,6 +371,16 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
371
371
 
372
372
  const eventsBytes = this._traceBytes
373
373
  const totalSize = prefixBytes.length + eventsBytes.length
374
+
375
+ // The metadata prefix (built here, not during `encode`) and the events are
376
+ // capped independently, so both can stay under the cap while the assembled
377
+ // payload crosses it. An oversized metadata tag also overflows the prefix
378
+ // chunk itself inside `_encodePayloadStart` above; either way the tagged
379
+ // error propagates to the writer's flush-time catch to drop the payload.
380
+ if (totalSize > MAX_SIZE) {
381
+ throw new OverflowError(totalSize)
382
+ }
383
+
374
384
  const buffer = Buffer.allocUnsafe(totalSize)
375
385
  prefixBytes.buffer.copy(buffer, 0, 0, prefixBytes.length)
376
386
  eventsBytes.buffer.copy(buffer, prefixBytes.length, 0, eventsBytes.length)
@@ -1,11 +1,12 @@
1
1
  'use strict'
2
- const { MsgpackChunk } = require('../msgpack')
2
+ const { MsgpackChunk, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack')
3
3
 
4
4
  const {
5
5
  distributionMetric,
6
6
  TELEMETRY_ENDPOINT_PAYLOAD_SERIALIZATION_MS,
7
7
  TELEMETRY_ENDPOINT_PAYLOAD_EVENTS_COUNT,
8
8
  } = require('../ci-visibility/telemetry')
9
+ const log = require('../log')
9
10
  const FormData = require('../exporters/common/form-data')
10
11
  const { AgentEncoder } = require('./0.4')
11
12
 
@@ -40,7 +41,24 @@ class CoverageCIVisibilityEncoder extends AgentEncoder {
40
41
  const startTime = Date.now()
41
42
 
42
43
  this._coveragesCount++
43
- this.encodeCodeCoverage(this._coverageBytes, coverage)
44
+ try {
45
+ this.encodeCodeCoverage(this._coverageBytes, coverage)
46
+ } catch (error) {
47
+ if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
48
+ // The coverage payload hit the chunk cap. `_coverageBytes` may
49
+ // hold a partial entry and a stale `coverages` array prefix. Drop
50
+ // the whole queued payload so the next encode starts from a clean
51
+ // state; the parent `AgentEncoder` does the equivalent for its own
52
+ // bytes / string cache via the inherited `_reset()`.
53
+ const dropped = this._coveragesCount
54
+ this.reset()
55
+ log.error(
56
+ 'Coverage encoder reset after exceeding the %d byte chunk cap; dropped %d coverage(s)',
57
+ MAX_CHUNK_SIZE,
58
+ dropped
59
+ )
60
+ return
61
+ }
44
62
 
45
63
  distributionMetric(
46
64
  TELEMETRY_ENDPOINT_PAYLOAD_SERIALIZATION_MS,
@@ -3,6 +3,7 @@
3
3
  const { channel } = require('dc-polyfill')
4
4
 
5
5
  const log = require('../../log')
6
+ const { MAX_SIZE: MAX_CHUNK_SIZE } = require('../../msgpack')
6
7
  const request = require('./request')
7
8
  const { safeJSONStringify } = require('./util')
8
9
 
@@ -27,7 +28,22 @@ class Writer {
27
28
  this.#isFirstFlush = false
28
29
  this._beforeFirstFlush()
29
30
  }
30
- const payload = this._encoder.makePayload()
31
+ let payload
32
+ try {
33
+ payload = this._encoder.makePayload()
34
+ } catch (error) {
35
+ if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
36
+ // Multi-chunk encoders (v0.5, CI Visibility) only learn the assembled
37
+ // payload exceeds the cap when `makePayload` stitches the chunks
38
+ // together, after `encode` already returned — so the encode-time catch
39
+ // never sees it. Drop the queued payload here instead of letting the
40
+ // RangeError escape into the host application; the agent would reject
41
+ // the oversized payload at the network boundary anyway.
42
+ this._encoder.reset()
43
+ log.error('Writer dropped %d trace(s) that exceeded the %d byte chunk cap', count, MAX_CHUNK_SIZE)
44
+ done()
45
+ return
46
+ }
31
47
  this._sendPayload(payload, count, done)
32
48
  } else {
33
49
  done()
@@ -48,6 +48,8 @@ function guard (fn) {
48
48
  if (!clobberBailout && (NODE_MAJOR < minMajor || NODE_MAJOR >= nextMajor)) {
49
49
  initBailout = true
50
50
  var runtimeInfo = 'Incompatible runtime Node.js ' + version + ', supported runtimes: Node.js ' + supportedRange
51
+ // When not forced, the process bails out here and may call process.exit() right away;
52
+ // forward synchronously so the telemetry child can't outlive us and wedge the exit.
51
53
  telemetry([
52
54
  { name: 'abort', tags: ['reason:incompatible_runtime'] },
53
55
  { name: 'abort.runtime', tags: [] }
@@ -55,7 +57,7 @@ function guard (fn) {
55
57
  result: 'abort',
56
58
  result_class: 'incompatible_runtime',
57
59
  result_reason: runtimeInfo
58
- })
60
+ }, !forced)
59
61
  log.info('Aborting application instrumentation due to incompatible_runtime.')
60
62
  log.info('Found incompatible runtime Node.js %s, Supported runtimes: Node.js %s.', version, supportedRange)
61
63
  if (forced) {
@@ -1,7 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  var fs = require('fs')
4
- var spawn = require('child_process').spawn
4
+ // Capture the child_process functions at load time, before the tracer wraps the module.
5
+ // Reaching through require('child_process') at send time would route the forwarder through
6
+ // the tracer's own child_process instrumentation once the tracer is initialized.
7
+ var childProcess = require('child_process')
8
+ var spawn = childProcess.spawn
9
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
10
+ var spawnSync = childProcess.spawnSync
5
11
  var tracerVersion = require('../../../../package.json').version
6
12
  var log = require('./log')
7
13
 
@@ -47,7 +53,7 @@ function shouldSend (point) {
47
53
  return true
48
54
  }
49
55
 
50
- function sendTelemetry (name, tags, resultMetadata) {
56
+ function sendTelemetry (name, tags, resultMetadata, synchronous) {
51
57
  var points = name
52
58
  if (typeof name === 'string') {
53
59
  points = [{ name: name, tags: tags || [] }]
@@ -74,6 +80,37 @@ function sendTelemetry (name, tags, resultMetadata) {
74
80
  }
75
81
  }
76
82
 
83
+ var payload = JSON.stringify({ metadata: currentMetadata, points: points })
84
+
85
+ // A forwarder spawned asynchronously can still be tearing down its stdio pipes when the
86
+ // injected app calls process.exit(); on Node 24.0.0/24.1.x that deadlocks the exit
87
+ // (fixed upstream in 24.2), hanging short-lived single-step-install processes. On the
88
+ // bailout path the caller passes synchronous=true, so the child is fully reaped before we
89
+ // return and nothing survives to race the exit. spawnSync is only reached on that path,
90
+ // before any instrumentation is active, so it never traces the forwarder. It exists since
91
+ // Node 0.11.12; the guardrails still target >=0.8, which predates the exit bug anyway.
92
+ if (synchronous && spawnSync) {
93
+ // Bound the blocking send: this telemetry is best-effort and the whole point of the
94
+ // synchronous path is to avoid a hung exit, so a forwarder that wedges must not become a
95
+ // new hard hang. On timeout spawnSync kills the child and returns error.code ETIMEDOUT.
96
+ var result = spawnSync(telemetryForwarderPath, ['library_entrypoint'], {
97
+ input: payload,
98
+ stdio: ['pipe', 'ignore', 'ignore'],
99
+ timeout: 1000,
100
+ killSignal: 'SIGKILL'
101
+ })
102
+ if (result.error) {
103
+ if (result.error.code === 'ETIMEDOUT') {
104
+ log.error('Telemetry forwarder timed out')
105
+ } else {
106
+ log.error('Failed to spawn telemetry forwarder')
107
+ }
108
+ } else if (result.status) {
109
+ log.error('Telemetry forwarder exited with code', result.status)
110
+ }
111
+ return
112
+ }
113
+
77
114
  var proc = spawn(telemetryForwarderPath, ['library_entrypoint'], {
78
115
  stdio: 'pipe'
79
116
  })
@@ -88,5 +125,5 @@ function sendTelemetry (name, tags, resultMetadata) {
88
125
  proc.stdin.on('error', function () {
89
126
  log.error('Failed to write telemetry data to telemetry forwarder')
90
127
  })
91
- proc.stdin.end(JSON.stringify({ metadata: currentMetadata, points: points }))
128
+ proc.stdin.end(payload)
92
129
  }
@@ -13,6 +13,7 @@ module.exports = {
13
13
  ML_APP: '_ml_obs.meta.ml_app',
14
14
  PROPAGATED_PARENT_ID_KEY: '_dd.p.llmobs_parent_id',
15
15
  PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
16
+ PROPAGATED_SESSION_ID_KEY: '_dd.p.llmobs_sid',
16
17
  PARENT_ID_KEY: '_ml_obs.llmobs_parent_id',
17
18
  PROPAGATED_SAMPLE_RATE_KEY: '_dd.p.llmobs_sr',
18
19
  PROPAGATED_SAMPLING_DECISION_KEY: '_dd.p.llmobs_sd',
@@ -35,6 +35,7 @@ const {
35
35
  INTEGRATION,
36
36
  DECORATOR,
37
37
  PROPAGATED_ML_APP_KEY,
38
+ PROPAGATED_SESSION_ID_KEY,
38
39
  DEFAULT_PROMPT_NAME,
39
40
  INTERNAL_CONTEXT_VARIABLE_KEYS,
40
41
  INTERNAL_QUERY_VARIABLE_KEYS,
@@ -139,7 +140,8 @@ class LLMObsTagger {
139
140
  if (modelName) this.tagModelName(span, modelName)
140
141
  if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
141
142
 
142
- sessionId = sessionId || registry.get(parent)?.[SESSION_ID]
143
+ const traceTags = span.context()._trace.tags
144
+ sessionId = sessionId || registry.get(parent)?.[SESSION_ID] || traceTags[PROPAGATED_SESSION_ID_KEY]
143
145
  if (sessionId) this._setTag(span, SESSION_ID, sessionId)
144
146
  if (integration) this._setTag(span, INTEGRATION, integration)
145
147
  if (_decorator) this._setTag(span, DECORATOR, _decorator)
@@ -752,6 +754,18 @@ class LLMObsTagger {
752
754
 
753
755
  const tagsCarrier = registry.get(span)
754
756
  tagsCarrier[key] = value
757
+
758
+ // The first session set in a trace becomes the trace-level default, stored on the trace-shared
759
+ // propagating tags so later spans (incl. those under a session-less parent) inherit it and it
760
+ // rides `x-datadog-tags` across service boundaries. Established here, the single choke point for
761
+ // session writes, so sessions post-populated by integrations after span start also seed it.
762
+ // First-writer wins, so an explicit session still overrides locally.
763
+ if (key === SESSION_ID && value) {
764
+ const traceTags = span.context()._trace.tags
765
+ if (traceTags[PROPAGATED_SESSION_ID_KEY] === undefined) {
766
+ traceTags[PROPAGATED_SESSION_ID_KEY] = value
767
+ }
768
+ }
755
769
  }
756
770
  }
757
771
 
@@ -10,6 +10,33 @@ const SHRINK_AFTER_FLUSHES = 32
10
10
  // shape: after a halving step the post-shrink fill is the prior peak doubled,
11
11
  // still under 50 %.
12
12
  const SHRINK_USAGE_RATIO = 4
13
+ // Hard cap on chunk growth. The agent's trace intake rejects payloads over
14
+ // 50 MiB, so anything past that is dead on arrival anyway. A pathological
15
+ // single trace (unsanitized meta tag the size of a media file, multi-MB
16
+ // stack trace) used to grow the buffer without limit until either the
17
+ // allocation failed or the process tripped its memory ceiling. `reserve`
18
+ // now refuses the growth with a tagged `RangeError`; `AgentEncoder` catches
19
+ // it, resets the in-flight payload, and logs.
20
+ const MAX_SIZE = 50 * 1024 * 1024 // 50 MiB
21
+
22
+ /**
23
+ * Thrown when a chunk — or an assembled payload stitched from several chunks —
24
+ * would cross `MAX_SIZE`. Shared so the `reserve` cap and the per-encoder
25
+ * assembled-size guards (`0.5`, agentless CI Visibility) raise the same `code`,
26
+ * which every writer's `flush` recognises to drop the payload instead of
27
+ * crashing the host.
28
+ */
29
+ class OverflowError extends RangeError {
30
+ code = 'ERR_MSGPACK_CHUNK_OVERFLOW'
31
+
32
+ /**
33
+ * @param {number} needed Requested total byte size.
34
+ */
35
+ constructor (needed) {
36
+ super(`MsgpackChunk capped at ${MAX_SIZE} bytes; requested ${needed}`)
37
+ this.name = 'OverflowError'
38
+ }
39
+ }
13
40
 
14
41
  /**
15
42
  * Resizable msgpack write buffer. Owns the byte-layout primitives the encoder
@@ -100,11 +127,14 @@ class MsgpackChunk {
100
127
  const needed = this.length + size
101
128
 
102
129
  if (needed > this.buffer.length) {
130
+ if (needed > MAX_SIZE) {
131
+ throw new OverflowError(needed)
132
+ }
103
133
  let newSize = this.buffer.length
104
134
  // `*= 2` instead of `<<= 1`: `1073741824 << 1` is negative as int32,
105
135
  // and msgpack values can legitimately reach the multi-GiB range.
106
136
  while (newSize < needed) newSize *= 2
107
- this.#resize(newSize)
137
+ this.#resize(Math.min(newSize, MAX_SIZE))
108
138
  }
109
139
 
110
140
  this.length += size
@@ -454,3 +484,5 @@ class MsgpackChunk {
454
484
  }
455
485
 
456
486
  module.exports = MsgpackChunk
487
+ module.exports.MAX_SIZE = MAX_SIZE
488
+ module.exports.OverflowError = OverflowError
@@ -97,4 +97,9 @@ function writeMap (bytes, value) {
97
97
  }
98
98
  }
99
99
 
100
- module.exports = { MsgpackChunk, encode }
100
+ module.exports = {
101
+ MsgpackChunk,
102
+ encode,
103
+ MAX_SIZE: MsgpackChunk.MAX_SIZE,
104
+ OverflowError: MsgpackChunk.OverflowError,
105
+ }
@@ -14,32 +14,34 @@ class NoopSpanProcessor {
14
14
  }
15
15
 
16
16
  class MultiSpanProcessor extends NoopSpanProcessor {
17
+ #processors
18
+
17
19
  constructor (spanProcessors) {
18
20
  super()
19
- this._processors = spanProcessors
21
+ this.#processors = spanProcessors
20
22
  }
21
23
 
22
24
  forceFlush () {
23
25
  return Promise.all(
24
- this._processors.map(p => p.forceFlush())
26
+ this.#processors.map(p => p.forceFlush())
25
27
  )
26
28
  }
27
29
 
28
30
  onStart (span, context) {
29
- for (const processor of this._processors) {
31
+ for (const processor of this.#processors) {
30
32
  processor.onStart(span, context)
31
33
  }
32
34
  }
33
35
 
34
36
  onEnd (span) {
35
- for (const processor of this._processors) {
37
+ for (const processor of this.#processors) {
36
38
  processor.onEnd(span)
37
39
  }
38
40
  }
39
41
 
40
42
  shutdown () {
41
43
  return Promise.all(
42
- this._processors.map(p => p.shutdown())
44
+ this.#processors.map(p => p.shutdown())
43
45
  )
44
46
  }
45
47
  }
@@ -10,46 +10,62 @@ const { MultiSpanProcessor, NoopSpanProcessor } = require('./span_processor')
10
10
  const Tracer = require('./tracer')
11
11
 
12
12
  class TracerProvider {
13
+ #activeProcessor = new NoopSpanProcessor()
14
+ #contextManager = new ContextManager()
15
+ #processors = []
16
+ #tracers = new Map()
17
+
13
18
  constructor (config = {}) {
14
19
  this.config = config
15
20
  this.resource = config.resource
16
21
 
17
- this._processors = []
18
- this._tracers = new Map()
19
- this._activeProcessor = new NoopSpanProcessor()
20
- this._contextManager = new ContextManager()
22
+ // @opentelemetry/sdk-trace 2.x (used by @opentelemetry/sdk-node 0.220+)
23
+ // dropped `addSpanProcessor` and hands the processors to the provider
24
+ // constructor instead. Wire them the same way the 1.x `addSpanProcessor`
25
+ // path does, so a NodeSDK configured with a trace exporter or custom
26
+ // processors still delivers onStart/onEnd to them.
27
+ if (Array.isArray(config.spanProcessors)) {
28
+ for (const spanProcessor of config.spanProcessors) {
29
+ this.addSpanProcessor(spanProcessor)
30
+ }
31
+ }
21
32
  }
22
33
 
23
34
  getTracer (name = 'opentelemetry', version = '0.0.0', options) {
24
35
  const key = `${name}@${version}`
25
- if (!this._tracers.has(key)) {
26
- this._tracers.set(key, new Tracer(
36
+ if (!this.#tracers.has(key)) {
37
+ this.#tracers.set(key, new Tracer(
27
38
  { ...options, name, version },
28
39
  this.config,
29
40
  this
30
41
  ))
31
42
  }
32
- return this._tracers.get(key)
43
+ return this.#tracers.get(key)
33
44
  }
34
45
 
46
+ /**
47
+ * @param {NoopSpanProcessor} spanProcessor
48
+ */
35
49
  addSpanProcessor (spanProcessor) {
36
- if (!this._processors.length) {
37
- this._activeProcessor.shutdown()
50
+ if (this.#processors.includes(spanProcessor)) return
51
+
52
+ if (!this.#processors.length) {
53
+ this.#activeProcessor.shutdown()
38
54
  }
39
- this._processors.push(spanProcessor)
40
- this._activeProcessor = new MultiSpanProcessor(
41
- this._processors
55
+ this.#processors.push(spanProcessor)
56
+ this.#activeProcessor = new MultiSpanProcessor(
57
+ this.#processors
42
58
  )
43
59
  }
44
60
 
45
61
  getActiveSpanProcessor () {
46
- return this._activeProcessor
62
+ return this.#activeProcessor
47
63
  }
48
64
 
49
65
  // Not actually required by the SDK spec, but the official Node.js SDK does
50
66
  // this and the docs reflect that so we should do this too for familiarity.
51
67
  register (config = {}) {
52
- context.setGlobalContextManager(this._contextManager)
68
+ context.setGlobalContextManager(this.#contextManager)
53
69
  if (!trace.setGlobalTracerProvider(this)) {
54
70
  trace.getTracerProvider().setDelegate(this)
55
71
  }
@@ -69,11 +85,11 @@ class TracerProvider {
69
85
  }
70
86
 
71
87
  exporter._writer?.flush()
72
- return this._activeProcessor.forceFlush()
88
+ return this.#activeProcessor.forceFlush()
73
89
  }
74
90
 
75
91
  shutdown () {
76
- return this._activeProcessor.shutdown()
92
+ return this.#activeProcessor.shutdown()
77
93
  }
78
94
  }
79
95
 
@@ -84,6 +84,9 @@ const plugins = {
84
84
  get langchain () { return require('../../../datadog-plugin-langchain/src') },
85
85
  get mariadb () { return require('../../../datadog-plugin-mariadb/src') },
86
86
  get memcached () { return require('../../../datadog-plugin-memcached/src') },
87
+ // mercurius is traced under the graphql plugin: its instrumentation opens the
88
+ // top-level graphql.request span handled by the graphql CompositePlugin.
89
+ get mercurius () { return require('../../../datadog-plugin-graphql/src') },
87
90
  get 'microgateway-core' () { return require('../../../datadog-plugin-microgateway-core/src') },
88
91
  get mocha () { return require('../../../datadog-plugin-mocha/src') },
89
92
  get 'mocha-each' () { return require('../../../datadog-plugin-mocha/src') },