dd-trace 5.106.0 → 5.107.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 (96) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +5 -7
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  7. package/packages/datadog-instrumentations/src/connect.js +3 -2
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +7 -0
  10. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  11. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  12. package/packages/datadog-instrumentations/src/express.js +24 -20
  13. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  14. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  15. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  16. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  17. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  18. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  19. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  20. package/packages/datadog-instrumentations/src/jest.js +143 -73
  21. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  23. package/packages/datadog-instrumentations/src/multer.js +3 -2
  24. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  25. package/packages/datadog-instrumentations/src/net.js +8 -6
  26. package/packages/datadog-instrumentations/src/openai.js +19 -7
  27. package/packages/datadog-instrumentations/src/pg.js +19 -0
  28. package/packages/datadog-instrumentations/src/router.js +12 -10
  29. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  30. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  31. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  32. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  33. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  34. package/packages/datadog-plugin-http/src/client.js +0 -3
  35. package/packages/datadog-plugin-http/src/server.js +11 -1
  36. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  37. package/packages/datadog-plugin-pg/src/index.js +10 -0
  38. package/packages/dd-trace/src/aiguard/index.js +34 -15
  39. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  40. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  41. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  42. package/packages/dd-trace/src/config/defaults.js +14 -0
  43. package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -1
  44. package/packages/dd-trace/src/config/helper.js +1 -0
  45. package/packages/dd-trace/src/config/index.js +5 -9
  46. package/packages/dd-trace/src/config/parsers.js +8 -0
  47. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  48. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  49. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  50. package/packages/dd-trace/src/debugger/config.js +1 -1
  51. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  52. package/packages/dd-trace/src/debugger/index.js +1 -2
  53. package/packages/dd-trace/src/dogstatsd.js +2 -3
  54. package/packages/dd-trace/src/encode/0.4.js +49 -41
  55. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  56. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  57. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  58. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  59. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  60. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  61. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  62. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  63. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  64. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  65. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  66. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  67. package/packages/dd-trace/src/llmobs/util.js +54 -0
  68. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  69. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  70. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  71. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  72. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  73. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  74. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  75. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
  76. package/packages/dd-trace/src/opentracing/span.js +23 -18
  77. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  78. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  79. package/packages/dd-trace/src/priority_sampler.js +6 -5
  80. package/packages/dd-trace/src/profiling/config.js +1 -2
  81. package/packages/dd-trace/src/proxy.js +13 -10
  82. package/packages/dd-trace/src/remote_config/index.js +1 -2
  83. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  84. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  85. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  86. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  87. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  88. package/packages/dd-trace/src/span_format.js +33 -25
  89. package/packages/dd-trace/src/span_stats.js +1 -1
  90. package/packages/dd-trace/src/startup-log.js +1 -2
  91. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  92. package/packages/dd-trace/src/tracer.js +1 -1
  93. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  94. package/vendor/dist/shell-quote/index.js +1 -1
  95. package/packages/dd-trace/src/agent/url.js +0 -28
  96. package/scripts/preinstall.js +0 -34
