dd-trace 5.109.0 → 5.110.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 (128) hide show
  1. package/ci/init.js +4 -3
  2. package/index.d.ts +66 -24
  3. package/loader-hook.mjs +88 -2
  4. package/package.json +18 -14
  5. package/packages/datadog-esbuild/index.js +26 -0
  6. package/packages/datadog-esbuild/src/utils.js +46 -3
  7. package/packages/datadog-instrumentations/src/connect.js +4 -3
  8. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  9. package/packages/datadog-instrumentations/src/graphql.js +17 -6
  10. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  11. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  12. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  13. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  14. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  16. package/packages/datadog-instrumentations/src/koa.js +3 -2
  17. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  18. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  19. package/packages/datadog-instrumentations/src/next.js +10 -10
  20. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  21. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  22. package/packages/datadog-instrumentations/src/restify.js +4 -3
  23. package/packages/datadog-instrumentations/src/router.js +18 -5
  24. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  25. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  26. package/packages/datadog-instrumentations/src/vitest.js +264 -7
  27. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +13 -9
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  30. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +83 -91
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
  32. package/packages/datadog-plugin-http/src/client.js +8 -3
  33. package/packages/datadog-plugin-http2/src/client.js +5 -2
  34. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  35. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  36. package/packages/datadog-plugin-undici/src/index.js +5 -2
  37. package/packages/datadog-plugin-vitest/src/index.js +7 -1
  38. package/packages/datadog-webpack/index.js +17 -1
  39. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  40. package/packages/dd-trace/src/aiguard/sdk.js +13 -1
  41. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  42. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  43. package/packages/dd-trace/src/appsec/index.js +10 -6
  44. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  45. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  46. package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
  47. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  48. package/packages/dd-trace/src/config/defaults.js +22 -3
  49. package/packages/dd-trace/src/config/generated-config-types.d.ts +535 -1
  50. package/packages/dd-trace/src/config/helper.js +133 -33
  51. package/packages/dd-trace/src/config/index.js +4 -1
  52. package/packages/dd-trace/src/config/parsers.js +19 -1
  53. package/packages/dd-trace/src/config/supported-configurations.json +21 -3
  54. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -35
  55. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  56. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  57. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  58. package/packages/dd-trace/src/debugger/index.js +5 -0
  59. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  60. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  61. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  62. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  63. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  64. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  65. package/packages/dd-trace/src/guardrails/index.js +8 -5
  66. package/packages/dd-trace/src/index.js +6 -4
  67. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  68. package/packages/dd-trace/src/llmobs/index.js +16 -5
  69. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  70. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  71. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  72. package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
  73. package/packages/dd-trace/src/llmobs/tagger.js +55 -0
  74. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  75. package/packages/dd-trace/src/log/index.js +18 -20
  76. package/packages/dd-trace/src/openfeature/flagging_provider.js +2 -7
  77. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  78. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  79. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  80. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  81. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  82. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  83. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  84. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  85. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
  87. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  88. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  89. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  90. package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
  91. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  92. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  93. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  94. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  95. package/packages/dd-trace/src/plugin_manager.js +7 -1
  96. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -1
  97. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  98. package/packages/dd-trace/src/plugins/util/test.js +80 -0
  99. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  100. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  101. package/packages/dd-trace/src/priority_sampler.js +1 -13
  102. package/packages/dd-trace/src/profiling/config.js +129 -198
  103. package/packages/dd-trace/src/profiling/constants.js +4 -7
  104. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  105. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  106. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  107. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  108. package/packages/dd-trace/src/profiling/oom.js +71 -0
  109. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  110. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  111. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  112. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  113. package/packages/dd-trace/src/ritm.js +4 -2
  114. package/packages/dd-trace/src/serverless.js +1 -2
  115. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  116. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  117. package/packages/dd-trace/src/span_format.js +2 -1
  118. package/packages/dd-trace/src/span_processor.js +6 -0
  119. package/packages/dd-trace/src/telemetry/send-data.js +12 -10
  120. package/packages/dd-trace/src/tracer.js +15 -1
  121. package/packages/dd-trace/src/util.js +14 -0
  122. package/register.js +58 -1
  123. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  124. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  125. package/vendor/dist/pprof-format/index.js +1 -1
  126. package/vendor/dist/protobufjs/index.js +1 -1
  127. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  128. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -72,44 +72,53 @@ class Sqs extends BaseAwsSdkPlugin {
72
72
  })
