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
@@ -206,6 +206,32 @@ function wrapConnection (Connection, version) {
206
206
  })
207
207
  }
208
208
  }
209
+ /**
210
+ * mysql2 defers a busy pool's `getConnection` callback and runs it in the releasing connection's
211
+ * async context; capture the caller's context and restore it around the callback so spans created
212
+ * by the queued query attach to the caller rather than the previous query that freed the connection.
213
+ *
214
+ * @param {Function} Pool
215
+ * @returns {Function}
216
+ */
217
+ function wrapGetConnection (Pool) {
218
+ const connectionStartCh = channel('apm:mysql2:connection:start')
219
+ const connectionFinishCh = channel('apm:mysql2:connection:finish')
220
+
221
+ shimmer.wrap(Pool.prototype, 'getConnection', getConnection => function (cb) {
222
+ const ctx = {}
223
+ arguments[0] = function (...args) {
224
+ return connectionFinishCh.runStores(ctx, cb, this, ...args)
225
+ }
226
+
227
+ connectionStartCh.publish(ctx)
228
+
229
+ return getConnection.apply(this, arguments)
230
+ })
231
+
232
+ return Pool
233
+ }
234
+
209
235
  /**
210
236
  * @param {Function} Pool
211
237
  * @param {string} version
@@ -215,6 +241,8 @@ function wrapPool (Pool, version) {
215
241
  const startOuterQueryCh = channel('datadog:mysql2:outerquery:start')
216
242
  const shouldEmitEndAfterQueryAbort = satisfies(version, '>=1.3.3')
217
243
 
244
+ wrapGetConnection(Pool)
245
+
218
246
  shimmer.wrap(Pool.prototype, 'query', query => function (sql, values, cb) {
219
247
  if (!startOuterQueryCh.hasSubscribers) return query.apply(this, arguments)
220
248
 
@@ -373,6 +401,12 @@ addHook(
373
401
  /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapPool)
374
402
  )
375
403
 
404
+ // mysql2 >=3.11.5 moved the pool onto BasePool in lib/base/pool.js.
405
+ addHook(
406
+ { name: 'mysql2', file: 'lib/base/pool.js', versions: ['>=3.11.5'] },
407
+ /** @type {(moduleExports: unknown, version: string) => unknown} */ (wrapGetConnection)
408
+ )
409
+
376
410
  // PoolNamespace.prototype.query does not exist in mysql2<2.3.0
