dd-trace 5.99.0 → 5.100.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 (74) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/package.json +24 -5
  3. package/packages/datadog-instrumentations/src/cucumber.js +69 -5
  4. package/packages/datadog-instrumentations/src/express.js +3 -2
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/hono.js +15 -4
  7. package/packages/datadog-instrumentations/src/jest.js +89 -63
  8. package/packages/datadog-instrumentations/src/mocha/main.js +18 -22
  9. package/packages/datadog-instrumentations/src/mocha/utils.js +114 -96
  10. package/packages/datadog-instrumentations/src/mocha/worker.js +2 -2
  11. package/packages/datadog-instrumentations/src/path-to-regexp.js +44 -0
  12. package/packages/datadog-instrumentations/src/playwright.js +108 -18
  13. package/packages/datadog-instrumentations/src/router.js +53 -33
  14. package/packages/datadog-instrumentations/src/vitest.js +76 -30
  15. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -1
  16. package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
  17. package/packages/datadog-plugin-bullmq/src/consumer.js +3 -2
  18. package/packages/datadog-plugin-bullmq/src/producer.js +25 -11
  19. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +32 -9
  20. package/packages/datadog-plugin-cypress/src/support.js +22 -21
  21. package/packages/datadog-plugin-dd-trace-api/src/index.js +1 -1
  22. package/packages/datadog-plugin-graphql/src/utils.js +2 -2
  23. package/packages/datadog-plugin-grpc/src/client.js +1 -1
  24. package/packages/datadog-plugin-grpc/src/server.js +1 -1
  25. package/packages/datadog-plugin-memcached/src/index.js +1 -1
  26. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -3
  27. package/packages/datadog-plugin-playwright/src/index.js +6 -0
  28. package/packages/datadog-plugin-router/src/index.js +13 -0
  29. package/packages/dd-trace/index.js +4 -3
  30. package/packages/dd-trace/src/aiguard/sdk.js +2 -2
  31. package/packages/dd-trace/src/appsec/blocking.js +18 -6
  32. package/packages/dd-trace/src/appsec/graphql.js +1 -1
  33. package/packages/dd-trace/src/baggage.js +26 -13
  34. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +1 -1
  35. package/packages/dd-trace/src/config/generated-config-types.d.ts +45 -69
  36. package/packages/dd-trace/src/config/index.js +13 -12
  37. package/packages/dd-trace/src/config/normalize-service.js +31 -0
  38. package/packages/dd-trace/src/config/supported-configurations.json +31 -76
  39. package/packages/dd-trace/src/debugger/config.js +1 -1
  40. package/packages/dd-trace/src/dogstatsd.js +5 -8
  41. package/packages/dd-trace/src/encode/0.4.js +1 -1
  42. package/packages/dd-trace/src/encode/tags-processors.js +3 -3
  43. package/packages/dd-trace/src/exporter.js +1 -1
  44. package/packages/dd-trace/src/git_metadata_tagger.js +1 -1
  45. package/packages/dd-trace/src/heap_snapshots.js +4 -4
  46. package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
  47. package/packages/dd-trace/src/llmobs/sdk.js +21 -1
  48. package/packages/dd-trace/src/llmobs/span_processor.js +14 -1
  49. package/packages/dd-trace/src/llmobs/writers/base.js +7 -1
  50. package/packages/dd-trace/src/llmobs/writers/spans.js +1 -1
  51. package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +2 -2
  52. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -8
  53. package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -5
  54. package/packages/dd-trace/src/opentelemetry/metrics/index.js +6 -6
  55. package/packages/dd-trace/src/opentelemetry/span-helpers.js +170 -0
  56. package/packages/dd-trace/src/opentelemetry/span.js +14 -42
  57. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +1 -1
  58. package/packages/dd-trace/src/opentelemetry/tracer.js +11 -36
  59. package/packages/dd-trace/src/opentracing/propagation/text_map.js +44 -23
  60. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +42 -12
  61. package/packages/dd-trace/src/opentracing/span.js +4 -3
  62. package/packages/dd-trace/src/plugin_manager.js +6 -6
  63. package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
  64. package/packages/dd-trace/src/plugins/util/ci.js +119 -32
  65. package/packages/dd-trace/src/plugins/util/test.js +295 -29
  66. package/packages/dd-trace/src/profiling/ssi-heuristics.js +2 -2
  67. package/packages/dd-trace/src/propagation-hash/index.js +1 -1
  68. package/packages/dd-trace/src/proxy.js +9 -9
  69. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  70. package/packages/dd-trace/src/span_processor.js +1 -1
  71. package/packages/dd-trace/src/telemetry/telemetry.js +7 -5
  72. package/packages/dd-trace/src/tracer_metadata.js +1 -1
  73. package/vendor/dist/path-to-regexp/LICENSE +0 -21
  74. package/vendor/dist/path-to-regexp/index.js +0 -1