73
73
  }
74
74
 
75
+ /**
76
+ * Start the consumer (`aws.response`) span for a receive. The first message carrying trace
77
+ * context becomes the parent; every additional one fans in as a span link.
78
+ *
79
+ * @param {{ request: object, response: object, needsFinish?: boolean, currentStore?: object }} ctx
80
+ * @returns {object | undefined} The store to activate for the consumer span, else the parent store.
81
+ */
75
82
  #startResponseSpan (ctx) {
76
83
  const { request, response } = ctx
77
- const contextExtraction = this.responseExtract(request.params, request.operation, response)
84
+ const carriers = this.responseExtract(request.params, request.operation, response)
78
85
 
79
86
  let store = this._parentMap.get(request)
80
87
  let span
81
- let parsedMessageAttributes
82
- let parsedFirstBody
83
- let firstBodyChecked = false
84
- if (contextExtraction !== undefined) {
85
- parsedFirstBody = contextExtraction.parsedBody
86
- firstBodyChecked = contextExtraction.bodyChecked === true
87
- if (contextExtraction.datadogContext !== undefined) {
88
- parsedMessageAttributes = contextExtraction.parsedAttributes
89
- // request:start records requestTags only after the isEnabled gate, so an absent entry
90
- // means this consumer is disabled — gate on it instead of paying isEnabled again here.
91
- const requestTags = this.requestTags.get(request)
92
- if (requestTags !== undefined) {
93
- ctx.needsFinish = true
94
- const options = {
95
- childOf: contextExtraction.datadogContext,
96
- meta: {
97
- ...requestTags,
98
- 'span.kind': 'server',
99
- },
100
- integrationName: 'aws-sdk',
88
+
89
+ if (carriers !== undefined) {
90
+ // request:start records requestTags only after the isEnabled gate, so an absent entry
91
+ // means this consumer is disabled — gate on it instead of paying isEnabled again here.
92
+ const requestTags = this.requestTags.get(request)
93
+ if (requestTags !== undefined) {
94
+ // A receive can return messages from many producers; fanning the extra ones in as span
95
+ // links is the shape dd-trace-java and dd-trace-py use for batch SQS receives.
96
+ for (const carrier of carriers) {
97
+ if (carrier === undefined) continue
98
+ const datadogContext = this.tracer.extract('text_map', carrier)
99
+ // A DSM-only carrier (a non-first sendMessageBatch entry when batchPropagationEnabled
100
+ // is off) extracts to null; span.addLink dereferences the context and would throw on it.
101
+ if (datadogContext === null) continue
102
+ if (span === undefined) {
103
+ ctx.needsFinish = true
104
+ span = this.startSpan('aws.response', {
105
+ childOf: datadogContext,
106
+ meta: {
107
+ ...requestTags,
108
+ 'span.kind': 'server',
109
+ },
110
+ integrationName: 'aws-sdk',
111
+ }, ctx)
112
+ store = ctx.currentStore
113
+ } else {
114
+ span.addLink({ context: datadogContext })
101
115
  }
102
- span = this.startSpan('aws.response', options, ctx)
103
- store = ctx.currentStore
104
116
  }
105
117
  }
106
118
  }
107
119
 
108
120
  // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
109
- this.responseExtractDSMContext(
110
- request.operation, request.params, response, span ?? null,
111
- { parsedAttributes: parsedMessageAttributes, parsedFirstBody, firstBodyChecked }
112
- )
121
+ this.responseExtractDSMContext(request.operation, request.params, response, span ?? null, carriers)
113
122
 
114
123
  return store
115
124
  }
@@ -183,19 +192,39 @@ class Sqs extends BaseAwsSdkPlugin {
183
192
  return tags
184
193
  }
185
194
 
195
+ /**
196
+ * Parse the trace-context carrier of every received message, in message order.
197
+ * Entries are `undefined` for messages that carry no `_datadog` context.
198
+ *
199
+ * @param {{ MaxNumberOfMessages?: number }} params
200
+ * @param {string} operation
201
+ * @param {{ Messages?: object[] }} response
202
+ * @returns {Array<Record<string, string> | undefined> | undefined}
203
+ */
186
204
  responseExtract (params, operation, response) {
187
205
  if (operation !== 'receiveMessage') return
188
- if (params.MaxNumberOfMessages && params.MaxNumberOfMessages !== 1) return
189
- if (!response || !response.Messages || !response.Messages[0]) return
206
+ if (!response?.Messages?.length) return
190
207
 
191
- let message = response.Messages[0]
192
- let parsedBody
208
+ return response.Messages.map(message => this.parseMessageCarrier(message))
209
+ }
193
210
 
211
+ /**
212
+ * Resolve the trace-context carrier for a single received message. The
213
+ * `MessageAttributes._datadog` text map (direct SQS or SNS to SQS) wins;
214
+ * otherwise the EventBridge envelope, optionally wrapped in an SNS
215
+ * `Notification` (see getEventBridgeContext). Checking MessageAttributes first
216
+ * avoids parsing a large SNS `Message` just to rule out an EventBridge envelope.
217
+ *
218
+ * @param {object} message A single `response.Messages` entry.
219
+ * @returns {Record<string, string> | undefined}
220
+ */
221
+ parseMessageCarrier (message) {
222
+ let parsedBody
194
223
  if (message.Body) {
195
224
  try {
196
225
  parsedBody = JSON.parse(message.Body)
197
226
  } catch {
198
- // SQS to SQS
227
+ // Opaque, non-JSON body (SQS to SQS).
199
228
  }
200
229
  // SNS to SQS
201
230
  if (parsedBody?.Type === 'Notification') {
@@ -203,34 +232,9 @@ class Sqs extends BaseAwsSdkPlugin {
203
232
  }
204
233
  }
205
234
 
206
- // Check MessageAttributes first (common direct-SQS/SNS path): avoids parsing
207
- // the body (e.g. a large SNS `Message`) just to rule out an EventBridge
208
- // envelope. Precedence matches responseExtractDSMContext.
209
235
  const datadogAttribute = message.MessageAttributes?._datadog
210
- if (datadogAttribute) {
211
- const parsedAttributes = this.parseDatadogAttributes(datadogAttribute)
212
- if (parsedAttributes) {
213
- return {
214
- datadogContext: this.tracer.extract('text_map', parsedAttributes),
215
- parsedAttributes,
216
- parsedBody,
217
- bodyChecked: true,
218
- }
219
- }
220
- }
221
-
222
- // Then the EventBridge envelope (optionally via SNS); see getEventBridgeContext.
223
- const eventBridgeContext = getEventBridgeContext(parsedBody)
224
- if (eventBridgeContext) {
225
- return {
226
- datadogContext: this.tracer.extract('text_map', eventBridgeContext),
227
- parsedAttributes: eventBridgeContext,
228
- parsedBody,
229
- bodyChecked: true,
230
- }
231
- }
232
-
233
- return { parsedBody, bodyChecked: true }
236
+ const carrier = datadogAttribute ? this.parseDatadogAttributes(datadogAttribute) : undefined
237
+ return carrier ?? getEventBridgeContext(parsedBody)
234
238
  }
235
239
 
236
240
  parseDatadogAttributes (attributes) {
@@ -247,51 +251,39 @@ class Sqs extends BaseAwsSdkPlugin {
247
251
  }
248
252
  }
249
253
 
250
- responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
251
- let { parsedAttributes } = kwargs
252
- const { parsedFirstBody, firstBodyChecked } = kwargs
254
+ /**
255
+ * @param {string} operation
256
+ * @param {{ QueueUrl: string }} params
257
+ * @param {{ Messages?: object[] }} response
258
+ * @param {import('../../../dd-trace/src/opentracing/span') | null} span
259
+ * @param {Array<Record<string, string> | undefined>} [carriers] Per-message carriers already
260
+ * parsed by `responseExtract`; reused so each message body is parsed once. When omitted, the
261
+ * carriers are parsed here.
262
+ */
263
+ responseExtractDSMContext (operation, params, response, span, carriers) {
253
264
  if (!this.config.dsmEnabled) return
254
265
  if (operation !== 'receiveMessage') return
255
- if (!response || !response.Messages || !response.Messages[0]) return
266
+ if (!response?.Messages?.length) return
256
267
 
268
+ const messages = response.Messages
257
269
  // Only attribute payloadSize to the span when there is a single message.
258
- span = response.Messages.length > 1 ? null : span
270
+ span = messages.length > 1 ? null : span
259
271
 
260
272
  // QueueUrl is the same for the whole receive batch.
261
273
  const queue = params.QueueUrl.slice(params.QueueUrl.lastIndexOf('/') + 1)
262
274
 
263
- for (let i = 0; i < response.Messages.length; i++) {
264
- let message = response.Messages[i]
265
- if (!parsedAttributes) {
266
- let body
267
- // responseExtract already parsed message[0]; reuse that result instead of re-parsing.
268
- if (i === 0 && firstBodyChecked) {
269
- body = parsedFirstBody
270
- } else if (message.Body) {
271
- try {
272
- body = JSON.parse(message.Body)
273
- } catch {
274
- // SQS to SQS
275
- }
276
- }
277
- // SNS to SQS
278
- if (body?.Type === 'Notification') {
279
- message = body
280
- }
281
- // MessageAttributes for direct SQS/SNS; else the EventBridge envelope.
282
- parsedAttributes = message.MessageAttributes?._datadog
283
- ? this.parseDatadogAttributes(message.MessageAttributes._datadog)
284
- : getEventBridgeContext(body)
275
+ for (let i = 0; i < messages.length; i++) {
276
+ const message = messages[i]
277
+ const carrier = carriers === undefined ? this.parseMessageCarrier(message) : carriers[i]
278
+ if (carrier) {
279
+ // Inert for EventBridge until its producer emits a pathway (separate
280
+ // change) no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
281
+ this.tracer.decodeDataStreamsContext(carrier)
285
282
  }
286
283
  const payloadSize = getHeadersSize({
287
284
  Body: message.Body,
288
285
  MessageAttributes: message.MessageAttributes,
289
286
  })
290
- if (parsedAttributes) {
291
- // Inert for EventBridge until its producer emits a pathway (separate
292
- // change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
293
- this.tracer.decodeDataStreamsContext(parsedAttributes)
294
- }
295
287
  this.tracer
296
288
  .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
297
289
  }
@@ -11,6 +11,7 @@ const {
11
11
  TEST_IS_RUM_ACTIVE,
12
12
  TEST_CODE_OWNERS,
13
13
  getTestEnvironmentMetadata,
14
+ getTestLevelsMetadataTags,
14
15
  CI_APP_ORIGIN,
15
16
  getTestParentSpan,
16
17
  getCodeOwnersFileEntries,
@@ -23,6 +24,7 @@ const {
23
24
  TEST_MODULE_ID,
24
25
  TEST_SESSION_ID,
25
26
  TEST_COMMAND,
27
+ TEST_LEVELS_METADATA,
26
28
  TEST_MODULE,
27
29
  TEST_SOURCE_START,
28
30
  finishAllTraceSpans,
@@ -1043,7 +1045,13 @@ class CypressPlugin {
1043
1045
  )
1044
1046
 
1045
1047
  if (this.tracer._tracer._exporter?.addMetadataTags) {
1046
- const metadataTags = { '*': { [TEST_COMMAND]: this.command, [TEST_SESSION_NAME]: testSessionName } }
1048
+ const metadataTags = {
1049
+ [TEST_LEVELS_METADATA]: {
1050
+ [TEST_COMMAND]: this.command,
1051
+ [TEST_SESSION_NAME]: testSessionName,
1052
+ ...getTestLevelsMetadataTags(this.testEnvironmentMetadata),
1053
+ },
1054
+ }
1047
1055
  const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, this.frameworkVersion)
1048
1056
  metadataTags.test = {
1049
1057
  ...libraryCapabilitiesTags,
@@ -8,6 +8,7 @@ const tags = require('../../../ext/tags')
8
8
  const formats = require('../../../ext/formats')
9
9
  const HTTP_HEADERS = formats.HTTP_HEADERS
10
10
  const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter')
11
+ const { buildClientHttpUrl } = require('../../dd-trace/src/plugins/util/url')
11
12
  const log = require('../../dd-trace/src/log')
12
13
  const { CLIENT_PORT_KEY, COMPONENT, ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants')
13
14
 
@@ -27,13 +28,17 @@ class HttpClientPlugin extends ClientPlugin {
27
28
  const protocol = options.protocol || agent.protocol || 'http:'
28
29
  const hostname = options.hostname || options.host || 'localhost'
29
30
  const host = options.port ? `${hostname}:${options.port}` : hostname
30
- const pathname = options.path || options.pathname
31
+ const base = `${protocol}//${host}`
32
+ // A URL object (e.g. from the fetch integration) carries the query in
33
+ // `options.search`, not `options.path`; keep it so url.full retains the query.
34
+ const pathname = options.path || `${options.pathname || ''}${options.search || ''}`
31
35
  const path = pathname ? pathname.split(/[?#]/)[0] : '/'
32
- const uri = `${protocol}//${host}${path}`
36
+ const uri = `${base}${path}`
33
37
 
34
38
  const allowed = this.config.filter(uri)
35
39
 
36
40
  const method = (options.method || 'GET').toUpperCase()
41
+ const otelSemantics = this.config.DD_TRACE_OTEL_SEMANTICS_ENABLED
37
42
  const childOf = store && allowed ? store.span : null
38
43
  // TODO delegate to super.startspan
39
44
  const span = this.startSpan(this.operationName(), {
@@ -46,7 +51,7 @@ class HttpClientPlugin extends ClientPlugin {
46
51
  'resource.name': method,
47
52
  'span.type': 'http',
48
53
  'http.method': method,
49
- 'http.url': uri,
54
+ 'http.url': otelSemantics ? buildClientHttpUrl(this.config, base, pathname, uri) : uri,
50
55
  'out.host': hostname,
51
56
  },
52
57
  metrics: {
@@ -10,6 +10,7 @@ const kinds = require('../../../ext/kinds')
10
10
  const formats = require('../../../ext/formats')
11
11
  const { COMPONENT, CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
12
12
  const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter')
13
+ const { buildClientHttpUrl } = require('../../dd-trace/src/plugins/util/url')
13
14
 
14
15
  const HTTP_HEADERS = formats.HTTP_HEADERS
15
16
  const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE
@@ -33,8 +34,10 @@ class Http2ClientPlugin extends ClientPlugin {
33
34
  const path = headers[HTTP2_HEADER_PATH] || '/'
34
35
  const pathname = path.split(/[?#]/)[0]
35
36
  const method = headers[HTTP2_HEADER_METHOD] || HTTP2_METHOD_GET
36
- const uri = `${sessionDetails.protocol}//${sessionDetails.host}:${sessionDetails.port}${pathname}`
37
+ const base = `${sessionDetails.protocol}//${sessionDetails.host}:${sessionDetails.port}`
38
+ const uri = `${base}${pathname}`
37
39
  const allowed = this.config.filter(uri)
40
+ const otelSemantics = this.config.DD_TRACE_OTEL_SEMANTICS_ENABLED
38
41
 
39
42
  const store = storage('legacy').getStore()
40
43
  const childOf = store && allowed ? store.span : null
@@ -48,7 +51,7 @@ class Http2ClientPlugin extends ClientPlugin {
48
51
  'resource.name': method,
49
52
  'span.type': 'http',
50
53
  'http.method': method,
51
- 'http.url': uri,
54
+ 'http.url': otelSemantics ? buildClientHttpUrl(this.config, base, path, uri) : uri,
52
55
  'out.host': sessionDetails.host,
53
56
  },
54
57
  metrics: {
@@ -11,11 +11,16 @@ class MySQLPlugin extends DatabasePlugin {
11
11
  constructor () {
12
12
  super(...arguments)
13
13
 
14
+ // Capture into `currentStore` (not `parentStore`) so connection:finish can
15
+ // restore the caller context even when the connection resolves inside an
16
+ // instrumentation skip (a noop store), as the mariadb pool does: the store
17
+ // binding only honors an explicit `currentStore` through a noop store.
18
+ // Without a skip (mysql/mysql2) this is unchanged.
14
19
  this.addSub(`apm:${this.component}:connection:start`, ctx => {
15
- ctx.parentStore = storage('legacy').getStore()
20
+ ctx.currentStore = storage('legacy').getStore()
16
21
  })
17
22
 
18
- this.addBind(`apm:${this.component}:connection:finish`, ctx => ctx.parentStore)
23
+ this.addBind(`apm:${this.component}:connection:finish`, ctx => ctx.currentStore)
19
24
  }
20
25
 
21
26
  bindStart (ctx) {
@@ -47,10 +47,10 @@ class SchemaExtractor {
47
47
 
48
48
  static extractProperty (field, schemaName, fieldName, builder, depth) {
49
49
  let array = false
50
- let description
51
50
  let ref
52
51
  let enumValues
53
52
 
53
+ const description = field.comment
54
54
  const resolvedType = field.resolvedType ? field.resolvedType.constructor.name : field.type
55
55
 
56
56
  const isRepeatedField = field.rule === 'repeated'
@@ -124,7 +124,7 @@ class SchemaExtractor {
124
124
  }
125
125
 
126
126
  iterateOverSchema (builder) {
127
- this.constructor.extractSchema(this.schema, builder, 0)
127
+ SchemaExtractor.extractSchema(this.schema, builder, 0)
128
128
  }
129
129
 
130
130
  static attachSchemaOnSpan (args, span, operation, tracer) {
@@ -6,6 +6,7 @@ const tags = require('../../../ext/tags')
6
6
  const formats = require('../../../ext/formats')
7
7
  const HTTP_HEADERS = formats.HTTP_HEADERS
8
8
  const log = require('../../dd-trace/src/log')
9
+ const { buildClientHttpUrl } = require('../../dd-trace/src/plugins/util/url')
9
10
  const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
10
11
 
11
12
  const {
@@ -60,10 +61,12 @@ class UndiciPlugin extends HttpClientPlugin {
60
61
  }
61
62
 
62
63
  const host = port ? `${hostname}:${port}` : hostname
64
+ const base = `${protocol}//${host}`
63
65
  const pathname = path.split(/[?#]/)[0]
64
- const uri = `${protocol}//${host}${pathname}`
66
+ const uri = `${base}${pathname}`
65
67
 
66
68
  const allowed = this.config.filter(uri)
69
+ const otelSemantics = this.config.DD_TRACE_OTEL_SEMANTICS_ENABLED
67
70
  const childOf = store && allowed ? store.span : null
68
71
 
69
72
  const span = this.startSpan(this.operationName(), {
@@ -71,7 +74,7 @@ class UndiciPlugin extends HttpClientPlugin {
71
74
  meta: {
72
75
  'span.kind': 'client',
73
76
  'http.method': method,
74
- 'http.url': uri,
77
+ 'http.url': otelSemantics ? buildClientHttpUrl(this.config, base, path, uri) : uri,
75
78
  'out.host': hostname,
76
79
  },
77
80
  metrics: {
@@ -9,6 +9,7 @@ const {
9
9
  finishAllTraceSpans,
10
10
  getTestSuitePath,
11
11
  getTestSuiteCommonTags,
12
+ getTestLevelsMetadataTags,
12
13
  getTestSessionName,
13
14
  getIsFaultyEarlyFlakeDetection,
14
15
  TEST_SOURCE_FILE,
@@ -16,6 +17,7 @@ const {
16
17
  TEST_CODE_COVERAGE_LINES_PCT,
17
18
  TEST_CODE_OWNERS,
18
19
  TEST_COMMAND,
20
+ TEST_LEVELS_METADATA,
19
21
  TEST_SESSION_NAME,
20
22
  TEST_SOURCE_START,
21
23
  TEST_IS_NEW,
@@ -328,7 +330,11 @@ class VitestPlugin extends CiPlugin {
328
330
  const testSessionName = getTestSessionName(this.config, trimmedCommand, this.testEnvironmentMetadata)
329
331
  if (this.tracer._exporter.addMetadataTags) {
330
332
  this.tracer._exporter.addMetadataTags({
331
- '*': { [TEST_COMMAND]: testCommand, [TEST_SESSION_NAME]: testSessionName },
333
+ [TEST_LEVELS_METADATA]: {
334
+ [TEST_COMMAND]: testCommand,
335
+ [TEST_SESSION_NAME]: testSessionName,
336
+ ...getTestLevelsMetadataTags(this.testEnvironmentMetadata),
337
+ },
332
338
  test: getLibraryCapabilitiesTags(this.constructor.id),
333
339
  })
334
340
  }
@@ -6,6 +6,7 @@ const fs = require('node:fs')
6
6
  const instrumentations = require('../datadog-instrumentations/src/helpers/instrumentations')
7
7
  const extractPackageAndModulePath = require('../datadog-instrumentations/src/helpers/extract-package-and-module-path')
8
8
  const hooks = require('../datadog-instrumentations/src/helpers/hooks')
9
+ const { matchesOptionalPeerFile } = require('../datadog-instrumentations/src/helpers/optional-peer-bundler')
9
10
  const { isESMFile } = require('../datadog-esbuild/src/utils')
10
11
  const log = require('./src/log')
11
12
 
@@ -130,11 +131,26 @@ class DatadogWebpackPlugin {
130
131
  nmf.hooks.afterResolve.tap(PLUGIN_NAME, (resolveData) => {
131
132
  const { createData } = resolveData
132
133
  const resource = createData?.resource
133
- if (!resource || !resource.includes('node_modules')) {
134
+ if (!resource) {
134
135
  return
135
136
  }
136
137
 
137
138
  const normalizedResource = resource.replaceAll('\\', '/')
139
+
140
+ // Rewrite optional-peer loads so installed peers get bundled and survive relocation
141
+ // (#8980); absent peers stay opaque, so a build that does not opt into the feature does
142
+ // not follow their dependency chain (#8635).
143
+ if (matchesOptionalPeerFile(normalizedResource)) {
144
+ createData.loaders = createData.loaders || []
145
+ createData.loaders.push({ loader: require.resolve('./src/optional-peer-loader') })
146
+ log.debug('INLINE: optional-peer loader applied to %s', normalizedResource)
147
+ return
148
+ }
149
+
150
+ if (!resource.includes('node_modules')) {
151
+ return
152
+ }
153
+
138
154
  const { pkg, path: modulePath, pkgJson } = extractPackageAndModulePath(normalizedResource)
139
155
  if (!pkg) {
140
156
  return
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const { rewriteOptionalPeerLoads } = require('../../datadog-instrumentations/src/helpers/optional-peer-bundler')
4
+
5
+ /**
6
+ * Webpack loader applied to the optional-peer loader files. Rewrites each
7
+ * `requireOptionalPeer('name')` whose peer is installed at build time into a literal
8
+ * `require('name')` so webpack bundles the peer (#8980). Peers that are absent stay opaque, so
9
+ * builds that do not opt into the feature keep the #8635 guarantee.
10
+ *
11
+ * @param {string} source
12
+ * @returns {string}
13
+ */
14
+ module.exports = function optionalPeerLoader (source) {
15
+ this.cacheable(false)
16
+ return rewriteOptionalPeerLoads(source, this.context)
17
+ }
@@ -63,6 +63,18 @@ class AIGuardClientError extends Error {
63
63
  }
64
64
  }
65
65
 
66
+ /**
67
+ * Resolves the AI Guard host for a given Datadog site. Sites with a single subdomain level
68
+ * (e.g. `datadoghq.com`, `ddog-gov.com`) are served from the `app.` subdomain, while regional
69
+ * sites (e.g. `us3.datadoghq.com`, `ap1.datadoghq.com`) are used as-is.
70
+ *
71
+ * @param {string} site - Datadog site (e.g. `datadoghq.com`, `us3.datadoghq.com`)
72
+ * @returns {string} The host to use for the AI Guard endpoint
73
+ */
74
+ function aiGuardHost (site) {
75
+ return site.split('.').length === 2 ? `app.${site}` : site
76
+ }
77
+
66
78
  class AIGuard extends NoopAIGuard {
67
79
  #initialized
68
80
  #tracer
@@ -94,7 +106,7 @@ class AIGuard extends NoopAIGuard {
94
106
  'DD-AI-GUARD-SOURCE': 'SDK',
95
107
  'DD-AI-GUARD-LANGUAGE': 'nodejs',
96
108
  }
97
- const endpoint = config.experimental.aiguard.endpoint || `https://app.${config.site}/api/v2/ai-guard`
109
+ const endpoint = config.experimental.aiguard.endpoint || `https://${aiGuardHost(config.site)}/api/v2/ai-guard`
98
110
  this.#evaluateUrl = `${endpoint}/evaluate`
99
111
  this.#timeout = config.experimental.aiguard.timeout
100
112
  this.#maxMessagesLength = config.experimental.aiguard.maxMessagesLength
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ const web = require('../../plugins/util/web')
4
+ const { isSchemaAttribute } = require('../reporter')
5
+ const appsecTelemetry = require('../telemetry')
6
+ const sampler = require('./sampler')
7
+
8
+ /**
9
+ * Map a sampling decision into the corresponding API Security telemetry metric.
10
+ *
11
+ * The decision is done by the sampler, here the outcome is translated to emitted metrics:
12
+ * - SAMPLE: request.schema / request.no_schema depending on WAF schema attributes
13
+ * - MISSING_ROUTE: missing_route
14
+ * - SKIP: no metric emitted
15
+ *
16
+ * @param {import('http').IncomingMessage} req
17
+ * @param {'sample' | 'missing_route' | 'skip'} samplingDecision Sampler decision
18
+ * @param {{ attributes?: Record<string, unknown> } | undefined} wafResult WAF run result
19
+ */
20
+ function reportRequest (req, samplingDecision, wafResult) {
21
+ switch (samplingDecision) {
22
+ case sampler.SamplingDecision.SAMPLE: {
23
+ const framework = getFramework(req)
24
+ if (hasSchemaAttributes(wafResult?.attributes)) {
25
+ appsecTelemetry.incrementApiSecRequestSchemaMetric(framework)
26
+ } else {
27
+ appsecTelemetry.incrementApiSecRequestNoSchemaMetric(framework)
28
+ }
29
+ break
30
+ }
31
+ case sampler.SamplingDecision.MISSING_ROUTE:
32
+ appsecTelemetry.incrementApiSecMissingRouteMetric(getFramework(req))
33
+ break
34
+ }
35
+ }
36
+
37
+ function getFramework (req) {
38
+ return web.root(req)?.context()?.getTag?.('component')
39
+ }
40
+
41
+ function hasSchemaAttributes (attributes) {
42
+ if (!attributes) return false
43
+ for (const key of Object.keys(attributes)) {
44
+ if (isSchemaAttribute(key)) return true
45
+ }
46
+ return false
47
+ }
48
+
49
+ module.exports = {
50
+ configure: sampler.configure,
51
+ disable: sampler.disable,
52
+ sampleRequest: sampler.sampleRequest,
53
+ reportRequest,
54
+ SamplingDecision: sampler.SamplingDecision,
55
+ }