@@ -44,27 +44,46 @@ function disable () {
44
44
  /**
45
45
  * Handles channel messages with pre-converted messages.
46
46
  *
47
- * @param {{messages: Array<object>, integration?: string, resolve: Function, reject: Function}} ctx
47
+ * @param {object} ctx
48
+ * @param {Array<object>} ctx.messages
49
+ * @param {string} [ctx.integration]
50
+ * @param {object} [ctx.parentSpan] - LLM span to parent the `ai_guard` span under.
51
+ * @param {AbortController} ctx.abortController
52
+ * @param {Array<Promise<void>>} ctx.pending - Subscribers push only when they evaluate.
48
53
  */
49
54
  function onEvaluate (ctx) {
55
+ // Decline to evaluate empty payloads by not pushing to pending.
50
56
  if (!ctx.messages?.length) {
51
- ctx.resolve()
52
57
  return
53
58
  }
54
59
 
55
- const opts = { block, source: SOURCE_AUTO, integration: ctx.integration || INTEGRATION_NONE }
56
- aiguard.evaluate(ctx.messages, opts)
57
- .then(() => {
58
- ctx.resolve()
59
- })
60
- .catch(err => {
61
- if (err.name === 'AIGuardAbortError') {
62
- ctx.reject(err)
63
- } else {
64
- log.error('AIGuard: unexpected error during evaluation: %s', err.message)
65
- ctx.resolve()
66
- }
67
- })
60
+ const opts = {
61
+ block,
62
+ source: SOURCE_AUTO,
63
+ integration: ctx.integration || INTEGRATION_NONE,
64
+ childOf: ctx.parentSpan,
65
+ }
66
+
67
+ try {
68
+ ctx.pending.push(aiguard.evaluate(ctx.messages, opts).catch(handleEvaluationError.bind(null, ctx)))
69
+ } catch (err) {
70
+ ctx.pending.push(Promise.resolve().then(() => handleEvaluationError(ctx, err)))
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Handles an AI Guard evaluation failure.
76
+ *
77
+ * @param {object} ctx
78
+ * @param {AbortController} ctx.abortController
79
+ * @param {Error} err
80
+ */
81
+ function handleEvaluationError (ctx, err) {
82
+ if (err.name === 'AIGuardAbortError') {
83
+ ctx.abortController.abort(err)
84
+ } else {
85
+ log.error('AIGuard: unexpected error during evaluation: %s', err.message)
86
+ }
68
87
  }
69
88
 
70
89
  module.exports = { enable, disable }
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
4
- const { HTTP_CLIENT_IP, NETWORK_CLIENT_IP } = require('../../../../ext/tags')
4
+ const { HTTP_CLIENT_IP, NETWORK_CLIENT_IP, HTTP_USERAGENT } = require('../../../../ext/tags')
5
+ const { USER_ID, USER_SESSION_ID } = require('../appsec/addresses')
5
6
  const { getActiveRequest } = require('../appsec/store')
6
7
  const log = require('../log')
7
8
  const { extractIp } = require('../plugins/util/ip_extractor')
@@ -17,6 +18,16 @@ const aiguardMetrics = telemetryMetrics.manager.namespace('ai_guard')
17
18
 
18
19
  const ALLOW = 'ALLOW'
19
20
 
21
+ // Tags from the service entry span that must be mirrored onto every AI Guard span
22
+ // so anomaly detection pipelines can process each span independently.
23
+ const SERVICE_ENTRY_TAG_MAPPINGS = [
24
+ [HTTP_USERAGENT, TAGS.HTTP_USERAGENT_TAG_KEY],
25
+ [HTTP_CLIENT_IP, TAGS.HTTP_CLIENT_IP_TAG_KEY],
26
+ [NETWORK_CLIENT_IP, TAGS.NETWORK_CLIENT_IP_TAG_KEY],
27
+ [USER_ID, TAGS.USR_ID_TAG_KEY],
28
+ [USER_SESSION_ID, TAGS.USR_SESSION_ID_TAG_KEY],
29
+ ]
30
+
20
31
  /**
21
32
  * Reports a telemetry error
22
33
  *
@@ -181,13 +192,32 @@ class AIGuard extends NoopAIGuard {
181
192
  }
182
193
  }
183
194
 
195
+ /**
196
+ * Copies service entry span tags needed by anomaly detection to the AI Guard span.
197
+ *
198
+ * @param {import('../opentracing/span')} guardSpan
199
+ * @param {import('../opentracing/span')} rootSpan
200
+ */
201
+ #copyServiceEntryTagsToGuardSpan (guardSpan, rootSpan) {
202
+ const rootTags = rootSpan.context().getTags()
203
+ for (const [sourceTag, destTag] of SERVICE_ENTRY_TAG_MAPPINGS) {
204
+ const value = rootTags[sourceTag]
205
+ if (value !== undefined && value !== null) {
206
+ guardSpan.setTag(destTag, value)
207
+ }
208
+ }
209
+ }
210
+
184
211
  evaluate (messages, opts) {
185
212
  if (!this.#initialized) {
186
213
  return super.evaluate(messages, opts)
187
214
  }
188
- const { block = true, source = TAGS.SOURCE_SDK, integration = TAGS.INTEGRATION_NONE } = opts ?? {}
215
+ const { block = true, source = TAGS.SOURCE_SDK, integration = TAGS.INTEGRATION_NONE, childOf } = opts ?? {}
189
216
  const telemetryTags = { source, integration }
190
- return this.#tracer.trace(TAGS.RESOURCE, {}, async (span) => {
217
+ // Only pass `childOf` when truthy so `tracer.trace`'s default (`scope().active()`)
218
+ // still applies for SDK callers that don't supply an explicit parent.
219
+ const traceOpts = childOf ? { childOf } : {}
220
+ return this.#tracer.trace(TAGS.RESOURCE, traceOpts, async (span) => {
191
221
  const last = messages[messages.length - 1]
192
222
  const target = this.#isToolCall(last) ? 'tool' : 'prompt'
193
223
  span.setTag(TAGS.TARGET_TAG_KEY, target)
@@ -206,6 +236,7 @@ class AIGuard extends NoopAIGuard {
206
236
  const rootSpan = span.context()?._trace?.started?.[0]
207
237
  if (rootSpan) {
208
238
  this.#setRootSpanClientIpTags(rootSpan)
239
+ this.#copyServiceEntryTagsToGuardSpan(span, rootSpan)
209
240
  // keepTrace must be called before executeRequest so the sampling decision
210
241
  // is propagated correctly to outgoing HTTP client calls.
211
242
  keepTrace(rootSpan, AI_GUARD)
@@ -10,6 +10,12 @@ module.exports = {
10
10
  EVENT_TAG_KEY: 'ai_guard.event',
11
11
  META_STRUCT_KEY: 'ai_guard',
12
12
 
13
+ HTTP_USERAGENT_TAG_KEY: 'ai_guard.http.useragent',
14
+ HTTP_CLIENT_IP_TAG_KEY: 'ai_guard.http.client_ip',
15
+ NETWORK_CLIENT_IP_TAG_KEY: 'ai_guard.network.client.ip',
16
+ USR_ID_TAG_KEY: 'ai_guard.usr.id',
17
+ USR_SESSION_ID_TAG_KEY: 'ai_guard.usr.session_id',
18
+
13
19
  TELEMETRY_REQUESTS: 'requests',
14
20
  TELEMETRY_TRUNCATED: 'truncated',
15
21
  TELEMETRY_ERROR: 'error',
@@ -9,7 +9,7 @@ const CoverageWriter = require('./coverage-writer')
9
9
  class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
10
10
  constructor (config) {
11
11
  super(config)
12
- const { tags, site, url, isTestDynamicInstrumentationEnabled } = config
12
+ const { tags, site, DD_CIVISIBILITY_AGENTLESS_URL: url, isTestDynamicInstrumentationEnabled } = config
13
13
  // we don't need to request /info because we are using agentless by configuration
14
14
  this._isInitialized = true
15
15
  this._resolveCanUseCiVisProtocol(true)
@@ -72,6 +72,12 @@ for (const [name, value] of Object.entries(defaults)) {
72
72
  function generateTelemetry (value = null, origin, optionName) {
73
73
  const tableEntry = configurationsTable[optionName]
74
74
  const { type, canonicalName = optionName } = tableEntry ?? { type: typeof value }
75
+ // Sensitive configurations are excluded from configuration telemetry: their
76
+ // entry is never added to `configWithOrigin`, the single source for every
77
+ // telemetry path (app-started and app-client-configuration-change).
78
+ if (sensitiveConfigurations.has(canonicalName)) {
79
+ return
80
+ }
75
81
  // TODO: Should we not send defaults to telemetry to reduce size?
76
82
  // TODO: How to handle aliases/actual names in the future? Optional fields? Normalize the name at intake?
77
83
  // TODO: Validate that space separated tags are parsed by the backend. Optimizations would be possible with that.
@@ -196,6 +202,11 @@ const fallbackConfigurations = new Map()
196
202
 
197
203
  const regExps = {}
198
204
 
205
+ // Canonical names of configurations whose value is excluded from configuration
206
+ // telemetry. Driven by the `sensitive: true` attribute in
207
+ // `supported-configurations.json` so new entries opt in without code changes.
208
+ const sensitiveConfigurations = new Set()
209
+
199
210
  for (const [canonicalName, entries] of Object.entries(supportedConfigurations)) {
200
211
  if (entries.length !== 1) {
201
212
  // TODO: Determine if we really want to support multiple entries for a canonical name.
@@ -207,6 +218,9 @@ for (const [canonicalName, entries] of Object.entries(supportedConfigurations))
207
218
  )
208
219
  }
209
220
  for (const entry of entries) {
221
+ if (entry.sensitive) {
222
+ sensitiveConfigurations.add(canonicalName)
223
+ }
210
224
  const configurationNames = entry.internalPropertyName ? [entry.internalPropertyName] : entry.configurationNames
211
225
  const fullPropertyName = configurationNames?.[0] ?? canonicalName
212
226
  const type = entry.type.toUpperCase()
@@ -73,7 +73,7 @@ export interface GeneratedConfig {
73
73
  DD_APP_KEY: string | undefined;
74
74
  DD_AZURE_RESOURCE_GROUP: string | undefined;
75
75
  DD_CIVISIBILITY_AGENTLESS_ENABLED: boolean;
76
- DD_CIVISIBILITY_AGENTLESS_URL: string | undefined;
76
+ DD_CIVISIBILITY_AGENTLESS_URL: URL | undefined;
77
77
  DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER: string | undefined;
78
78
  DD_CIVISIBILITY_DANGEROUSLY_FORCE_COVERAGE: boolean;
79
79
  DD_CIVISIBILITY_DANGEROUSLY_FORCE_TEST_SKIPPING: boolean;
@@ -13,6 +13,7 @@
13
13
  * @property {string} [transform]
14
14
  * @property {string} [allowed]
15
15
  * @property {string|boolean} [deprecated]
16
+ * @property {boolean} [sensitive] Excludes the configuration value from configuration telemetry.
16
17
  */
17
18
 
18
19
  /**
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs')
4
4
  const os = require('node:os')
5
- const { URL } = require('node:url')
5
+ const { URL, format } = require('node:url')
6
6
 
7
7
  const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
8
8
  const uuid = require('../../../../vendor/dist/crypto-randomuuid') // we need to keep the old uuid dep because of cypress
@@ -322,18 +322,14 @@ class Config extends ConfigBase {
322
322
  #applyCalculated () {
323
323
  undo(this, 'calculated')
324
324
 
325
- if (this.DD_CIVISIBILITY_AGENTLESS_URL ||
326
- this.url ||
325
+ if (this.url ||
327
326
  os.type() !== 'Windows_NT' &&
328
327
  !trackedConfigOrigins.has('hostname') &&
329
328
  !trackedConfigOrigins.has('port') &&
330
- !this.DD_CIVISIBILITY_AGENTLESS_ENABLED &&
331
329
  fs.existsSync('/var/run/datadog/apm.socket')) {
332
- setAndTrack(
333
- this,
334
- 'url',
335
- new URL(this.DD_CIVISIBILITY_AGENTLESS_URL || this.url || 'unix:///var/run/datadog/apm.socket')
336
- )
330
+ setAndTrack(this, 'url', new URL(this.url || 'unix:///var/run/datadog/apm.socket'))
331
+ } else {
332
+ setAndTrack(this, 'url', new URL(format({ protocol: 'http:', hostname: this.hostname, port: this.port })))
337
333
  }
338
334
 
339
335
  if (this.isCiVisibility) {
@@ -145,6 +145,14 @@ const transformers = {
145
145
  }
146
146
  return value.replaceAll(/\s*:\s*/g, ':')
147
147
  },
148
+ /**
149
+ * @param {string} value
150
+ */
151
+ toURL (value) {
152
+ try {
153
+ return new URL(value)
154
+ } catch {}
155
+ },
148
156
  validatePropagationStyles (value, optionName) {
149
157
  value = transformers.toLowerCase(value)
150
158
  for (let index = 0; index < value.length; index++) {
@@ -111,7 +111,8 @@
111
111
  "aliases": [
112
112
  "DATADOG_API_KEY"
113
113
  ],
114
- "internalPropertyName": "apiKey"
114
+ "internalPropertyName": "apiKey",
115
+ "sensitive": true
115
116
  }
116
117
  ],
117
118
  "DD_API_SECURITY_ENABLED": [
@@ -423,7 +424,8 @@
423
424
  {
424
425
  "implementation": "A",
425
426
  "type": "string",
426
- "default": null
427
+ "default": null,
428
+ "sensitive": true
427
429
  }
428
430
  ],
429
431
  "DD_AZURE_RESOURCE_GROUP": [
@@ -444,6 +446,7 @@
444
446
  {
445
447
  "implementation": "A",
446
448
  "type": "string",
449
+ "transform": "toURL",
447
450
  "default": null
448
451
  }
449
452
  ],
@@ -3913,7 +3916,8 @@
3913
3916
  {
3914
3917
  "implementation": "B",
3915
3918
  "type": "map",
3916
- "default": null
3919
+ "default": null,
3920
+ "sensitive": true
3917
3921
  }
3918
3922
  ],
3919
3923
  "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": [
@@ -3930,7 +3934,8 @@
3930
3934
  "default": null,
3931
3935
  "aliases": [
3932
3936
  "OTEL_EXPORTER_OTLP_HEADERS"
3933
- ]
3937
+ ],
3938
+ "sensitive": true
3934
3939
  }
3935
3940
  ],
3936
3941
  "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": [
@@ -3968,7 +3973,8 @@
3968
3973
  "default": null,
3969
3974
  "aliases": [
3970
3975
  "OTEL_EXPORTER_OTLP_HEADERS"
3971
- ]
3976
+ ],
3977
+ "sensitive": true
3972
3978
  }
3973
3979
  ],
3974
3980
  "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": [
@@ -4006,7 +4012,8 @@
4006
4012
  "default": null,
4007
4013
  "aliases": [
4008
4014
  "OTEL_EXPORTER_OTLP_HEADERS"
4009
- ]
4015
+ ],
4016
+ "sensitive": true
4010
4017
  }
4011
4018
  ],
4012
4019
  "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": [
@@ -7,7 +7,6 @@ const libdatadog = require('@datadog/libdatadog')
7
7
  const binding = libdatadog.load('crashtracker')
8
8
 
9
9
  const log = require('../log')
10
- const { getAgentUrl } = require('../agent/url')
11
10
  const pkg = require('../../../../package.json')
12
11
  const processTags = require('../process-tags')
13
12
 
@@ -68,7 +67,7 @@ class Crashtracker {
68
67
  * @param {import('../config/config-base')} config - Tracer configuration
69
68
  */
70
69
  #getConfig (config) {
71
- const url = getAgentUrl(config)
70
+ const url = config.url
72
71
 
73
72
  // Out-of-process symbolication currently works on
74
73
  // Linux only, does not work on Mac.
@@ -78,6 +77,7 @@ class Crashtracker {
78
77
 
79
78
  return {
80
79
  additional_files: [],
80
+ collect_all_threads: true,
81
81
  create_alt_stack: true,
82
82
  use_alt_stack: true,
83
83
  endpoint: {
@@ -5,7 +5,6 @@ const pkg = require('../../../../package.json')
5
5
  const log = require('../log')
6
6
  const request = require('../exporters/common/request')
7
7
  const { encode: encodeMsgpack } = require('../msgpack')
8
- const { getAgentUrl } = require('../agent/url')
9
8
 
10
9
  function makeRequest (data, url, cb) {
11
10
  const options = {
@@ -29,7 +28,7 @@ function makeRequest (data, url, cb) {
29
28
 
30
29
  class DataStreamsWriter {
31
30
  constructor (config) {
32
- this._url = getAgentUrl(config)
31
+ this._url = config.url
33
32
  }
34
33
 
35
34
  flush (payload) {
@@ -16,7 +16,7 @@ module.exports = function getDebuggerConfig (config, inputPath) {
16
16
  repositoryUrl,
17
17
  runtimeId: config.tags['runtime-id'],
18
18
  service: config.service,
19
- url: config.url?.toString(),
19
+ url: config.url.toString(),
20
20
  version: config.version,
21
21
  inputPath,
22
22
  }
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { workerData: { config: parentConfig, parentThreadId, configPort } } = require('node:worker_threads')
4
- const { getAgentUrl } = require('../../agent/url')
5
4
  const processTags = require('../../process-tags')
6
5
  const log = require('./log')
7
6
 
@@ -21,6 +20,8 @@ configPort.on('messageerror', (err) =>
21
20
  )
22
21
 
23
22
  function updateConfig (updates) {
24
- config.url = getAgentUrl(updates)
23
+ // The worker receives a serialized config (see ../config.js) where `url` is a string, so it is
24
+ // reconstructed into a URL here rather than read directly off a Config instance.
25
+ config.url = new URL(updates.url)
25
26
  config.dynamicInstrumentation.captureTimeoutNs = BigInt(updates.dynamicInstrumentation.captureTimeoutMs) * 1_000_000n
26
27
  }
@@ -6,7 +6,6 @@ const { join } = require('path')
6
6
  const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_threads')
7
7
  const log = require('../log')
8
8
  const { fetchAgentInfo } = require('../agent/info')
9
- const { getAgentUrl } = require('../agent/url')
10
9
  const getDebuggerConfig = require('./config')
11
10
  const { DEBUGGER_DIAGNOSTICS_V1, DEBUGGER_INPUT_V2 } = require('./constants')
12
11
 
@@ -211,7 +210,7 @@ function cleanup (error) {
211
210
  function detectDebuggerEndpoint (config, cb) {
212
211
  log.debug('[debugger] Detecting available debugger endpoints...')
213
212
 
214
- fetchAgentInfo(getAgentUrl(config), (err, agentInfo) => {
213
+ fetchAgentInfo(config.url, (err, agentInfo) => {
215
214
  if (err) {
216
215
  log.warn('[debugger] Failed to query agent %s endpoint, falling back to %s',
217
216
  DEBUGGER_INPUT_V2,
@@ -6,7 +6,6 @@ const isIP = require('net').isIP
6
6
  const request = require('./exporters/common/request')
7
7
  const log = require('./log')
8
8
  const Histogram = require('./histogram')
9
- const { getAgentUrl } = require('./agent/url')
10
9
  const { entityId } = require('./exporters/common/docker')
11
10
 
12
11
  const MAX_BUFFER_SIZE = 1024 // limit from the agent
@@ -191,8 +190,8 @@ class DogStatsDClient {
191
190
  lookup: config.lookup,
192
191
  }
193
192
 
194
- if (config.url || config.port) {
195
- clientConfig.metricsProxyUrl = getAgentUrl(config)
193
+ if (config.url) {
194
+ clientConfig.metricsProxyUrl = config.url
196
195
  }
197
196
 
198
197
  return clientConfig
@@ -3,7 +3,7 @@
3
3
  const getConfig = require('../config')
4
4
  const { MsgpackChunk } = require('../msgpack')
5
5
  const log = require('../log')
6
- const { normalizeSpan } = require('./tags-processors')
6
+ const { normalizeSpan, eventTimeNano } = require('./tags-processors')
7
7
 
8
8
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
9
9
  // Values longer than this byte threshold skip the `_stringMap` lookup and
@@ -114,8 +114,10 @@ const ATTR_PAYLOAD_BOOL_FALSE = Buffer.concat([ATTR_PREFIX_BOOL, Buffer.from([0x
114
114
  function formatSpanWithLegacyEvents (span) {
115
115
  span = normalizeSpan(span)
116
116
  if (span.span_events) {
117
- // TODO: this is currently a main cost driver. By unifying it with the formatter
118
- // it should be possible to improve performance significantly overall.
117
+ // Reads the raw `_events` array directly (no formatter pre-reshape) and
118
+ // serializes to the legacy meta.events JSON string. The serialization is
119
+ // still the main cost on the legacy path; the native span_events slot
120
+ // (`#encodeSpanEvents`) avoids it entirely.
119
121
  span.meta.events = stringifySpanEvents(span.span_events)
120
122
  // `= undefined` over `delete` to keep the span's hidden class — `delete`
121
123
  // would push every event-bearing span into V8 dictionary mode.
@@ -125,14 +127,15 @@ function formatSpanWithLegacyEvents (span) {
125
127
  }
126
128
 
127
129
  /**
128
- * Hand-written stringifier for `span.span_events`. The shape is fixed by
129
- * `extractSpanEvents` (`{ name, time_unix_nano, attributes? }`) and attribute
130
- * values are pre-sanitized to primitives or arrays of primitives, so we can
131
- * skip everything `JSON.stringify` does for the generic case (toJSON probing,
132
- * key iteration over the prototype chain, replacer hooks). Output matches
133
- * `JSON.stringify(spanEvents)` byte-for-byte for the post-sanitization shape.
130
+ * Hand-written stringifier for `span.span_events`. Events arrive in their raw
131
+ * `{ name, startTime, attributes? }` shape; `time_unix_nano` is derived per
132
+ * event via `eventTimeNano` and empty attribute objects are dropped, matching
133
+ * what the formatter used to precompute. Attribute values are pre-sanitized to
134
+ * primitives or arrays of primitives, so we skip everything `JSON.stringify`
135
+ * does for the generic case (toJSON probing, prototype-chain key iteration,
136
+ * replacer hooks).
134
137
  *
135
- * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
138
+ * @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
136
139
  * @returns {string}
137
140
  */
138
141
  function stringifySpanEvents (spanEvents) {
@@ -140,17 +143,21 @@ function stringifySpanEvents (spanEvents) {
140
143
  for (let index = 0; index < spanEvents.length; index++) {
141
144
  if (index > 0) result += ','
142
145
  const event = spanEvents[index]
146
+ // `_sanitizeEventAttributes` leaves `attributes` undefined when empty, so a
147
+ // present value always has entries — no emptiness probe here.
148
+ const attributes = event.attributes
143
149
  // `addEvent` does not type-check `name`; defer the unusual cases to
144
- // `JSON.stringify` so non-string names match the prior behaviour
145
- // instead of throwing in `escapeJsonString`.
150
+ // `JSON.stringify` so non-string names match the prior behaviour instead
151
+ // of throwing in `escapeJsonString`. Build the wire-shaped object so the
152
+ // emitted key stays `time_unix_nano`, not the raw `startTime`.
146
153
  if (typeof event.name !== 'string') {
147
- result += JSON.stringify(event)
154
+ result += JSON.stringify({ name: event.name, time_unix_nano: eventTimeNano(event), attributes })
148
155
  continue
149
156
  }
150
157
  result += '{"name":' + escapeJsonString(event.name) +
151
- ',"time_unix_nano":' + jsonNumber(event.time_unix_nano)
152
- if (event.attributes) {
153
- result += ',"attributes":' + stringifyAttributes(event.attributes)
158
+ ',"time_unix_nano":' + jsonNumber(eventTimeNano(event))
159
+ if (attributes) {
160
+ result += ',"attributes":' + stringifyAttributes(attributes)
154
161
  }
155
162
  result += '}'
156
163
  }
@@ -241,8 +248,8 @@ class AgentEncoder {
241
248
  this.#config = getConfig()
242
249
  this.#debugEncoding = this.#config.DD_TRACE_ENCODING_DEBUG
243
250
  // Pick the per-span formatter once so the hot loop pays no per-span
244
- // config check. The native path doesn't need to reshape `span_events`
245
- // because `#encodeSpanEvents` works directly on the raw attributes.
251
+ // config check. The native path keeps the raw `span_events` slot for
252
+ // `#encodeSpanEvents`; the legacy path serializes it into meta.events.
246
253
  this.#formatSpan = this.#config.DD_TRACE_NATIVE_SPAN_EVENTS
247
254
  ? normalizeSpan
248
255
  : formatSpanWithLegacyEvents
@@ -320,16 +327,18 @@ class AgentEncoder {
320
327
  const resourceLen = resourceEntry.length
321
328
  const serviceLen = serviceEntry.length
322
329
 
323
- // Almost every span carries `error: 0` or `error: 1` AND a nanosecond
324
- // `start` timestamp ≥ 2³² (so `start` always encodes as a u64). When
325
- // both hold, the block fuses error key+value, the start key + 0xCF
326
- // type byte + 8-byte timestamp, and the duration key into the per-span
327
- // reserve. The fallback path covers synthetic/test inputs with small
328
- // starts and rare non-binary error flags by keeping per-field emits so
329
- // each integer picks the shortest msgpack encoding.
330
- const errorIsFixint = span.error === 0 || span.error === 1
331
- const startFitsU64 = span.start >= 0x1_00_00_00_00
332
- const fuseTail = errorIsFixint && startFitsU64
330
+ // `error` is `0` or `1` on nearly every span, and `start` is a
331
+ // nanosecond timestamp ≥ 2³² (always a msgpack u64). Decide the fused
332
+ // error key+value up front (`KEY_ERROR_0` / `KEY_ERROR_1`, or `undefined`
333
+ // for the rare non-binary flag) so the tail fuses without re-deciding the
334
+ // error shape twice. The fused tail also needs `start` as a u64; when
335
+ // either misses (synthetic small `start`, non-binary error) the tail
336
+ // routes each integer through `writeIntOrFloat` for the shortest
337
+ // encoding.
338
+ const errorEntry = span.error === 0
339
+ ? KEY_ERROR_0
340
+ : span.error === 1 ? KEY_ERROR_1 : undefined
341
+ const fuseTail = errorEntry !== undefined && span.start >= 0x1_00_00_00_00
333
342
 
334
343
  let blockSize = 1 +
335
344
  KEY_TRACE_ID_PREFIX.length + 8 +
@@ -340,7 +349,7 @@ class AgentEncoder {
340
349
  KEY_SERVICE.length + serviceLen
341
350
  if (typeEntry) blockSize += KEY_TYPE.length + typeEntry.length
342
351
  if (fuseTail) {
343
- blockSize += KEY_ERROR_0.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
352
+ blockSize += errorEntry.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
344
353
  }
345
354
 
346
355
  const blockOffset = bytes.length
@@ -377,8 +386,8 @@ class AgentEncoder {
377
386
  cursor += serviceLen
378
387
 
379
388
  if (fuseTail) {
380
- target.set(span.error === 0 ? KEY_ERROR_0 : KEY_ERROR_1, cursor)
381
- cursor += KEY_ERROR_0.length
389
+ target.set(errorEntry, cursor)
390
+ cursor += errorEntry.length
382
391
 
383
392
  target.set(KEY_START_PREFIX, cursor)
384
393
  cursor += KEY_START_PREFIX.length
@@ -389,15 +398,14 @@ class AgentEncoder {
389
398
  cursor += 8
390
399
 
391
400
  target.set(KEY_DURATION, cursor)
401
+ } else if (errorEntry) {
402
+ bytes.set(errorEntry)
403
+ bytes.set(KEY_START)
404
+ bytes.writeIntOrFloat(span.start)
405
+ bytes.set(KEY_DURATION)
392
406
  } else {
393
- if (span.error === 0) {
394
- bytes.set(KEY_ERROR_0)
395
- } else if (span.error === 1) {
396
- bytes.set(KEY_ERROR_1)
397
- } else {
398
- bytes.set(KEY_ERROR)
399
- bytes.writeIntOrFloat(span.error)
400
- }
407
+ bytes.set(KEY_ERROR)
408
+ bytes.writeIntOrFloat(span.error)
401
409
  bytes.set(KEY_START)
402
410
  bytes.writeIntOrFloat(span.start)
403
411
  bytes.set(KEY_DURATION)
@@ -763,7 +771,7 @@ class AgentEncoder {
763
771
  * values — no `formatSpanEvents` pre-pass and no recursive generic walk.
764
772
  *
765
773
  * @param {MsgpackChunk} bytes
766
- * @param {Array<{ name: string, time_unix_nano: number, attributes?: object }>} spanEvents
774
+ * @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
767
775
  */
768
776
  #encodeSpanEvents (bytes, spanEvents) {
769
777
  const offset = bytes.length
@@ -784,7 +792,7 @@ class AgentEncoder {
784
792
  bytes.set(KEY_NAME)
785
793
  this._encodeString(bytes, event.name)
786
794
  bytes.set(KEY_EVENT_TIME)
787
- bytes.writeFloat(event.time_unix_nano)
795
+ bytes.writeFloat(eventTimeNano(event))
788
796
 
789
797
  const attributes = event.attributes
790
798
  if (attributes !== null && typeof attributes === 'object') {
@@ -3,6 +3,7 @@
3
3
  const log = require('../log')
4
4
  const { TOP_LEVEL_KEY } = require('../constants')
5
5
  const { normalizeSpan } = require('./tags-processors')
6
+ const { stringifySpanEvents } = require('./0.4')
6
7
 
7
8
  // Soft limit for estimated payload size. Triggers an early flush to stay under intake request size limits.
8
9
  const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
@@ -20,7 +21,10 @@ function formatSpan (span, isFirstSpan) {
20
21
  delete span.meta['_dd.p.tid']
21
22
 
22
23
  if (span.span_events) {
23
- span.meta.events = JSON.stringify(span.span_events)
24
+ // Events arrive raw (`{ name, startTime, attributes? }`); stringifySpanEvents
25
+ // derives `time_unix_nano` and drops empty attributes, matching the JSON the
26
+ // reshaped array used to produce.
27
+ span.meta.events = stringifySpanEvents(span.span_events)
24
28
  delete span.span_events
25
29
  }
26
30
 
@@ -46,6 +46,19 @@ function truncateSpanTestOpt (span) {
46
46
  return span
47
47
  }
48
48
 
49
+ /**
50
+ * Convert a raw span event's `startTime` (milliseconds, sub-millisecond
51
+ * precision) to the wire `time_unix_nano`. Single source of truth for the
52
+ * formula so the four encoders that consume `span_events` stay in lockstep;
53
+ * the formatter no longer reshapes events, it hands the raw array through.
54
+ *
55
+ * @param {{ startTime: number }} event
56
+ * @returns {number}
57
+ */
58
+ function eventTimeNano (event) {
59
+ return Math.round(event.startTime * 1e6)
60
+ }
61
+
49
62
  function normalizeSpan (span) {
50
63
  span.service = span.service || DEFAULT_SERVICE_NAME
51
64
  if (span.service.length > MAX_SERVICE_LENGTH) {
@@ -69,6 +82,7 @@ module.exports = {
69
82
  truncateSpan,
70
83
  truncateSpanTestOpt,
71
84
  normalizeSpan,
85
+ eventTimeNano,
72
86
  MAX_META_KEY_LENGTH,
73
87
  MAX_META_VALUE_LENGTH,
74
88
  MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION,