dd-trace 5.86.0 → 5.87.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 (38) hide show
  1. package/index.d.ts +18 -3
  2. package/package.json +1 -1
  3. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  4. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  5. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  6. package/packages/datadog-instrumentations/src/jest.js +104 -4
  7. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  8. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  9. package/packages/datadog-instrumentations/src/playwright.js +8 -0
  10. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  11. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  12. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  13. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
  14. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  15. package/packages/dd-trace/src/appsec/channels.js +5 -1
  16. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  17. package/packages/dd-trace/src/appsec/index.js +103 -0
  18. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  19. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  20. package/packages/dd-trace/src/config/defaults.js +2 -0
  21. package/packages/dd-trace/src/config/index.js +6 -0
  22. package/packages/dd-trace/src/config/supported-configurations.json +2 -0
  23. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  24. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  25. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  26. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  27. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  28. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  29. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  30. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  31. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  32. package/packages/dd-trace/src/plugins/database.js +42 -43
  33. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  34. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  35. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  36. package/packages/dd-trace/src/plugins/util/web.js +8 -7
  37. package/packages/dd-trace/src/startup-log.js +2 -2
  38. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
package/index.d.ts CHANGED
@@ -3243,13 +3243,13 @@ declare namespace tracer {
3243
3243
  /**
3244
3244
  * The type of evaluation metric, one of 'categorical', 'score', or 'boolean'
3245
3245
  */
3246
- metricType: 'categorical' | 'score' | 'boolean',
3246
+ metricType: 'categorical' | 'score' | 'boolean' | 'json',
3247
3247
 
3248
3248
  /**
3249
3249
  * The value of the evaluation metric.
3250
- * Must be string for 'categorical' metrics, number for 'score' metrics, and boolean for 'boolean' metrics.
3250
+ * Must be string for 'categorical' metrics, number for 'score' metrics, boolean for 'boolean' metrics and a JSON object for 'json' metrics.
3251
3251
  */
3252
- value: string | number | boolean,
3252
+ value: string | number | boolean | { [key: string]: any },
3253
3253
 
3254
3254
  /**
3255
3255
  * An object of string key-value pairs to tag the evaluation metric with.
@@ -3265,6 +3265,21 @@ declare namespace tracer {
3265
3265
  * The timestamp in milliseconds when the evaluation metric result was generated.
3266
3266
  */
3267
3267
  timestampMs?: number
3268
+
3269
+ /**
3270
+ * Reasoning for the evaluation result.
3271
+ */
3272
+ reasoning?: string,
3273
+
3274
+ /**
3275
+ * Whether the evaluation passed or failed. Valid values are pass and fail.
3276
+ */
3277
+ assessment?: 'pass' | 'fail',
3278
+
3279
+ /**
3280
+ * Arbitrary JSON data associated with the evaluation.
3281
+ */
3282
+ metadata?: { [key: string]: any }
3268
3283
  }
3269
3284
 
3270
3285
  interface Document {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.86.0",
3
+ "version": "5.87.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -353,6 +353,20 @@ function wrapRun (pl, isLatestVersion, version) {
353
353
  isEfdRetry = numRetries > 0
354
354
  }
355
355
 
356
+ // Check if all EFD retries failed
357
+ if (isEfdRetry && (isNew || isModified)) {
358
+ const statuses = lastStatusByPickleId.get(this.pickle.id)
359
+ if (statuses.length === earlyFlakeDetectionNumRetries + 1) {
360
+ const { fail } = statuses.reduce((acc, status) => {
361
+ acc[status]++
362
+ return acc
363
+ }, { pass: 0, fail: 0 })
364
+ if (fail === earlyFlakeDetectionNumRetries + 1) {
365
+ hasFailedAllRetries = true
366
+ }
367
+ }
368
+ }
369
+
356
370
  const attemptCtx = numAttemptToCtx.get(numAttempt)
357
371
 
358
372
  const error = getErrorFromCucumberResult(result)