@@ -1,8 +1,22 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../dd-trace/src/log')
3
4
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
4
5
  const { DsmPathwayCodec, getMessageSize } = require('../../dd-trace/src/datastreams')
5
6
 
7
+ // Customer-controlled metadata may be malformed JSON. Returning a fresh `{}`
8
+ // on parse failure keeps the publish path alive instead of throwing into
9
+ // `Queue.add` / `Queue.addBulk`.
10
+ function parseTelemetryMetadata (raw) {
11
+ if (!raw) return {}
12
+ try {
13
+ return JSON.parse(raw)
14
+ } catch (error) {
15
+ log.warn('bullmq: ignoring malformed telemetry.metadata: %s', error.message)
16
+ return {}
17
+ }
18
+ }
19
+
6
20
  class BaseBullmqProducerPlugin extends ProducerPlugin {
7
21
  static id = 'bullmq'
8
22
 
@@ -45,9 +59,9 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
45
59
  _injectIntoOpts (span, opts) {
46
60
  const carrier = {}
47
61
  this.tracer.inject(span, 'text_map', carrier)
48
- const existing = opts.telemetry?.metadata ? JSON.parse(opts.telemetry.metadata) : {}
49
- existing._datadog = carrier
50
- opts.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
62
+ const metadata = parseTelemetryMetadata(opts.telemetry?.metadata)
63
+ metadata._datadog = carrier
64
+ opts.telemetry = { metadata: JSON.stringify(metadata), omitContext: true }
51
65
  }
52
66
 
53
67
  setProducerCheckpoint (span, ctx) {
@@ -56,10 +70,10 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
56
70
  const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
57
71
 
58
72
  if (optsTarget && typeof optsTarget === 'object') {
59
- const existing = optsTarget.telemetry?.metadata ? JSON.parse(optsTarget.telemetry.metadata) : {}
60
- DsmPathwayCodec.encode(dataStreamsContext, existing._datadog || existing)
61
- if (!existing._datadog) existing._datadog = {}
62
- optsTarget.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
73
+ const metadata = parseTelemetryMetadata(optsTarget.telemetry?.metadata)
74
+ DsmPathwayCodec.encode(dataStreamsContext, metadata._datadog || metadata)
75
+ if (!metadata._datadog) metadata._datadog = {}
76
+ optsTarget.telemetry = { metadata: JSON.stringify(metadata), omitContext: true }
63
77
  }
64
78
  }
65
79
 
@@ -161,10 +175,10 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
161
175
  const payloadSize = getMessageSize(job.data)
162
176
  const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
163
177
  job.opts = job.opts || {}
164
- const existing = job.opts.telemetry?.metadata ? JSON.parse(job.opts.telemetry.metadata) : {}
165
- DsmPathwayCodec.encode(dataStreamsContext, existing._datadog || existing)
166
- if (!existing._datadog) existing._datadog = {}
167
- job.opts.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
178
+ const metadata = parseTelemetryMetadata(job.opts.telemetry?.metadata)
179
+ DsmPathwayCodec.encode(dataStreamsContext, metadata._datadog || metadata)
180
+ if (!metadata._datadog) metadata._datadog = {}
181
+ job.opts.telemetry = { metadata: JSON.stringify(metadata), omitContext: true }
168
182
  }
169
183
  }
170
184
  }
@@ -55,7 +55,9 @@ const {
55
55
  TEST_IS_MODIFIED,
56
56
  TEST_HAS_DYNAMIC_NAME,
57
57
  DYNAMIC_NAME_RE,
58
- logDynamicNamesWarning,
58
+ recordAttemptToFixExecution,
59
+ logAttemptToFixTestExecution,
60
+ logTestOptimizationSummary,
59
61
  getPullRequestBaseBranch,
60
62
  TEST_FINAL_STATUS,
61
63
  } = require('../../dd-trace/src/plugins/util/test')