377
411
  addHook(
378
412
  { name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['2.3.0 - 3.11.4'] },
@@ -49,20 +49,22 @@ addHook({ name: 'net' }, (net) => {
49
49
 
50
50
  const emit = this.emit
51
51
  let pendingReadyEvents = 2
52
- this.emit = shimmer.wrapFunction(emit, emit => function (eventName) {
52
+ // Named `emit`/arity-1 mirrors the socket method so the per-socket wrap
53
+ // skips its name/length rewrite.
54
+ this.emit = shimmer.wrapFunction(emit, originalEmit => function emit (eventName) {
53
55
  switch (eventName) {
54
56
  case 'ready':
55
57
  case 'connect':
56
- if (--pendingReadyEvents === 0) this.emit = emit
58
+ if (--pendingReadyEvents === 0) this.emit = originalEmit
57
59
  return readyCh.runStores(ctx, () => {
58
- return emit.apply(this, arguments)
60
+ return Reflect.apply(originalEmit, this, arguments)
59
61
  })
60
62
  case 'error':
61
63
  case 'close':
62
- this.emit = emit
63
- return emit.apply(this, arguments)
64
+ this.emit = originalEmit
65
+ return Reflect.apply(originalEmit, this, arguments)
64
66
  default:
65
- return emit.apply(this, arguments)
67
+ return Reflect.apply(originalEmit, this, arguments)
66
68
  }
67
69
  })
68
70
 
@@ -240,6 +240,10 @@ for (const extension of extensions) {
240
240
  }
241
241
 
242
242
  return ch.start.runStores(ctx, () => {
243
+ // Explicit childOf rather than async-context: the _thenUnwrap/parse path
244
+ // decouples the lazy evaluation from the active scope at call time.
245
+ if (guard) guard.parentSpan = ctx.currentStore?.span
246
+
243
247
  const apiProm = methodFn.apply(this, args)
244
248
 
245
249
  if (baseResource === 'chat.completions' && typeof apiProm._thenUnwrap === 'function') {
@@ -300,23 +304,31 @@ function handleUnwrappedAPIPromise (apiProm, ctx, stream, guard) {
300
304
  return body
301
305
  }
302
306
 
303
- finish(ctx, {
307
+ const responseData = {
304
308
  headers: response.headers,
305
309
  data: body,
306
310
  request: {
307
311
  path: response.url,
308
312
  method: options.method,
309
313
  },
310
- })
314
+ }
311
315
 
312
- if (!guard) return body
316
+ if (!guard) {
317
+ finish(ctx, responseData)
318
+ return body
319
+ }
313
320
 
314
- return aiGuard.evaluateOutput(guard, body).then(() => body)
321
+ // Finish after evaluation so a block propagates the error to openai.request
322
+ // and the span wraps its ai_guard child instead of closing before it.
323
+ return aiGuard.evaluateOutput(guard, body).then(() => {
324
+ finish(ctx, responseData)
325
+ return body
326
+ })
315
327
  })
316
328
  .catch(error => {
317
- // ctx.result is set inside finish(); if absent, finish never ran (sync throw in
318
- // success branch, before-model block, or openai error) — record the error now.
319
- // If finish already ran successfully (after-model block), don't double-publish.
329
+ // ctx.result is set inside finish(); if absent, finish never ran (sync throw in the success
330
+ // branch, Before Model block, After Model block, or openai error) — record the error now so
331
+ // the openai.request span is marked errored. If finish already ran, don't double-publish.
320
332
  if (!ctx.result) finish(ctx, undefined, error)
321
333
  throw error
322
334
  })
@@ -15,6 +15,9 @@ const errorCh = channel('apm:pg:query:error')
15
15
  const startPoolQueryCh = channel('datadog:pg:pool:query:start')
16
16
  const finishPoolQueryCh = channel('datadog:pg:pool:query:finish')
17
17
 
18
+ const poolConnectStartCh = channel('apm:pg:pool:connect:start')
19
+ const poolConnectFinishCh = channel('apm:pg:pool:connect:finish')
20
+
18
21
  // Drivers like pg-promise reuse the same prepared-statement query object across executions; cache
19
22
  // the un-injected `text` so the wrap doesn't capture a previous DBM injection as the new original.
20
23
  const originalTextCache = new WeakMap()
@@ -30,6 +33,22 @@ addHook({ name: 'pg', versions: ['>=8.0.3'], file: 'lib/client.js' }, Client =>
30
33
  })
31
34
 
32
35
  addHook({ name: 'pg', versions: ['>=8.0.3'] }, pg => {
36
+ // pg defers a busy pool's connect callback and runs it in the releasing query's async context;
37
+ // capture the caller's context and restore it around the callback so spans attach to the caller.
38
+ shimmer.wrap(pg.Pool.prototype, 'connect', connect => function (cb) {
39
+ if (typeof cb !== 'function' || !poolConnectStartCh.hasSubscribers) {
40
+ return connect.apply(this, arguments)
41
+ }
42
+
43
+ const ctx = {}
44
+ arguments[0] = function (...args) {
45
+ return poolConnectFinishCh.runStores(ctx, cb, this, ...args)
46
+ }
47
+
48
+ poolConnectStartCh.publish(ctx)
49
+
50
+ return connect.apply(this, arguments)
51
+ })
33
52
  shimmer.wrap(pg.Pool.prototype, 'query', query => wrapPoolQuery(query))
34
53
  return pg
35
54
  })
@@ -157,9 +157,9 @@ function createWrapRouterMethod (name, compile) {
157
157
  }
158
158
 
159
159
  function wrapNext (req, originalNext) {
160
- // Per layer dispatch, N per request. `shimmer.wrapCallback` preserves
161
- // only `name` + `length`; see its JSDoc for the full contract.
162
- return shimmer.wrapCallback(originalNext, next => function (error) {
160
+ // Per layer dispatch, N per request. Named `next`/arity-1 mirrors the
161
+ // router continuation so wrapCallback skips its name/length rewrite.
162
+ return shimmer.wrapCallback(originalNext, original => function next (error) {
163
163
  if (error && error !== 'route' && error !== 'router') {
164
164
  errorChannel.publish({ req, error })
165
165
  }
@@ -167,7 +167,7 @@ function createWrapRouterMethod (name, compile) {
167
167
  nextChannel.publish({ req })
168
168
  finishChannel.publish({ req })
169
169
 
170
- next.apply(this, arguments)
170
+ original.apply(this, arguments)
171
171
  })
172
172
  }
173
173
 
@@ -328,7 +328,8 @@ const routerParamStartCh = channel('datadog:router:param:start')
328
328
  const visitedParams = new WeakSet()
329
329
 
330
330
  function wrapHandleRequest (original) {
331
- return function wrappedHandleRequest (req, res, next) {
331
+ return function wrappedHandleRequest (...args) {
332
+ const req = args[0]
332
333
  if (routerParamStartCh.hasSubscribers && !visitedParams.has(req.params) && Object.keys(req.params).length) {
333
334
  visitedParams.add(req.params)
334
335
 
@@ -336,7 +337,7 @@ function wrapHandleRequest (original) {
336
337
 
337
338
  routerParamStartCh.publish({
338
339
  req,
339
- res,
340
+ res: args[1],
340
341
  params: req?.params,
341
342
  abortController,
342
343
  })
@@ -344,7 +345,7 @@ function wrapHandleRequest (original) {
344
345
  if (abortController.signal.aborted) return
345
346
  }
346
347
 
347
- return original.apply(this, arguments)
348
+ return Reflect.apply(original, this, args)
348
349
  }
349
350
  }
350
351
 
@@ -358,7 +359,8 @@ addHook({
358
359
  function wrapParam (original) {
359
360
  return function wrappedProcessParams (...args) {
360
361
  args[1] = shimmer.wrapFunction(args[1], (originalFn) => {
361
- return function wrappedFn (req, res) {
362
+ return function wrappedFn (...fnArgs) {
363
+ const req = fnArgs[0]
362
364
  if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) {
363
365
  visitedParams.add(req.params)
364
366
 
@@ -366,7 +368,7 @@ function wrapParam (original) {
366
368
 
367
369
  routerParamStartCh.publish({
368
370
  req,
369
- res,
371
+ res: fnArgs[1],
370
372
  params: req?.params,
371
373
  abortController,
372
374
  })
@@ -374,7 +376,7 @@ function wrapParam (original) {
374
376
  if (abortController.signal.aborted) return
375
377
  }
376
378
 
377
- return originalFn.apply(this, arguments)
379
+ return Reflect.apply(originalFn, this, fnArgs)
378
380
  }
379
381
  })
380
382
 
@@ -57,6 +57,7 @@ const codeCoverageReportCh = channel('ci:vitest:coverage-report')
57
57
 
58
58
  const taskToCtx = new WeakMap()
59
59
  const taskToStatuses = new WeakMap()
60
+ const taskToReportedErrorCount = new WeakMap()
60
61
  const attemptToFixTaskToStatuses = new WeakMap()
61
62
  const originalHookFns = new WeakMap()
62
63
  const newTasks = new WeakSet()
@@ -316,6 +317,27 @@ function recordFinalAttemptToFixExecution (task, status, providedContext) {
316
317
  })
317
318
  }
318
319
 
320
+ function disableFrameworkRetries (task) {
321
+ task.retry = 0
322
+ }
323
+
324
+ /**
325
+ * Vitest accumulates retry and repeat errors on one task result. The first error added since
326
+ * the last reported attempt is the primary error for the failed attempt currently being reported.
327
+ *
328
+ * @param {object} task
329
+ * @param {Array<object> | undefined} errors
330
+ * @returns {object | undefined}
331
+ */
332
+ function getCurrentAttemptTestError (task, errors) {
333
+ if (!errors?.length) return
334
+
335
+ const previousErrorCount = taskToReportedErrorCount.get(task) ?? 0
336
+ const testError = errors[previousErrorCount] ?? errors[0]
337
+ taskToReportedErrorCount.set(task, errors.length)
338
+ return testError
339
+ }
340
+
319
341
  /**
320
342
  * Wraps a function so it runs inside the current test span context.
321
343
  * @param {object} task
@@ -801,6 +823,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
801
823
  onDone: (isAttemptToFix) => {
802
824
  if (isAttemptToFix) {
803
825
  isRetryReasonAttemptToFix = task.repeats !== testManagementAttemptToFixRetries
826
+ disableFrameworkRetries(task)
804
827
  task.repeats = testManagementAttemptToFixRetries
805
828
  attemptToFixTasks.add(task)
806
829
  attemptToFixTaskToStatuses.set(task, [])
@@ -831,6 +854,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
831
854
  if (isImpacted) {
832
855
  if (isEarlyFlakeDetectionEnabled) {
833
856
  isRetryReasonEfd = true
857
+ disableFrameworkRetries(task)
834
858
  task.repeats = numRepeats
835
859
  }
836
860
  modifiedTasks.add(task)
@@ -849,6 +873,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
849
873
  if (isNew && !attemptToFixTasks.has(task)) {
850
874
  if (isEarlyFlakeDetectionEnabled && !modifiedTasks.has(task)) {
851
875
  isRetryReasonEfd = true
876
+ disableFrameworkRetries(task)
852
877
  task.repeats = numRepeats
853
878
  }
854
879
  newTasks.add(task)
@@ -986,7 +1011,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
986
1011
  const promises = {}
987
1012
  const shouldSetProbe = isDiEnabled && numAttempt === 1
988
1013
  const ctx = taskToCtx.get(task)
989
- const testError = task.result?.errors?.[0]
1014
+ const testError = getCurrentAttemptTestError(task, task.result?.errors)
990
1015
  if (ctx) {
991
1016
  testErrorCh.publish({
992
1017
  error: testError,
@@ -1016,7 +1041,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
1016
1041
  const ctx = taskToCtx.get(task)
1017
1042
  if (ctx) {
1018
1043
  if (lastExecutionStatus === 'fail') {
1019
- const testError = task.result?.errors?.[0]
1044
+ const testError = getCurrentAttemptTestError(task, task.result?.errors)
1020
1045
  testErrorCh.publish({ error: testError, ...ctx.currentStore })
1021
1046
  } else {
1022
1047
  testPassCh.publish({ task, ...ctx.currentStore })
@@ -1040,7 +1065,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
1040
1065
 
1041
1066
  const ctx = taskToCtx.get(task)
1042
1067
  if (lastExecutionStatus === 'fail') {
1043
- const testError = task.result?.errors?.[0]
1068
+ const testError = getCurrentAttemptTestError(task, task.result?.errors)
1044
1069
  testErrorCh.publish({ error: testError, ...ctx.currentStore })
1045
1070
  } else {
1046
1071
  testPassCh.publish({ task, ...ctx.currentStore })
@@ -1389,7 +1414,7 @@ addHook({
1389
1414
 
1390
1415
  if (result) {
1391
1416
  const { state, duration, errors } = result
1392
- const testError = errors?.[0]
1417
+ const testError = getCurrentAttemptTestError(task, errors)
1393
1418
  if (attemptToFixTasks.has(task)) {
1394
1419
  const status = getFinalAttemptToFixStatus(task, state, isSwitchedStatus, testCtx)
1395
1420
  recordFinalAttemptToFixExecution(task, status, providedContext)
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
4
3
  const ClientPlugin = require('../../dd-trace/src/plugins/client')
5
4
  const { storage } = require('../../datadog-core')
6
5
  const { tagsFromRequest, tagsFromResponse } = require('../../dd-trace/src/payload-tagging')
@@ -113,8 +112,6 @@ class BaseAwsSdkPlugin extends ClientPlugin {
113
112
  integrationName: 'aws-sdk',
114
113
  }, ctx)
115
114
 
116
- analyticsSampler.sample(span, this.config.measured)
117
-
118
115
  storage('legacy').run(ctx.currentStore, () => {
119
116
  this.requestInject(span, request)
120
117
  })
@@ -5,6 +5,43 @@ const BaseAwsSdkPlugin = require('../base')
5
5
  const { DsmPathwayCodec, getHeadersSize } = require('../../../dd-trace/src/datastreams')
6
6
  const { extractQueueMetadata, isEmpty } = require('../util')
7
7
 
8
+ /**
9
+ * @typedef {{
10
+ * 'detail-type'?: string,
11
+ * detail?: { _datadog?: Record<string, string> },
12
+ * Type?: string,
13
+ * Message?: string
14
+ * }} ParsedSqsBody
15
+ */
16
+
17
+ /**
18
+ * Resolve the EventBridge `_datadog` text map from a parsed SQS body — for both
19
+ * EventBridge -> SQS (`body.detail._datadog`) and EventBridge -> SNS -> SQS (the
20
+ * envelope is the SNS `Notification`'s stringified `Message`). Keyed off
21
+ * `detail-type`, the marker AWS sets on every PutEvents delivery. Relies on the
22
+ * default SQS-target shape; a target InputTransformer can drop `detail`.
23
+ *
24
+ * @param {ParsedSqsBody} [parsedBody]
25
+ * @returns {Record<string, string> | undefined}
26
+ */
27
+ function getEventBridgeContext (parsedBody) {
28
+ let envelope
29
+ if (parsedBody?.['detail-type'] !== undefined) {
30
+ envelope = parsedBody // EventBridge -> SQS
31
+ } else if (parsedBody?.Type === 'Notification' && typeof parsedBody.Message === 'string') {
32
+ // EventBridge -> SNS -> SQS
33
+ try {
34
+ const innerEnvelope = JSON.parse(parsedBody.Message)
35
+ if (innerEnvelope?.['detail-type'] !== undefined) {
36
+ envelope = innerEnvelope
37
+ }
38
+ } catch {
39
+ // SNS `Message` not JSON
40
+ }
41
+ }
42
+ return envelope?.detail?._datadog
43
+ }
44
+
8
45
  class Sqs extends BaseAwsSdkPlugin {
9
46
  static id = 'sqs'
10
47
  static peerServicePrecursors = ['queuename']
@@ -149,17 +186,28 @@ class Sqs extends BaseAwsSdkPlugin {
149
186
  }
150
187
  }
151
188
 
152
- if (!message.MessageAttributes || !message.MessageAttributes._datadog) {
153
- return { parsedBody, bodyChecked: true }
189
+ // Check MessageAttributes first (common direct-SQS/SNS path): avoids parsing
190
+ // the body (e.g. a large SNS `Message`) just to rule out an EventBridge
191
+ // envelope. Precedence matches responseExtractDSMContext.
192
+ const datadogAttribute = message.MessageAttributes?._datadog
193
+ if (datadogAttribute) {
194
+ const parsedAttributes = this.parseDatadogAttributes(datadogAttribute)
195
+ if (parsedAttributes) {
196
+ return {
197
+ datadogContext: this.tracer.extract('text_map', parsedAttributes),
198
+ parsedAttributes,
199
+ parsedBody,
200
+ bodyChecked: true,
201
+ }
202
+ }
154
203
  }
155
204
 
156
- const datadogAttribute = message.MessageAttributes._datadog
157
-
158
- const parsedAttributes = this.parseDatadogAttributes(datadogAttribute)
159
- if (parsedAttributes) {
205
+ // Then the EventBridge envelope (optionally via SNS); see getEventBridgeContext.
206
+ const eventBridgeContext = getEventBridgeContext(parsedBody)
207
+ if (eventBridgeContext) {
160
208
  return {
161
- datadogContext: this.tracer.extract('text_map', parsedAttributes),
162
- parsedAttributes,
209
+ datadogContext: this.tracer.extract('text_map', eventBridgeContext),
210
+ parsedAttributes: eventBridgeContext,
163
211
  parsedBody,
164
212
  bodyChecked: true,
165
213
  }
@@ -213,15 +261,18 @@ class Sqs extends BaseAwsSdkPlugin {
213
261
  if (body?.Type === 'Notification') {
214
262
  message = body
215
263
  }
216
- if (message.MessageAttributes && message.MessageAttributes._datadog) {
217
- parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
218
- }
264
+ // MessageAttributes for direct SQS/SNS; else the EventBridge envelope.
265
+ parsedAttributes = message.MessageAttributes?._datadog
266
+ ? this.parseDatadogAttributes(message.MessageAttributes._datadog)
267
+ : getEventBridgeContext(body)
219
268
  }
220
269
  const payloadSize = getHeadersSize({
221
270
  Body: message.Body,
222
271
  MessageAttributes: message.MessageAttributes,
223
272
  })
224
273
  if (parsedAttributes) {
274
+ // Inert for EventBridge until its producer emits a pathway (separate
275
+ // change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
225
276
  this.tracer.decodeDataStreamsContext(parsedAttributes)
226
277
  }
227
278
  this.tracer
@@ -80,6 +80,7 @@ class CucumberPlugin extends CiPlugin {
80
80
  isTestManagementTestsEnabled,
81
81
  isParallel,
82
82
  }) => {
83
+ this._exportPendingWorkerTraces()
83
84
  const {
84
85
  isSuitesSkippingEnabled,
85
86
  isCodeCoverageEnabled,
@@ -186,6 +187,7 @@ class CucumberPlugin extends CiPlugin {
186
187
  integrationName: this.constructor.id,
187
188
  })
188
189
  this._testSuiteSpansByTestSuite.set(testSuitePath, testSuiteSpan)
190
+ this._exportPendingWorkerTracesForTestSuite(testSuitePath)
189
191
 
190
192
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
191
193
  if (this.libraryConfig?.isCodeCoverageEnabled) {
@@ -136,6 +136,7 @@ function getRetriedTests (test, numRetries, tags) {
136
136
  // TODO: signal in framework logs that this is a retry.
137
137
  // TODO: Change it so these tests are allowed to fail.
138
138
  const clonedTest = test.clone()
139
+ disableFrameworkRetries(clonedTest)
139
140
  if (tags.includes('_ddIsEfdRetry')) {
140
141
  clonedTest._ddEfdRetryIndex = retryIndex + 1
141
142
  }
@@ -149,6 +150,19 @@ function getRetriedTests (test, numRetries, tags) {
149
150
  return retriedTests
150
151
  }
151
152
 
153
+ function disableFrameworkRetries (test) {
154
+ test._retries = 0
155
+ }
156
+
157
+ function shouldDisableFrameworkRetries (test) {
158
+ return test && (
159
+ test._ddIsAttemptToFix ||
160
+ (isTestIsolationEnabled &&
161
+ isEarlyFlakeDetectionEnabled &&
162
+ (test._ddIsNew || test._ddIsModified))
163
+ )
164
+ }
165
+
152
166
  const oldRunTests = Cypress.mocha.getRunner().runTests
153
167
  Cypress.mocha.getRunner().runTests = function (suite, fn) {
154
168
  if (!isKnownTestsEnabled && !isTestManagementEnabled && !isImpactedTestsEnabled) {
@@ -188,9 +202,11 @@ Cypress.mocha.getRunner().runTests = function (suite, fn) {
188
202
  let retryMessage = ''
189
203
  if (isAtemptToFix) {
190
204
  test._ddIsAttemptToFix = true
205
+ disableFrameworkRetries(test)
191
206
  retryMessage = 'because it is an attempt to fix'
192
207
  retriedTests = getRetriedTests(test, testManagementAttemptToFixRetries, ['_ddIsAttemptToFix'])
193
208
  } else if (isModified && isEarlyFlakeDetectionEnabled) {
209
+ disableFrameworkRetries(test)
194
210
  retryMessage = 'to detect flakes because it is modified'
195
211
  retriedTests = getRetriedTests(test, earlyFlakeDetectionNumRetries, [
196
212
  '_ddIsModified',
@@ -198,6 +214,7 @@ Cypress.mocha.getRunner().runTests = function (suite, fn) {
198
214
  isKnownTestsEnabled && isNewTest(test) && '_ddIsNew',
199
215
  ])
200
216
  } else if (isNew && isEarlyFlakeDetectionEnabled) {
217
+ disableFrameworkRetries(test)
201
218
  retryMessage = 'to detect flakes because it is new'
202
219
  retriedTests = getRetriedTests(test, earlyFlakeDetectionNumRetries, ['_ddIsNew', '_ddIsEfdRetry'])
203
220
  }
@@ -214,8 +231,21 @@ Cypress.mocha.getRunner().runTests = function (suite, fn) {
214
231
  return oldRunTests.apply(this, [suite, fn])
215
232
  }
216
233
 
234
+ Cypress.on('test:before:run', (attributes, test) => {
235
+ if (shouldDisableFrameworkRetries(test)) {
236
+ disableFrameworkRetries(test)
237
+ }
238
+ })
239
+
240
+ Cypress.on('test:before:run:async', (attributes, test) => {
241
+ if (shouldDisableFrameworkRetries(test)) {
242
+ disableFrameworkRetries(test)
243
+ }
244
+ })
245
+
217
246
  beforeEach(function () {
218
- const testName = Cypress.mocha.getRunner().suite.ctx.currentTest.fullTitle()
247
+ const currentTest = Cypress.mocha.getRunner().suite.ctx.currentTest
248
+ const testName = currentTest.fullTitle()
219
249
 
220
250
  const retryMessage = retryReasonsByTestName.get(testName)
221
251
  if (retryMessage) {
@@ -5,7 +5,6 @@ const { URL } = require('url')
5
5
  const ClientPlugin = require('../../dd-trace/src/plugins/client')
6
6
  const { storage } = require('../../datadog-core')
7
7
  const tags = require('../../../ext/tags')
8
- const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
9
8
  const formats = require('../../../ext/formats')
10
9
  const HTTP_HEADERS = formats.HTTP_HEADERS
11
10
  const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter')
@@ -69,8 +68,6 @@ class HttpClientPlugin extends ClientPlugin {
69
68
  this.tracer.inject(span, HTTP_HEADERS, options.headers)
70
69
  }
71
70
 
72
- analyticsSampler.sample(span, this.config.measured)
73
-
74
71
  message.span = span
75
72
  message.parentStore = store
76
73
  message.currentStore = { ...store, span }
@@ -34,6 +34,11 @@ class HttpServerPlugin extends ServerPlugin {
34
34
  if (this.#startConfig === undefined) {
35
35
  this.#refreshStartCache()
36
36
  }
37
+ // A fresh request has no span yet; web.startSpan creates and enters one.
38
+ // A reused context (HTTP/2 stream, serverless re-entry) returns the
39
+ // existing span without entering, so the explicit enter below is the only
40
+ // one in that case.
41
+ const spanCreated = !web.getContext(req)?.span
37
42
  const span = web.startSpan(
38
43
  this.tracer,
39
44
  this.#startConfig,
@@ -57,7 +62,12 @@ class HttpServerPlugin extends ServerPlugin {
57
62
  store = withRequest(store, req)
58
63
  }
59
64
 
60
- this.enter(span, store)
65
+ // Skip the re-enter when web.startSpan already entered { ...store, span }
66
+ // and AppSec did not rebuild the store with req: the bind would be
67
+ // identical to the one startSpan just made.
68
+ if (!spanCreated || appsecActive) {
69
+ this.enter(span, store)
70
+ }
61
71
 
62
72
  if (!context.instrumented) {
63
73
  context.res.writeHead = web.wrapWriteHead(context)
@@ -150,6 +150,7 @@ class MochaPlugin extends CiPlugin {
150
150
  ctx.parentStore = store
151
151
  ctx.currentStore = { ...store, testSuiteSpan }
152
152
  this._testSuiteSpansByTestSuite.set(testSuite, testSuiteSpan)
153
+ this._exportPendingWorkerTracesForTestSuite(testSuite)
153
154
  })
154
155
 
155
156
  this.addSub('ci:mocha:test-suite:finish', ({ testSuiteSpan, status }) => {
@@ -365,6 +366,7 @@ class MochaPlugin extends CiPlugin {
365
366
  isTestManagementEnabled,
366
367
  isParallel,
367
368
  }) => {
369
+ this._exportPendingWorkerTraces()
368
370
  if (this.testSessionSpan) {
369
371
  const {
370
372
  isSuitesSkippingEnabled,
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { storage } = require('../../datadog-core')
3
4
  const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
4
5
  const DatabasePlugin = require('../../dd-trace/src/plugins/database')
5
6
 
@@ -8,6 +9,15 @@ class PGPlugin extends DatabasePlugin {
8
9
  static operation = 'query'
9
10
  static system = 'postgres'
10
11
 
12
+ constructor () {
13
+ super(...arguments)
14
+
15
+ this.addSub('apm:pg:pool:connect:start', ctx => {
16
+ ctx.parentStore = storage('legacy').getStore()
17
+ })
18
+ this.addBind('apm:pg:pool:connect:finish', ctx => ctx.parentStore)
19
+ }
20
+
11
21
  bindStart (ctx) {
12
22
  const { params = {}, query, originalText, processId, stream } = ctx
13
23
  const service = this.serviceName({ pluginConfig: this.config, params })