@@ -136,6 +136,7 @@ module.exports = {
136
136
  'selenium-webdriver': () => require('../selenium'),
137
137
  sequelize: () => require('../sequelize'),
138
138
  sharedb: () => require('../sharedb'),
139
+ stripe: () => require('../stripe'),
139
140
  tedious: () => require('../tedious'),
140
141
  tinypool: { esmFirst: true, fn: () => require('../vitest') },
141
142
  undici: () => require('../undici'),
@@ -14,6 +14,7 @@ const finishChannel = channel('apm:http:client:request:finish')
14
14
  const endChannel = channel('apm:http:client:request:end')
15
15
  const asyncStartChannel = channel('apm:http:client:request:asyncStart')
16
16
  const errorChannel = channel('apm:http:client:request:error')
17
+ const responseFinishChannel = channel('apm:http:client:response:finish')
17
18
 
18
19
  const names = ['http', 'https', 'node:http', 'node:https']
19
20
 
@@ -39,6 +40,112 @@ function normalizeCallback (inputOptions, callback, inputURL) {
39
40
  return typeof inputOptions === 'function' ? [inputOptions, inputURL || {}] : [callback, inputOptions]
40
41
  }
41
42
 
43
+ /**
44
+ * Wires the downstream response so we can observe when the customer consumes
45
+ * the body and when the stream finishes
46
+ *
47
+ * @param {object} ctx - Instrumentation context
48
+ * @param {import('http').IncomingMessage} res - The downstream response object.
49
+ * @returns {{ finalizeIfNeeded: () => void }|null} Cleanup helper used for drain.
50
+ */
51
+ function setupResponseInstrumentation (ctx, res) {
52
+ const shouldInstrumentFinish = responseFinishChannel.hasSubscribers
53
+
54
+ if (!shouldInstrumentFinish) {
55
+ return null
56
+ }
57
+
58
+ let bodyConsumed = false
59
+ let finishCalled = false
60
+ let originalRead = null
61
+ let dataListenerAdded = false
62
+ let dataReadStarted = false
63
+
64
+ const { shouldCollectBody } = ctx
65
+ const bodyChunks = shouldCollectBody ? [] : null
66
+
67
+ const collectChunk = chunk => {
68
+ if (!shouldCollectBody || !chunk) return
69
+
70
+ if (typeof chunk === 'string') {
71
+ bodyChunks.push(chunk)
72
+ } else if (Buffer.isBuffer(chunk)) {
73
+ bodyChunks.push(chunk)
74
+ } else {
75
+ // Handle Uint8Array or other array-like types
76
+ bodyChunks.push(Buffer.from(chunk))
77
+ }
78
+ }
79
+
80
+ // Listen for body consumption
81
+ const onNewListener = (eventName) => {
82
+ if (eventName === 'data' || eventName === 'readable') {
83
+ bodyConsumed = true
84
+
85
+ // For 'data' events, add our own listener to collect chunks
86
+ if (eventName === 'data' && !dataListenerAdded && !dataReadStarted) {
87
+ dataListenerAdded = true
88
+ res.on('data', collectChunk)
89
+ }
90
+
91
+ // For 'readable' events, wrap the read() method
92
+ if (eventName === 'readable' && !originalRead && !dataListenerAdded && typeof res.read === 'function') {
93
+ originalRead = res.read
94
+ res.read = function () {
95
+ const chunk = originalRead.apply(this, arguments)
96
+ if (!dataListenerAdded) {
97
+ dataReadStarted = true
98
+ collectChunk(chunk)
99
+ }
100
+ return chunk
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ res.on('newListener', onNewListener)
107
+
108
+ // Cleanup function to restore original behavior
109
+ const cleanup = () => {
110
+ res.off('newListener', onNewListener)
111
+ res.off('data', collectChunk)
112
+
113
+ if (originalRead) {
114
+ res.read = originalRead
115
+ originalRead = null
116
+ }
117
+ }
118
+
119
+ const notifyFinish = () => {
120
+ if (finishCalled) return
121
+ finishCalled = true
122
+
123
+ // Combine collected chunks into a single body
124
+ let body = null
125
+ if (bodyChunks?.length) {
126
+ const firstChunk = bodyChunks[0]
127
+ body = typeof firstChunk === 'string'
128
+ ? bodyChunks.join('')
129
+ : Buffer.concat(bodyChunks)
130
+ }
131
+
132
+ responseFinishChannel.publish({ ctx, res, body })
133
+ cleanup()
134
+ }
135
+
136
+ res.once('end', notifyFinish)
137
+ res.once('close', notifyFinish)
138
+
139
+ return {
140
+ finalizeIfNeeded () {
141
+ if (!bodyConsumed) {
142
+ // Body not consumed, resume to complete the response
143
+ notifyFinish()
144
+ }
145
+ },
146
+ }
147
+ }
148
+
42
149
  function patch (http, methodName) {
43
150
  shimmer.wrap(http, methodName, instrumentRequest)
44
151
 
@@ -103,7 +210,18 @@ function patch (http, methodName) {
103
210
  ctx.res = res
104
211
  res.once('end', finish)
105
212
  res.once(errorMonitor, finish)
106
- break
213
+
214
+ const instrumentation = setupResponseInstrumentation(ctx, res)
215
+
216
+ if (!instrumentation) {
217
+ break
218
+ }
219
+
220
+ const result = emit.apply(this, arguments)
221
+
222
+ instrumentation.finalizeIfNeeded()
223
+
224
+ return result
107
225
  }
108
226
  case 'connect':
109
227
  case 'upgrade':
@@ -129,6 +129,8 @@ function getTestEnvironmentOptions (config) {
129
129
  return {}
130
130
  }
131
131
 
132
+ const MAX_IGNORED_TEST_NAMES = 10
133
+
132
134
  function getTestStats (testStatuses) {
133
135
  return testStatuses.reduce((acc, testStatus) => {
134
136
  acc[testStatus]++
@@ -136,6 +138,32 @@ function getTestStats (testStatuses) {
136
138
  }, { pass: 0, fail: 0 })
137
139
  }
138
140
 
141
+ /**
142
+ * @param {string[]} efdNames
143
+ * @param {string[]} quarantineNames
144
+ * @param {number} totalCount
145
+ */
146
+ function logIgnoredFailuresSummary (efdNames, quarantineNames, totalCount) {
147
+ const names = []
148
+ for (const n of efdNames) {
149
+ names.push({ name: n, reason: 'Early Flake Detection' })
150
+ }
151
+ for (const n of quarantineNames) {
152
+ names.push({ name: n, reason: 'Quarantine' })
153
+ }
154
+ const shown = names.slice(0, MAX_IGNORED_TEST_NAMES)
155
+ const more = names.length - shown.length
156
+ const moreSuffix = more > 0 ? `\n ... and ${more} more` : ''
157
+ const list = shown.map(({ name, reason }) => ` • ${name} (${reason})`).join('\n')
158
+ const line = '-'.repeat(50)
159
+ // eslint-disable-next-line no-console -- Intentional user-facing message when exit code is flipped
160
+ console.warn(
161
+ `\n${line}\nDatadog Test Optimization\n${line}\n` +
162
+ `${totalCount} test failure(s) were ignored. Exit code set to 0.\n\n` +
163
+ `${list}${moreSuffix}\n`
164
+ )
165
+ }
166
+
139
167
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
140
168
  return class DatadogEnvironment extends BaseEnvironment {
141
169
  constructor (config, context) {
@@ -576,6 +604,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
576
604
  } else {
577
605
  newTestsTestStatuses.set(testName, [status])
578
606
  }
607
+ const testStatuses = newTestsTestStatuses.get(testName)
608
+ // Check if this is the last EFD retry.
609
+ // If it is, we'll set the failedAllTests flag to true if all the tests failed
610
+ if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 &&
611
+ testStatuses.every(status => status === 'fail')) {
612
+ failedAllTests = true
613
+ }
579
614
  }
580
615
  }
581
616
 
@@ -994,11 +1029,18 @@ function getCliWrapper (isNewJestVersion) {
994
1029
  coverageMap,
995
1030
  numFailedTestSuites,
996
1031
  numFailedTests,
1032
+ numRuntimeErrorTestSuites = 0,
997
1033
  numTotalTests,
998
1034
  numTotalTestSuites,
1035
+ runExecError,
1036
+ wasInterrupted,
999
1037
  },
1000
1038
  } = result
1001
1039
 
1040
+ const hasSuiteLevelFailures = numRuntimeErrorTestSuites > 0
1041
+ const hasRunLevelFailure = runExecError != null || wasInterrupted === true
1042
+ const mustNotFlipSuccess = hasSuiteLevelFailures || hasRunLevelFailure
1043
+
1002
1044
  let testCodeCoverageLinesTotal
1003
1045
 
1004
1046
  if (isUserCodeCoverageEnabled) {
@@ -1018,16 +1060,44 @@ function getCliWrapper (isNewJestVersion) {
1018
1060
  * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
1019
1061
  */
1020
1062
  let numEfdFailedTestsToIgnore = 0
1063
+ const efdIgnoredNames = []
1064
+ const quarantineIgnoredNames = []
1065
+
1066
+ // Build fullName -> suite map from results (for EFD display)
1067
+ const fullNameToSuite = new Map()
1068
+ for (const { testResults, testFilePath } of result.results.testResults) {
1069
+ const suite = getTestSuitePath(testFilePath, result.globalConfig.rootDir)
1070
+ for (const { fullName } of testResults) {
1071
+ const name = testSuiteAbsolutePathsWithFastCheck.has(testFilePath)
1072
+ ? fullName.replace(SEED_SUFFIX_RE, '')
1073
+ : fullName
1074
+ fullNameToSuite.set(name, suite)
1075
+ }
1076
+ }
1077
+
1078
+ /** @type {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} */
1079
+ let ignoredFailuresSummary
1021
1080
  if (isEarlyFlakeDetectionEnabled) {
1022
- for (const testStatuses of newTestsTestStatuses.values()) {
1081
+ for (const [testName, testStatuses] of newTestsTestStatuses) {
1023
1082
  const { pass, fail } = getTestStats(testStatuses)
1024
1083
  if (pass > 0) { // as long as one passes, we'll consider the test passed
1025
1084
  numEfdFailedTestsToIgnore += fail
1085
+ const suite = fullNameToSuite.get(testName)
1086
+ efdIgnoredNames.push(suite ? `${suite} › ${testName}` : testName)
1026
1087
  }
1027
1088
  }
1028
1089
  // If every test that failed was an EFD retry, we'll consider the suite passed
1029
- if (numEfdFailedTestsToIgnore !== 0 && result.results.numFailedTests === numEfdFailedTestsToIgnore) {
1090
+ if (
1091
+ !mustNotFlipSuccess &&
1092
+ numEfdFailedTestsToIgnore !== 0 &&
1093
+ result.results.numFailedTests === numEfdFailedTestsToIgnore
1094
+ ) {
1030
1095
  result.results.success = true
1096
+ ignoredFailuresSummary = {
1097
+ efdNames: efdIgnoredNames,
1098
+ quarantineNames: [],
1099
+ totalCount: numEfdFailedTestsToIgnore,
1100
+ }
1031
1101
  }
1032
1102
  }
1033
1103
 
@@ -1062,8 +1132,10 @@ function getCliWrapper (isNewJestVersion) {
1062
1132
  // This uses `attempt_to_fix` because this is always the main process and it's not formatted in camelCase
1063
1133
  if (testManagementTest?.attempt_to_fix && (testManagementTest?.quarantined || testManagementTest?.disabled)) {
1064
1134
  numFailedQuarantinedOrDisabledAttemptedToFixTests++
1135
+ quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1065
1136
  } else if (testManagementTest?.quarantined) {
1066
1137
  numFailedQuarantinedTests++
1138
+ quarantineIgnoredNames.push(`${testSuite} › ${testName}`)
1067
1139
  }
1068
1140
  }
1069
1141
 
@@ -1071,22 +1143,42 @@ function getCliWrapper (isNewJestVersion) {
1071
1143
  // Note that if a test is attempted to fix,
1072
1144
  // it's considered quarantined both if it's disabled and if it's quarantined
1073
1145
  // (it'll run but its status is ignored)
1146
+ // Skip if EFD block already flipped (to avoid logging twice)
1074
1147
  if (
1148
+ !result.results.success &&
1149
+ !mustNotFlipSuccess &&
1075
1150
  (numFailedQuarantinedOrDisabledAttemptedToFixTests !== 0 || numFailedQuarantinedTests !== 0) &&
1076
1151
  result.results.numFailedTests ===
1077
1152
  numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1078
1153
  ) {
1079
1154
  result.results.success = true
1155
+ ignoredFailuresSummary = {
1156
+ efdNames: [],
1157
+ quarantineNames: quarantineIgnoredNames,
1158
+ totalCount: numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests,
1159
+ }
1080
1160
  }
1081
1161
  }
1082
1162
 
1083
1163
  // Combined check: if all failed tests are accounted for by EFD (flaky retries) and/or quarantine,
1084
1164
  // we should consider the suite passed even when neither check alone covers all failures.
1085
- if (!result.results.success && (isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)) {
1165
+ if (
1166
+ !result.results.success &&
1167
+ !mustNotFlipSuccess &&
1168
+ (isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)
1169
+ ) {
1086
1170
  const totalIgnoredFailures =
1087
1171
  numEfdFailedTestsToIgnore + numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1088
- if (totalIgnoredFailures !== 0 && result.results.numFailedTests === totalIgnoredFailures) {
1172
+ if (
1173
+ totalIgnoredFailures !== 0 &&
1174
+ result.results.numFailedTests === totalIgnoredFailures
1175
+ ) {
1089
1176
  result.results.success = true
1177
+ ignoredFailuresSummary = {
1178
+ efdNames: efdIgnoredNames,
1179
+ quarantineNames: quarantineIgnoredNames,
1180
+ totalCount: totalIgnoredFailures,
1181
+ }
1090
1182
  }
1091
1183
  }
1092
1184
 
@@ -1144,6 +1236,14 @@ function getCliWrapper (isNewJestVersion) {
1144
1236
  })
1145
1237
  }
1146
1238
 
1239
+ if (ignoredFailuresSummary) {
1240
+ logIgnoredFailuresSummary(
1241
+ ignoredFailuresSummary.efdNames,
1242
+ ignoredFailuresSummary.quarantineNames,
1243
+ ignoredFailuresSummary.totalCount
1244
+ )
1245
+ }
1246
+
1147
1247
  numSkippedSuites = 0
1148
1248
 
1149
1249
  return result
@@ -271,6 +271,7 @@ function getOnTestEndHandler (config) {
271
271
  const testStatuses = testsStatuses.get(testName)
272
272
 
273
273
  const isLastAttempt = testStatuses.length === config.testManagementAttemptToFixRetries + 1
274
+ const isLastEfdRetry = testStatuses.length === config.earlyFlakeDetectionNumRetries + 1
274
275
 
275
276
  if (test._ddIsAttemptToFix && isLastAttempt) {
276
277
  if (testStatuses.includes('fail')) {
@@ -283,6 +284,11 @@ function getOnTestEndHandler (config) {
283
284
  }
284
285
  }
285
286
 
287
+ if (test._ddIsEfdRetry && isLastEfdRetry &&
288
+ testStatuses.every(status => status === 'fail')) {
289
+ hasFailedAllRetries = true
290
+ }
291
+
286
292
  const isAttemptToFixRetry = test._ddIsAttemptToFix && testStatuses.length > 1
287
293
  const isAtrRetry = config.isFlakyTestRetriesEnabled &&
288
294
  !test._ddIsAttemptToFix &&