@@ -278,9 +280,8 @@ function getFinalStatus ({
278
280
  isQuarantined,
279
281
  isDisabled,
280
282
  }) {
281
- // If the test is quarantined or disabled, regardless of its actual execution result or active retry features,
282
- // the final status of its last execution should be reported as 'skip'.
283
- if (isQuarantined || isDisabled || status === 'skip') {
283
+ // If the test is quarantined or disabled, its final status is skip unless attempt-to-fix takes precedence.
284
+ if (status === 'skip' || (retryKind !== FINAL_STATUS_RETRY_KIND.atf && (isQuarantined || isDisabled))) {
284
285
  return 'skip'
285
286
  }
286
287
 
@@ -322,6 +323,8 @@ class CypressPlugin {
322
323
  isImpactedTestsEnabled = false
323
324
  modifiedFiles = []
324
325
  newTestsWithDynamicNames = new Set()
326
+ attemptToFixExecutions = new Map()
327
+ loggedAttemptToFixTests = new Set()
325
328
 
326
329
  constructor () {
327
330
  const {
@@ -394,6 +397,8 @@ class CypressPlugin {
394
397
  this.testManagementTests = undefined
395
398
  this.isImpactedTestsEnabled = false
396
399
  this.modifiedFiles = []
400
+ this.attemptToFixExecutions = new Map()
401
+ this.loggedAttemptToFixTests = new Set()
397
402
  this.activeTestSpan = null
398
403
  this.testSuiteSpan = null
399
404
  this.testModuleSpan = null
@@ -800,7 +805,10 @@ class CypressPlugin {
800
805
  this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
801
806
  }
802
807
 
803
- logDynamicNamesWarning(this.newTestsWithDynamicNames)
808
+ logTestOptimizationSummary({
809
+ attemptToFixExecutions: this.attemptToFixExecutions,
810
+ newTestsWithDynamicNames: this.newTestsWithDynamicNames,
811
+ })
804
812
 
805
813
  this.testModuleSpan.finish()
806
814
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
@@ -939,10 +947,10 @@ class CypressPlugin {
939
947
  if (cypressTest.displayError) {
940
948
  latestError = new Error(cypressTest.displayError)
941
949
  }
942
- // Update test status - but NOT for quarantined tests where we intentionally
950
+ // Update test status - but NOT for non-ATF quarantined tests where we intentionally
943
951
  // report 'fail' to Datadog even though Cypress sees it as 'pass'
944
952
  const isQuarantinedTest = finishedTest.testSpan?.context()?._tags?.[TEST_MANAGEMENT_IS_QUARANTINED] === 'true'
945
- if (cypressTestStatus !== finishedTest.testStatus && !isQuarantinedTest) {
953
+ if (cypressTestStatus !== finishedTest.testStatus && (!isQuarantinedTest || finishedTest.isAttemptToFix)) {
946
954
  finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
947
955
  finishedTest.testSpan.setTag('error', latestError)
948
956
  }
@@ -1050,6 +1058,10 @@ class CypressPlugin {
1050
1058
  return { shouldSkip: true }
1051
1059
  }
1052
1060
 
1061
+ if (isAttemptToFix) {
1062
+ logAttemptToFixTestExecution(testSuite, testName, this.loggedAttemptToFixTests)
1063
+ }
1064
+
1053
1065
  // For disabled tests (not attemptToFix), skip them
1054
1066
  if (!isAttemptToFix && isDisabled) {
1055
1067
  return { shouldSkip: true }
@@ -1090,6 +1102,7 @@ class CypressPlugin {
1090
1102
  isAttemptToFix,
1091
1103
  isModified,
1092
1104
  isQuarantined: isQuarantinedFromSupport,
1105
+ isDisabled: isDisabledFromSupport,
1093
1106
  } = test
1094
1107
  if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) {
1095
1108
  const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
@@ -1119,6 +1132,7 @@ class CypressPlugin {
1119
1132
  this.testStatuses[testName] = [testStatus]
1120
1133
  }
1121
1134
  const testStatuses = this.testStatuses[testName]
1135
+ const activeSpanTags = this.activeTestSpan.context()._tags
1122
1136
 
1123
1137
  if (error) {
1124
1138
  this.activeTestSpan.setTag('error', error)
@@ -1194,6 +1208,13 @@ class CypressPlugin {
1194
1208
  this.activeTestSpan.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true')
1195
1209
  }
1196
1210
  }
1211
+ recordAttemptToFixExecution(this.attemptToFixExecutions, {
1212
+ testSuite,
1213
+ testName,
1214
+ status: testStatus,
1215
+ isDisabled: activeSpanTags[TEST_MANAGEMENT_IS_DISABLED] === 'true',
1216
+ isQuarantined: activeSpanTags[TEST_MANAGEMENT_IS_QUARANTINED] === 'true',
1217
+ })
1197
1218
  }
1198
1219
  // ATR: set TEST_HAS_FAILED_ALL_RETRIES when all auto test retries were exhausted and every attempt failed
1199
1220
  if (this.isFlakyTestRetriesEnabled && !isAttemptToFix && !isEfdRetry &&
@@ -1207,6 +1228,9 @@ class CypressPlugin {
1207
1228
  if (isQuarantinedFromSupport) {
1208
1229
  this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
1209
1230
  }
1231
+ if (isDisabledFromSupport) {
1232
+ this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
1233
+ }
1210
1234
 
1211
1235
  const finishedTest = {
1212
1236
  testName,
@@ -1222,13 +1246,12 @@ class CypressPlugin {
1222
1246
  this.finishedTestsByFile[testSuite] = [finishedTest]
1223
1247
  }
1224
1248
  // test spans are finished at after:spec
1225
- const activeSpanTags = this.activeTestSpan.context()._tags
1226
1249
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
1227
1250
  hasCodeOwners: !!activeSpanTags[TEST_CODE_OWNERS],
1228
1251
  isNew,
1229
1252
  isRum: isRUMActive,
1230
1253
  browserDriver: 'cypress',
1231
- isQuarantined: isQuarantinedFromSupport,
1254
+ isQuarantined: activeSpanTags[TEST_MANAGEMENT_IS_QUARANTINED] === 'true',
1232
1255
  isModified,
1233
1256
  isDisabled: activeSpanTags[TEST_MANAGEMENT_IS_DISABLED] === 'true',
1234
1257
  })
@@ -15,8 +15,8 @@ let isModifiedTest = false
15
15
  let isTestIsolationEnabled = false
16
16
  // Array of test names that have been retried and the reason
17
17
  const retryReasonsByTestName = new Map()
18
- // Track quarantined test errors - we catch them in Cypress.on('fail') but need to report to Datadog
19
- const quarantinedTestErrors = new Map()
18
+ // Track test errors suppressed by test management so we can still report them to Datadog.
19
+ const suppressedTestFailures = new Map()
20
20
 
21
21
  // Track the most recently loaded window in the AUT. Updated via the 'window:load'
22
22
  // event so we always get the real app window (after cy.visit()), not the
@@ -61,13 +61,12 @@ Cypress.on('fail', (err, runnable) => {
61
61
  }
62
62
 
63
63
  const testName = runnable.fullTitle()
64
- const { isQuarantined, isDisabled } = getTestProperties(testName)
64
+ const { isAttemptToFix, isQuarantined, isDisabled } = getTestProperties(testName)
65
65
 
66
66
  // Suppress failures for quarantined or disabled tests so they don't affect the exit code.
67
- // This applies regardless of attempt-to-fix status: per spec, quarantined/disabled test
68
- // results are always ignored.
69
- if (isQuarantined || isDisabled) {
70
- quarantinedTestErrors.set(testName, err)
67
+ // Attempt-to-fix ignores quarantine/disabled suppression and keeps the normal framework result.
68
+ if (!isAttemptToFix && (isQuarantined || isDisabled)) {
69
+ suppressedTestFailures.set(testName, { error: err, isQuarantined, isDisabled })
71
70
  return
72
71
  }
73
72
 
@@ -245,13 +244,14 @@ afterEach(function () {
245
244
  const currentTest = Cypress.mocha.getRunner().suite.ctx.currentTest
246
245
  const testName = currentTest.fullTitle()
247
246
 
248
- // Check if this was a quarantined test that we suppressed the failure for
249
- const quarantinedError = quarantinedTestErrors.get(testName)
250
- const isQuarantinedTestThatFailed = !!quarantinedError
247
+ // Check if this was a test management test that we suppressed the failure for.
248
+ const suppressedTestFailure = suppressedTestFailures.get(testName)
249
+ const suppressedError = suppressedTestFailure && suppressedTestFailure.error
250
+ const isTestManagementTestThatFailed = !!suppressedError
251
251
 
252
- // For quarantined tests, convert Error to a serializable format for cy.task
253
- const errorToReport = isQuarantinedTestThatFailed
254
- ? { message: quarantinedError.message, stack: quarantinedError.stack }
252
+ // For suppressed test management tests, convert Error to a serializable format for cy.task.
253
+ const errorToReport = isTestManagementTestThatFailed
254
+ ? { message: suppressedError.message, stack: suppressedError.stack }
255
255
  : currentTest.err
256
256
 
257
257
  const testInfo = {
@@ -259,16 +259,17 @@ afterEach(function () {
259
259
  testItTitle: currentTest.title,
260
260
  testSuite: Cypress.mocha.getRootSuite().file,
261
261
  testSuiteAbsolutePath: Cypress.spec && Cypress.spec.absolute,
262
- // For quarantined tests, report the actual state (failed) to Datadog, not what Cypress thinks (passed)
263
- state: isQuarantinedTestThatFailed ? 'failed' : currentTest.state,
264
- // For quarantined tests, include the actual error that was suppressed
262
+ // Report the actual failed state to Datadog, not the pass state Cypress sees after suppression.
263
+ state: isTestManagementTestThatFailed ? 'failed' : currentTest.state,
264
+ // Include the actual error that was suppressed.
265
265
  error: errorToReport,
266
266
  isNew: currentTest._ddIsNew,
267
267
  isEfdRetry: currentTest._ddIsEfdRetry,
268
268
  isAttemptToFix: currentTest._ddIsAttemptToFix,
269
269
  isModified: currentTest._ddIsModified,
270
- // Mark quarantined tests that failed so the plugin knows to tag them appropriately
271
- isQuarantined: isQuarantinedTestThatFailed,
270
+ // Mark suppressed tests so the plugin can tag them with the correct test management reason.
271
+ isQuarantined: isTestManagementTestThatFailed && suppressedTestFailure.isQuarantined,
272
+ isDisabled: isTestManagementTestThatFailed && suppressedTestFailure.isDisabled,
272
273
  }
273
274
  try {
274
275
  const invocationDetails = Cypress.mocha.getRunner().currentRunnable.invocationDetails
@@ -292,9 +293,9 @@ afterEach(function () {
292
293
  // ignore error and continue
293
294
  }
294
295
 
295
- // Clean up the quarantined error tracking
296
- if (isQuarantinedTestThatFailed) {
297
- quarantinedTestErrors.delete(testName)
296
+ // Clean up the suppressed error tracking.
297
+ if (isTestManagementTestThatFailed) {
298
+ suppressedTestFailures.delete(testName)
298
299
  }
299
300
 
300
301
  cy.task('dd:afterEach', { test: testInfo, coverage })
@@ -14,7 +14,7 @@ module.exports = class DdTraceApiPlugin extends Plugin {
14
14
  super(...args)
15
15
 
16
16
  const tracer = this._tracer
17
- const injectionEnabledTag = `injection_enabled:${this._tracerConfig.injectionEnabled ? 'yes' : 'no'}`
17
+ const injectionEnabledTag = `injection_enabled:${this._tracerConfig.DD_INJECTION_ENABLED ? 'yes' : 'no'}`
18
18
 
19
19
  this.addSub('datadog-api:v1:tracerinit', ({ proxy }) => {
20
20
  const proxyVal = proxy()
@@ -26,8 +26,8 @@ function extractErrorIntoSpanEvent (config, span, exc) {
26
26
  attributes.message = exc.message
27
27
  }
28
28
 
29
- if (config.graphqlErrorExtensions) {
30
- for (const ext of config.graphqlErrorExtensions) {
29
+ if (config.DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) {
30
+ for (const ext of config.DD_TRACE_GRAPHQL_ERROR_EXTENSIONS) {
31
31
  if (exc.extensions?.[ext]) {
32
32
  const value = exc.extensions[ext]
33
33
 
@@ -64,7 +64,7 @@ class GrpcClientPlugin extends ClientPlugin {
64
64
 
65
65
  error ({ span = this.activeSpan, error }) {
66
66
  this.addCode(span, error.code)
67
- if (error.code && !this._tracerConfig.grpc.client.error.statuses.includes(error.code)) {
67
+ if (error.code && !this._tracerConfig.DD_GRPC_CLIENT_ERROR_STATUSES.includes(error.code)) {
68
68
  return
69
69
  }
70
70
  this.addError(error, span)
@@ -70,7 +70,7 @@ class GrpcServerPlugin extends ServerPlugin {
70
70
  if (!span) return
71
71
 
72
72
  this.addCode(span, error.code)
73
- if (error.code && !this._tracerConfig.grpc.server.error.statuses.includes(error.code)) {
73
+ if (error.code && !this._tracerConfig.DD_GRPC_SERVER_ERROR_STATUSES.includes(error.code)) {
74
74
  return
75
75
  }
76
76
  this.addError(error)
@@ -16,7 +16,7 @@ class MemcachedPlugin extends CachePlugin {
16
16
  [CLIENT_PORT_KEY]: address[1],
17
17
  }
18
18
 
19
- if (this.config.memcachedCommandEnabled) {
19
+ if (this.config.DD_TRACE_MEMCACHED_COMMAND_ENABLED) {
20
20
  meta['memcached.command'] = query.command
21
21
  }
22
22
 
@@ -160,10 +160,9 @@ function limitDepth (input) {
160
160
  input, output, depth,
161
161
  } = queue.pop()
162
162
  const nextDepth = depth + 1
163
- for (const key in input) {
164
- if (typeof input[key] === 'function') continue
165
-
163
+ for (const key of Object.keys(input)) {
166
164
  let child = input[key]
165
+ if (typeof child === 'function') continue
167
166
 
168
167
  if (isBSON(child)) {
169
168
  child = typeof child.toJSON === 'function' ? child.toJSON() : '?'
@@ -39,6 +39,7 @@ const {
39
39
  TEST_SUITE,
40
40
  TEST_HAS_DYNAMIC_NAME,
41
41
  DYNAMIC_NAME_RE,
42
+ TEST_FINAL_STATUS,
42
43
  } = require('../../dd-trace/src/plugins/util/test')
43
44
  const { RESOURCE_NAME } = require('../../../ext/tags')
44
45
  const { COMPONENT } = require('../../dd-trace/src/constants')
@@ -319,6 +320,7 @@ class PlaywrightPlugin extends CiPlugin {
319
320
  hasFailedAttemptToFixRetries,
320
321
  isAtrRetry,
321
322
  isModified,
323
+ finalStatus,
322
324
  onDone,
323
325
  }) => {
324
326
  if (!span) return
@@ -379,6 +381,9 @@ class PlaywrightPlugin extends CiPlugin {
379
381
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
380
382
  }
381
383
  }
384
+ if (finalStatus) {
385
+ span.setTag(TEST_FINAL_STATUS, finalStatus)
386
+ }
382
387
  for (const step of steps) {
383
388
  const stepStartTime = step.startTime.getTime()
384
389
  const stepSpan = this.tracer.startSpan('playwright.step', {
@@ -451,6 +456,7 @@ class PlaywrightPlugin extends CiPlugin {
451
456
  )
452
457
 
453
458
  span.setTag(TEST_STATUS, 'skip')
459
+ span.setTag(TEST_FINAL_STATUS, 'skip')
454
460
 
455
461
  if (isNew) {
456
462
  span.setTag(TEST_IS_NEW, 'true')
@@ -157,6 +157,13 @@ class RouterPlugin extends WebPlugin {
157
157
  }
158
158
 
159
159
  function isMoreSpecificThan (routeA, routeB) {
160
+ // Concrete paths beat catch-all wildcards (`/*splat`, `/api/*`) on the same
161
+ // request so that `/foo/bar` wins over `/foo/*splat` regardless of length.
162
+ if (routeA && routeB) {
163
+ const aWild = hasWildcard(routeA)
164
+ const bWild = hasWildcard(routeB)
165
+ if (aWild !== bWild) return !aWild
166
+ }
160
167
  if (!routeIsRegex(routeA) && routeIsRegex(routeB)) {
161
168
  return true
162
169
  }
@@ -167,4 +174,10 @@ function routeIsRegex (route) {
167
174
  return route.includes('(/')
168
175
  }
169
176
 
177
+ function hasWildcard (route) {
178
+ // RegExp routes are encoded as `(/.../)` and may legitimately contain `*`,
179
+ // so only treat plain string patterns as wildcards.
180
+ return !routeIsRegex(route) && route.includes('*')
181
+ }
182
+
170
183
  module.exports = RouterPlugin
@@ -30,9 +30,10 @@ if (!global._ddtrace) {
30
30
  configurable: true,
31
31
  writable: true,
32
32
  })
33
-
34
- global._ddtrace.default = global._ddtrace
35
- global._ddtrace.tracer = global._ddtrace
36
33
  }
37
34
 
38
35
  module.exports = global._ddtrace
36
+ // Static aliases so cjs-module-lexer surfaces them as ESM named exports
37
+ // (`import { tracer } from 'dd-trace'`).
38
+ module.exports.tracer = global._ddtrace
39
+ module.exports.default = global._ddtrace
@@ -70,7 +70,7 @@ class AIGuard extends NoopAIGuard {
70
70
  constructor (tracer, config) {
71
71
  super()
72
72
 
73
- if (!config.apiKey || !config.appKey) {
73
+ if (!config.apiKey || !config.DD_APP_KEY) {
74
74
  log.error('AIGuard: missing api and/or app keys, use env DD_API_KEY and DD_APP_KEY')
75
75
  this.#initialized = false
76
76
  return
@@ -78,7 +78,7 @@ class AIGuard extends NoopAIGuard {
78
78
  this.#tracer = tracer
79
79
  this.#headers = {
80
80
  'DD-API-KEY': config.apiKey,
81
- 'DD-APPLICATION-KEY': config.appKey,
81
+ 'DD-APPLICATION-KEY': config.DD_APP_KEY,
82
82
  'DD-AI-GUARD-VERSION': tracerVersion,
83
83
  'DD-AI-GUARD-SOURCE': 'SDK',
84
84
  'DD-AI-GUARD-LANGUAGE': 'nodejs',
@@ -1,10 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ const { LRUCache } = require('../../../../vendor/dist/lru-cache')
3
4
  const log = require('../log')
5
+ const web = require('../plugins/util/web')
4
6
  const blockedTemplates = require('./blocked_templates')
5
7
  const { updateBlockFailureMetric } = require('./telemetry')
6
8
 
7
- const detectedSpecificEndpoints = {}
9
+ // Bounded by the LRU as defense-in-depth: getSpecificKey already keys on the
10
+ // resolved route (or the path with the query string stripped) so cardinality
11
+ // follows the routing table, not the URL space.
12
+ const SPECIFIC_ENDPOINT_CACHE_MAX = 16_384
13
+ const detectedSpecificEndpoints = new LRUCache({ max: SPECIFIC_ENDPOINT_CACHE_MAX })
8
14
 
9
15
  const templateKeyword = '[security_response_id]'
10
16
 
@@ -38,12 +44,18 @@ const specificBlockingTypes = {
38
44
  GRAPHQL: 'graphqlJson',
39
45
  }
40
46
 
41
- function getSpecificKey (method, url) {
42
- return `${method}+${url}`
47
+ function getSpecificKey (req) {
48
+ const route = web.getContext(req)?.paths?.join('')
49
+ if (route) return `${req.method}+${route}`
50
+
51
+ // Strip the query string so unique parameters do not balloon the cache.
52
+ const url = req.originalUrl || req.url || ''
53
+ const queryStart = url.indexOf('?')
54
+ return `${req.method}+${queryStart === -1 ? url : url.slice(0, queryStart)}`
43
55
  }
44
56
 
45
- function addSpecificEndpoint (method, url, type) {
46
- detectedSpecificEndpoints[getSpecificKey(method, url)] = type
57
+ function addSpecificEndpoint (req, type) {
58
+ detectedSpecificEndpoints.set(getSpecificKey(req), type)
47
59
  }
48
60
 
49
61
  function getBlockWithRedirectData (actionParameters) {
@@ -65,7 +77,7 @@ function getBlockWithContentData (req, specificType, actionParameters) {
65
77
  let type
66
78
  let body
67
79
 
68
- const specificBlockingType = specificType || detectedSpecificEndpoints[getSpecificKey(req.method, req.url)]
80
+ const specificBlockingType = specificType || detectedSpecificEndpoints.get(getSpecificKey(req))
69
81
  if (specificBlockingType) {
70
82
  const specificBlockingContent = getTemplate(specificBlockingType, actionParameters)
71
83
  type = specificBlockingContent?.type
@@ -78,7 +78,7 @@ function enterInApolloRequest () {
78
78
  // Set isInGraphqlRequest=true since this function only runs for GraphQL requests
79
79
  // This works for both Apollo v4 (middleware) and v5 (HTTP server) contexts
80
80
  requestData.isInGraphqlRequest = true
81
- addSpecificEndpoint(req.method, req.originalUrl || req.url, specificBlockingTypes.GRAPHQL)
81
+ addSpecificEndpoint(req, specificBlockingTypes.GRAPHQL)
82
82
  }
83
83
  }
84
84
 
@@ -3,10 +3,9 @@
3
3
  const { storage } = require('../../datadog-core')
4
4
 
5
5
  /**
6
- * Spec (API semantics):
7
- * - OpenTelemetry Baggage API: https://opentelemetry.io/docs/specs/otel/baggage/api/
6
+ * In-process baggage map stored in async local storage. Frozen on every write.
8
7
  *
9
- * In-process baggage is a string->string map stored in async local storage.
8
+ * @see https://opentelemetry.io/docs/specs/otel/baggage/api/
10
9
  * @typedef {import('../../datadog-core/src/storage').Store<string>} BaggageStore
11
10
  */
12
11
 
@@ -18,6 +17,8 @@ const baggageStorage =
18
17
  /** @type {unknown} */ (storage('baggage'))
19
18
  )
20
19
 
20
+ const EMPTY_STORE = Object.freeze(/** @type {BaggageStore} */ ({}))
21
+
21
22
  // TODO: Implement metadata https://opentelemetry.io/docs/specs/otel/baggage/api/#set-value
22
23
  /**
23
24
  * @param {string} key
@@ -25,12 +26,11 @@ const baggageStorage =
25
26
  * @param {object} [metadata] Not used yet
26
27
  */
27
28
  function setBaggageItem (key, value, metadata) {
29
+ const store = baggageStorage.getStore()
28
30
  if (typeof key !== 'string' || typeof value !== 'string' || key === '') {
29
- return baggageStorage.getStore() ?? {}
31
+ return store ?? EMPTY_STORE
30
32
  }
31
-
32
- const store = baggageStorage.getStore()
33
- const newStore = { ...store, [key]: value }
33
+ const newStore = Object.freeze({ ...store, [key]: value })
34
34
  baggageStorage.enterWith(newStore)
35
35
  return newStore
36
36
  }
@@ -44,27 +44,40 @@ function getBaggageItem (key) {
44
44
  }
45
45
 
46
46
  function getAllBaggageItems () {
47
- return baggageStorage.getStore() ?? {}
47
+ return baggageStorage.getStore() ?? EMPTY_STORE
48
48
  }
49
49
 
50
50
  /**
51
- * @param {string} keyToRemove
51
+ * @param {string} keyToRemove No-op for non-string or empty keys.
52
52
  */
53
53
  function removeBaggageItem (keyToRemove) {
54
- const store = baggageStorage.getStore() ?? {}
54
+ const store = baggageStorage.getStore() ?? EMPTY_STORE
55
+ if (typeof keyToRemove !== 'string' || keyToRemove === '') {
56
+ return store
57
+ }
55
58
  const { [keyToRemove]: _, ...newBaggage } = store
59
+ Object.freeze(newBaggage)
56
60
  baggageStorage.enterWith(newBaggage)
57
61
  return newBaggage
58
62
  }
59
63
 
60
64
  function removeAllBaggageItems () {
61
- const newContext = /** @type {BaggageStore} */ ({})
62
- baggageStorage.enterWith(newContext)
63
- return newContext
65
+ baggageStorage.enterWith(EMPTY_STORE)
66
+ return EMPTY_STORE
67
+ }
68
+
69
+ /**
70
+ * @param {BaggageStore} items Frozen in place; do not mutate after.
71
+ */
72
+ function setAllBaggageItems (items) {
73
+ Object.freeze(items)
74
+ baggageStorage.enterWith(items)
75
+ return items
64
76
  }
65
77
 
66
78
  module.exports = {
67
79
  setBaggageItem,
80
+ setAllBaggageItems,
68
81
  getBaggageItem,
69
82
  getAllBaggageItems,
70
83
  removeBaggageItem,
@@ -234,7 +234,7 @@ class CiVisibilityExporter extends BufferingExporter {
234
234
  testManagementAttemptToFixRetries ?? this._config.testManagementAttemptToFixRetries,
235
235
  isImpactedTestsEnabled: isImpactedTestsEnabled && this._config.isImpactedTestsEnabled,
236
236
  isCoverageReportUploadEnabled,
237
- isKeepingCoverageConfiguration: this._config.isKeepingCoverageConfiguration,
237
+ DD_TEST_TIA_KEEP_COV_CONFIG: this._config.DD_TEST_TIA_KEEP_COV_CONFIG,
238
238
  }
239
239
  }
240
240