dd-trace 5.114.0 → 5.116.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 (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -8,7 +8,8 @@ const { getCompileToRegexp } = require('./path-to-regexp')
8
8
  const {
9
9
  getRouterMountPaths,
10
10
  joinPath,
11
- setLayerMatchers,
11
+ setLayerMeta,
12
+ getLayerMeta,
12
13
  isAppMounted,
13
14
  setRouterMountPath,
14
15
  extractMountPaths,
@@ -25,137 +26,219 @@ function isFastSlash (layer, matchers) {
25
26
  return layer.regexp?.fast_slash ?? matchers.hasSlashPath
26
27
  }
27
28
 
28
- // TODO: Move this function to a shared file between Express and Router
29
29
  /**
30
+ * Cache the per-layer dispatch metadata in a side table instead of replacing
31
+ * `layer.handle`. Phase-sorting hosts (loopback's `_findLayerByHandler`) map a
32
+ * layer back to the user handler by scanning the handle, so the handle has to
33
+ * stay the user's function.
34
+ *
35
+ * @param {{ handle: Function, name?: string, path?: string,
36
+ * regexp?: { fast_star?: boolean, fast_slash?: boolean } }} layer
37
+ * @param {Array<{ path?: string, regex?: RegExp }> & { hasStarPath?: boolean, hasSlashPath?: boolean }} matchers
38
+ */
39
+ function annotateLayer (layer, matchers) {
40
+ const handle = layer.handle
41
+ const name = handle._name || layer.name || handle.name
42
+
43
+ let captureRoute
44
+ let needMultiMatch = false
45
+ if (matchers.length !== 0 && !isFastStar(layer, matchers) && !isFastSlash(layer, matchers)) {
46
+ if (matchers.length === 1) {
47
+ captureRoute = matchers[0].path
48
+ } else {
49
+ needMultiMatch = true
50
+ }
51
+ }
52
+
53
+ setLayerMeta(layer, { name, captureRoute, needMultiMatch, matchers })
54
+ }
55
+
56
+ /**
57
+ * Resolve the route for a dispatched layer. Single-pattern layers carry a
58
+ * constant route; only multi-pattern stacks need the per-request `layer.path`
59
+ * match the host already computed.
60
+ *
61
+ * @param {{ captureRoute?: string, needMultiMatch: boolean,
62
+ * matchers: Array<{ path?: string, regex?: RegExp }> }} meta
63
+ * @param {{ path?: string }} layer
64
+ * @returns {string | undefined}
65
+ */
66
+ function resolveLayerRoute (meta, layer) {
67
+ if (!meta.needMultiMatch) return meta.captureRoute
68
+
69
+ for (const matcher of meta.matchers) {
70
+ if (matcher.regex?.test(layer.path)) return matcher.path
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Build the request/error dispatch wrappers for one host (`express` / `router`).
76
+ * They wrap the layer's prototype dispatch and read the side-table metadata, so
77
+ * `layer.handle` is never replaced. The arity guard mirrors the host's own
78
+ * (`handle_request` skips 4-arg handlers, `handle_error` skips the rest), so a
79
+ * span is published only for the layer the host actually runs.
80
+ *
30
81
  * @param {string} name Channel namespace (`apm:<name>:middleware:*`).
31
- * @param {((pattern: string | RegExp) => RegExp | undefined) | undefined} compile
32
- * Host-resolved path-to-regexp compile adapter, or undefined when the host
33
- * instance ships no path-to-regexp. Captured here so each express/router
34
- * instance keeps the dialect it actually loaded.
35
82
  */
36
- function createWrapRouterMethod (name, compile) {
83
+ function createLayerDispatchWrappers (name) {
37
84
  const enterChannel = channel(`apm:${name}:middleware:enter`)
38
85
  const exitChannel = channel(`apm:${name}:middleware:exit`)
39
86
  const finishChannel = channel(`apm:${name}:middleware:finish`)
40
87
  const errorChannel = channel(`apm:${name}:middleware:error`)
41
88
  const nextChannel = channel(`apm:${name}:middleware:next`)
42
- const routeAddedChannel = channel(`apm:${name}:route:added`)
89
+ const repeatChannel = channel(`apm:${name}:middleware:repeat`)
43
90
  // Bound per name so express and a bare router keep independent guards.
44
91
  const publishError = createErrorPublisher(errorChannel)
45
92
 
46
- function wrapLayerHandle (layer, original, matchers) {
47
- // Resolve `name` once at wrap time: cached on the original for any code
48
- // that reads `_name`, captured in the closure so the per-call body avoids
49
- // the property-lookup / `||` fallback.
50
- const name = original._name || layer.name || original.name
51
- original._name = name
52
-
53
- // Wrap-time matcher analysis. The single-pattern case yields a constant
54
- // route; only multi-pattern stacks need a per-request layer.path match.
55
- let captureRoute
56
- let needMultiMatch = false
57
- if (matchers.length !== 0 && !isFastStar(layer, matchers) && !isFastSlash(layer, matchers)) {
58
- if (matchers.length === 1) {
59
- captureRoute = matchers[0].path
60
- } else {
61
- needMultiMatch = true
62
- }
63
- }
93
+ /**
94
+ * @param {import('node:http').IncomingMessage} req
95
+ * @param {string | undefined} layerName
96
+ * @param {(error?: unknown) => void} originalNext
97
+ */
98
+ function wrapNext (req, layerName, originalNext) {
99
+ // Per layer dispatch, N per request. Named `next`/arity-1 mirrors the
100
+ // router continuation so wrapCallback skips its name/length rewrite.
101
+ let calls = 0
102
+ return shimmer.wrapCallback(originalNext, original => function next (error) {
103
+ // A handler that calls `next()` and then rejects (`next(); await bg()`)
104
+ // makes the host call this continuation twice. Publish once so the second
105
+ // pass cannot tag the already-finished span's parent with a late error.
106
+ calls++
107
+ if (calls === 1) {
108
+ if (error && error !== 'route' && error !== 'router') {
109
+ publishError({ req, error })
110
+ }
64
111
 
65
- // Split by arity: router only ever dispatches 3-arg request handlers
66
- // through `Layer.handleRequest` and 4-arg error handlers through
67
- // `Layer.handleError`. Specialising lets the per-call body use named
68
- // parameters and `.call`, avoiding the rest-spread Array allocation that
69
- // the unified shape forced on every middleware invocation.
70
- const wrapped = original.length === 4
71
- ? shimmer.wrapFunction(original, errorHandlerLayerWrap(layer, name, captureRoute, needMultiMatch, matchers))
72
- : shimmer.wrapFunction(original, requestHandlerLayerWrap(layer, name, captureRoute, needMultiMatch, matchers))
73
-
74
- // Workaround for loopback's phase-based middleware sorting. Its
75
- // `_findLayerByHandler` maps a layer back to the user handler by scanning
76
- // the layer handle's enumerable properties for the original function.
77
- // Replacing `layer.handle` with this wrapper hides that handler, so without
78
- // the back-reference loopback cannot tag the layer with its phase and
79
- // `app.middleware(phase, ...)` handlers run in insertion order instead of
80
- // phase order. The property name is part of that contract; keep it stable.
81
- wrapped._datadog_orig = original
112
+ nextChannel.publish({ req })
113
+ finishChannel.publish({ req })
114
+ } else if (calls === 2) {
115
+ // Surface the repeat as a diagnostic on the still-live request span. The
116
+ // host cannot tell a legitimate `next(); await bg()` from a buggy double
117
+ // `next()`, so this only records that it happened, not that it is wrong.
118
+ repeatChannel.publish({ req, name: layerName, error })
119
+ }
82
120
 
83
- return wrapped
121
+ original.apply(this, arguments)
122
+ })
84
123
  }
85
124
 
86
- function requestHandlerLayerWrap (layer, name, captureRoute, needMultiMatch, matchers) {
87
- return original => function (req, res, next) {
88
- if (!enterChannel.hasSubscribers) return original.call(this, req, res, next)
89
-
90
- const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
125
+ // Every host dispatch turns a synchronous throw into `next(error)`, and the
126
+ // hosts that await the handler (router >=2, express 5, express 4 +
127
+ // express-async-errors) do the same for a rejected promise. Passing
128
+ // `wrappedNext` through captures both without a tracer-side try/catch; only
129
+ // `exit` needs the `finally`. express 4's native dispatch converts only the
130
+ // synchronous throw — exactly what the pre-refactor handle wrap caught.
131
+ function wrapLayerRequest (originalRequest) {
132
+ return function (req, res, next) {
133
+ if (!enterChannel.hasSubscribers) return originalRequest.call(this, req, res, next)
91
134
 
92
- let route = captureRoute
93
- if (needMultiMatch) {
94
- for (const matcher of matchers) {
95
- if (matcher.regex?.test(layer.path)) {
96
- route = matcher.path
97
- break
98
- }
99
- }
100
- }
135
+ const meta = getLayerMeta(this)
136
+ if (meta === undefined || this.handle.length > 3) return originalRequest.call(this, req, res, next)
101
137
 
102
- enterChannel.publish({ name, req, route, layer })
138
+ const wrappedNext = typeof next === 'function' ? wrapNext(req, meta.name, next) : next
139
+ enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
103
140
 
104
141
  try {
105
- return original.call(this, req, res, wrappedNext)
106
- } catch (error) {
107
- publishError({ req, error })
108
- nextChannel.publish({ req })
109
- finishChannel.publish({ req })
110
-
111
- throw error
142
+ return originalRequest.call(this, req, res, wrappedNext)
112
143
  } finally {
113
144
  exitChannel.publish({ req })
114
145
  }
115
146
  }
116
147
  }
117
148
 
118
- function errorHandlerLayerWrap (layer, name, captureRoute, needMultiMatch, matchers) {
119
- return original => function (error, req, res, next) {
120
- if (!enterChannel.hasSubscribers) return original.call(this, error, req, res, next)
149
+ function wrapLayerError (originalError) {
150
+ return function (error, req, res, next) {
151
+ if (!enterChannel.hasSubscribers) return originalError.call(this, error, req, res, next)
121
152
 
122
- const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
153
+ const meta = getLayerMeta(this)
154
+ if (meta === undefined || this.handle.length !== 4) return originalError.call(this, error, req, res, next)
123
155
 
124
- let route = captureRoute
125
- if (needMultiMatch) {
126
- for (const matcher of matchers) {
127
- if (matcher.regex?.test(layer.path)) {
128
- route = matcher.path
129
- break
130
- }
131
- }
156
+ const wrappedNext = typeof next === 'function' ? wrapNext(req, meta.name, next) : next
157
+ enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
158
+
159
+ try {
160
+ return originalError.call(this, error, req, res, wrappedNext)
161
+ } finally {
162
+ exitChannel.publish({ req })
132
163
  }
164
+ }
165
+ }
133
166
 
134
- enterChannel.publish({ name, req, route, layer })
167
+ // express <4.6.0 has no `Layer` prototype dispatch: the router invokes
168
+ // `layer.handle` directly and routes errors by its arity. There the handle is
169
+ // replaced in place, with the arity preserved so the host still routes
170
+ // correctly. Newer express, express 5, and the router package keep `handle`
171
+ // pristine and are traced through the prototype wraps above.
172
+ function wrapLegacyHandle (layer, original) {
173
+ // `annotateLayer` always runs first in `wrapStack`, so the captured meta is
174
+ // never undefined here (unlike the prototype wraps, where `this` can be any
175
+ // layer the host dispatches).
176
+ const meta = getLayerMeta(layer)
177
+ const wrapped = shimmer.wrapFunction(original, inner => function (...args) {
178
+ if (!enterChannel.hasSubscribers) return inner.apply(this, args)
179
+
180
+ const isErrorHandler = original.length === 4
181
+ const req = args[isErrorHandler ? 1 : 0]
182
+ const nextIndex = isErrorHandler ? 3 : 2
183
+ if (typeof args[nextIndex] === 'function') args[nextIndex] = wrapNext(req, meta.name, args[nextIndex])
184
+
185
+ enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, layer), layer })
135
186
 
136
187
  try {
137
- return original.call(this, error, req, res, wrappedNext)
138
- } catch (caught) {
139
- publishError({ req, error: caught })
188
+ return inner.apply(this, args)
189
+ } catch (error) {
190
+ // Unlike the prototype hosts, this router catches a synchronous throw
191
+ // outside the layer and calls its own `next(error)`, never `wrappedNext`.
192
+ // Mirror `wrapNext` here so the throwing layer still tags its error and
193
+ // finishes, rather than lingering on the stack until request finish.
194
+ publishError({ req, error })
140
195
  nextChannel.publish({ req })
141
196
  finishChannel.publish({ req })
142
197
 
143
- throw caught
198
+ throw error
144
199
  } finally {
145
200
  exitChannel.publish({ req })
146
201
  }
147
- }
202
+ })
203
+ Object.defineProperty(wrapped, 'length', { value: original.length, configurable: true })
204
+ return wrapped
148
205
  }
149
206
 
207
+ return { wrapLayerRequest, wrapLayerError, wrapLegacyHandle }
208
+ }
209
+
210
+ /**
211
+ * @param {{ handle_request?: unknown, handleRequest?: unknown }} layer
212
+ */
213
+ function hasLayerDispatch (layer) {
214
+ return typeof layer.handle_request === 'function' || typeof layer.handleRequest === 'function'
215
+ }
216
+
217
+ // TODO: Move this function to a shared file between Express and Router
218
+ /**
219
+ * @param {string} name Channel namespace (`apm:<name>:middleware:*`).
220
+ * @param {((pattern: string | RegExp) => RegExp | undefined) | undefined} compile
221
+ * Host-resolved path-to-regexp compile adapter, or undefined when the host
222
+ * instance ships no path-to-regexp. Captured here so each express/router
223
+ * instance keeps the dialect it actually loaded.
224
+ * @param {((layer: object, original: Function) => Function) | undefined} [wrapLegacyHandle]
225
+ * Fallback that replaces `layer.handle` for hosts without a `Layer` prototype
226
+ * dispatch (express <4.6.0). Omitted for hosts that always ship one.
227
+ */
228
+ function createWrapRouterMethod (name, compile, wrapLegacyHandle) {
229
+ const routeAddedChannel = channel(`apm:${name}:route:added`)
230
+
150
231
  function wrapStack (layers, matchers) {
151
232
  for (const layer of layers) {
152
- if (layer.__handle) { // express-async-errors
153
- layer.__handle = wrapLayerHandle(layer, layer.__handle, matchers)
154
- } else {
155
- layer.handle = wrapLayerHandle(layer, layer.handle, matchers)
156
- }
233
+ annotateLayer(layer, matchers)
157
234
 
158
- setLayerMatchers(layer, matchers)
235
+ if (wrapLegacyHandle !== undefined && !hasLayerDispatch(layer)) {
236
+ if (layer.__handle) { // express-async-errors
237
+ layer.__handle = wrapLegacyHandle(layer, layer.__handle)
238
+ } else {
239
+ layer.handle = wrapLegacyHandle(layer, layer.handle)
240
+ }
241
+ }
159
242
 
160
243
  if (layer.route) {
161
244
  for (const method of METHODS) {
@@ -169,21 +252,6 @@ function createWrapRouterMethod (name, compile) {
169
252
  }
170
253
  }
171
254
 
172
- function wrapNext (req, originalNext) {
173
- // Per layer dispatch, N per request. Named `next`/arity-1 mirrors the
174
- // router continuation so wrapCallback skips its name/length rewrite.
175
- return shimmer.wrapCallback(originalNext, original => function next (error) {
176
- if (error && error !== 'route' && error !== 'router') {
177
- publishError({ req, error })
178
- }
179
-
180
- nextChannel.publish({ req })
181
- finishChannel.publish({ req })
182
-
183
- original.apply(this, arguments)
184
- })
185
- }
186
-
187
255
  function extractMatchers (fn) {
188
256
  const arg = Array.isArray(fn) ? fn.flat(Infinity) : [fn]
189
257
 
@@ -304,6 +372,15 @@ addHook({ name: 'router', versions: ['>=1 <2'] }, Router => {
304
372
  return Router
305
373
  })
306
374
 
375
+ addHook({ name: 'router', file: 'lib/layer.js', versions: ['>=1 <2'] }, Layer => {
376
+ const { wrapLayerRequest, wrapLayerError } = createLayerDispatchWrappers('router')
377
+
378
+ shimmer.wrap(Layer.prototype, 'handle_request', wrapLayerRequest)
379
+ shimmer.wrap(Layer.prototype, 'handle_error', wrapLayerError)
380
+
381
+ return Layer
382
+ })
383
+
307
384
  const queryParserReadCh = channel('datadog:query:read:finish')
308
385
 
309
386
  addHook({ name: 'router', versions: ['>=2'] }, Router => {
@@ -365,6 +442,14 @@ function wrapHandleRequest (original) {
365
442
  addHook({
366
443
  name: 'router', file: 'lib/layer.js', versions: ['>=2'],
367
444
  }, Layer => {
445
+ const { wrapLayerRequest, wrapLayerError } = createLayerDispatchWrappers('router')
446
+
447
+ // `handleRequest` carries two concerns: the middleware dispatch span and the
448
+ // param-start publish (`wrapHandleRequest`). Wrap the dispatch first so it
449
+ // sits inner and param-start still fires before `middleware:enter`, matching
450
+ // the order from when the handle itself was wrapped.
451
+ shimmer.wrap(Layer.prototype, 'handleRequest', wrapLayerRequest)
452
+ shimmer.wrap(Layer.prototype, 'handleError', wrapLayerError)
368
453
  shimmer.wrap(Layer.prototype, 'handleRequest', wrapHandleRequest)
369
454
  return Layer
370
455
  })
@@ -404,4 +489,4 @@ addHook({
404
489
  return router
405
490
  })
406
491
 
407
- module.exports = { createWrapRouterMethod }
492
+ module.exports = { createWrapRouterMethod, createLayerDispatchWrappers }
@@ -5,6 +5,10 @@ const realSetTimeout = setTimeout
5
5
 
6
6
  const shimmer = require('../../datadog-shimmer')
7
7
  const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
8
+ const {
9
+ RUM_TEST_EXECUTION_ID_COOKIE_NAME: DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME,
10
+ } = require('../../dd-trace/src/ci-visibility/rum')
11
+ const log = require('../../dd-trace/src/log')
8
12
  const { addHook, channel } = require('./helpers/instrument')
9
13
 
10
14
  const ciSeleniumDriverGetStartCh = channel('ci:selenium:driver:get')
@@ -20,7 +24,6 @@ if (window.DD_RUM && window.DD_RUM.stopSession) {
20
24
  const IS_RUM_ACTIVE_SCRIPT = 'return !!window.DD_RUM'
21
25
 
22
26
  const DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS = getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')
23
- const DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME = 'datadog-ci-visibility-test-execution-id'
24
27
 
25
28
  // TODO: can we increase the supported version range?
26
29
  addHook({
@@ -32,28 +35,47 @@ addHook({
32
35
  if (!ciSeleniumDriverGetStartCh.hasSubscribers) {
33
36
  return get.apply(this, arguments)
34
37
  }
35
- let traceId
36
- const setTraceId = (inputTraceId) => {
37
- traceId = inputTraceId
38
- }
39
38
  const getResult = await get.apply(this, arguments)
40
39
 
41
- const isRumActive = await this.executeScript(IS_RUM_ACTIVE_SCRIPT)
42
- const capabilities = await this.getCapabilities()
40
+ let isRumActive
41
+ try {
42
+ isRumActive = await this.executeScript(IS_RUM_ACTIVE_SCRIPT)
43
+ } catch (error) {
44
+ log.error('Selenium RUM detection error', error)
45
+ }
46
+
47
+ let browserName
48
+ let browserVersion
49
+ try {
50
+ const capabilities = await this.getCapabilities()
51
+ browserName = capabilities.getBrowserName()
52
+ browserVersion = capabilities.getBrowserVersion()
53
+ } catch (error) {
54
+ log.error('Selenium browser metadata error', error)
55
+ }
43
56
 
44
- ciSeleniumDriverGetStartCh.publish({
45
- setTraceId,
57
+ const context = {
46
58
  seleniumVersion,
47
- browserName: capabilities.getBrowserName(),
48
- browserVersion: capabilities.getBrowserVersion(),
59
+ browserName,
60
+ browserVersion,
49
61
  isRumActive,
50
- })
62
+ testExecutionId: undefined,
63
+ }
64
+ try {
65
+ ciSeleniumDriverGetStartCh.publish(context)
66
+ } catch (error) {
67
+ log.error('Selenium RUM correlation channel error', error)
68
+ }
51
69
 
52
- if (traceId && isRumActive) {
53
- await this.manage().addCookie({
54
- name: DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME,
55
- value: traceId,
56
- })
70
+ if (context.testExecutionId && isRumActive) {
71
+ try {
72
+ await this.manage().addCookie({
73
+ name: DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME,
74
+ value: context.testExecutionId,
75
+ })
76
+ } catch (error) {
77
+ log.error('Selenium RUM correlation cookie error', error)
78
+ }
57
79
  }
58
80
 
59
81
  return getResult
@@ -63,16 +85,20 @@ addHook({
63
85
  if (!ciSeleniumDriverGetStartCh.hasSubscribers) {
64
86
  return quit.apply(this, arguments)
65
87
  }
66
- const isRumActive = await this.executeScript(RUM_STOP_SESSION_SCRIPT)
88
+ try {
89
+ const isRumActive = await this.executeScript(RUM_STOP_SESSION_SCRIPT)
67
90
 
68
- if (isRumActive) {
69
- // We'll have time for RUM to flush the events (there's no callback to know when it's done)
70
- await new Promise(resolve => {
71
- realSetTimeout(() => {
72
- resolve()
73
- }, DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS)
74
- })
75
- await this.manage().deleteCookie(DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME)
91
+ if (isRumActive) {
92
+ // We'll have time for RUM to flush the events (there's no callback to know when it's done)
93
+ await new Promise(resolve => {
94
+ realSetTimeout(() => {
95
+ resolve()
96
+ }, DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS)
97
+ })
98
+ await this.manage().deleteCookie(DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME)
99
+ }
100
+ } catch (error) {
101
+ log.error('Selenium RUM cleanup error', error)
76
102
  }
77
103
 
78
104
  return quit.apply(this, arguments)
@@ -784,7 +784,7 @@ function getRepeatedTestReport (task, testName, testSuiteAbsolutePath, testPrope
784
784
 
785
785
  return {
786
786
  errors,
787
- finalAttempt: attempts[attempts.length - 1],
787
+ finalAttempt: attempts.at(-1),
788
788
  nonFinalAttempts: attempts.slice(0, -1),
789
789
  state,
790
790
  status,
@@ -880,7 +880,7 @@ function reportFinalTestAttempt (testReport) {
880
880
  const finalAttempt = testReport.finalAttempt
881
881
 
882
882
  if (status === 'fail') {
883
- const error = errors[errors.length - 1] || errors[0]
883
+ const error = errors.at(-1) || errors[0]
884
884
  reportTestAttempt(testReport, finalAttempt || {
885
885
  error,
886
886
  finalStatus,
@@ -1343,10 +1343,8 @@ function wrapTinyPoolRun (TinyPool) {
1343
1343
 
1344
1344
  shimmer.wrap(TinyPool.prototype, 'run', run => async function () {
1345
1345
  // We have to do this before and after because the threads list gets recycled, that is, the processes are re-created
1346
- // eslint-disable-next-line unicorn/no-array-for-each
1347
1346
  this.threads.forEach(threadHandler)
1348
1347
  const runResult = await run.apply(this, arguments)
1349
- // eslint-disable-next-line unicorn/no-array-for-each
1350
1348
  this.threads.forEach(threadHandler)
1351
1349
  return runResult
1352
1350
  })
@@ -3,6 +3,7 @@
3
3
  const { tracingChannel } = /** @type {import('node:diagnostics_channel')} */ (require('dc-polyfill'))
4
4
 
5
5
  const shimmer = require('../../datadog-shimmer')
6
+ const { getSegment } = require('../../dd-trace/src/util')
6
7
  const {
7
8
  addHook,
8
9
  channel,
@@ -106,7 +107,7 @@ function createWrapEmit (emit) {
106
107
  }
107
108
 
108
109
  const ctx = { req }
109
- ctx.req.resStatus = headers[0].split(' ')[1]
110
+ ctx.req.resStatus = getSegment(headers[0], ' ', 1)
110
111
 
111
112
  emitCh.runStores(ctx, () => {
112
113
  try {
@@ -94,7 +94,7 @@ class RunInChildContextPlugin extends BaseContextPlugin {
94
94
  static retryable = false
95
95
 
96
96
  bindStart (ctx) {
97
- if (SUPPRESSED_CHILD_CONTEXT_SUBTYPES.has(getRunInChildContextSubType(ctx))) {
97
+ if (SUPPRESSED_CHILD_CONTEXT_SUBTYPES.has(getRunInChildContextSubtype(ctx))) {
98
98
  // Pass the active store through unchanged so any nested spans
99
99
  // remain parented to the surrounding map/parallel span
100
100
  ctx.suppressed = true
@@ -105,7 +105,7 @@ class RunInChildContextPlugin extends BaseContextPlugin {
105
105
  }
106
106
 
107
107
  // runInChildContext has two overloads: `(name, fn, options)` and `(fn, options)`.
108
- function getRunInChildContextSubType (ctx) {
108
+ function getRunInChildContextSubtype (ctx) {
109
109
  const args = ctx.arguments || []
110
110
  const opts = typeof args[0] === 'function' ? args[1] : args[2]
111
111
  return opts?.subType
@@ -16,7 +16,8 @@ const REPLAYED_STATUSES = new Set(['SUCCEEDED', 'FAILED'])
16
16
  * @returns {{ stepId: string | undefined, stepData: object | undefined }}
17
17
  */
18
18
  function getStepDataForNext (ctxImpl) {
19
- const stepId = ctxImpl?.getNextStepId?.()
19
+ // @aws/durable-execution-sdk-js@2.2.0 renames .getNextStepId() to .peekStepId()
20
+ const stepId = ctxImpl?.peekStepId?.() ?? ctxImpl?.getNextStepId?.()
20
21
  const stepData = stepId ? ctxImpl?._executionContext?.getStepData?.(stepId) : undefined
21
22
  return { stepId, stepData }
22
23
  }
@@ -2,7 +2,6 @@
2
2
  const { DsmPathwayCodec, getSizeOrZero } = require('../../../dd-trace/src/datastreams')
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
- const { isEmpty } = require('../util')
6
5
 
7
6
  function recordDataAsString (data) {
8
7
  return Buffer.isBuffer(data) ? data.toString('utf8') : Buffer.from(data).toString('utf8')
@@ -13,6 +12,16 @@ function recordDataAsString (data) {
13
12
  // delete on consume, so their working set is ~the active shard count.
14
13
  const MAX_TRACKED_SHARD_ITERATORS = 1000
15
14
 
15
+ // The default Kinesis record limit is 1 MiB. Streams configured for larger records
16
+ // conservatively skip propagation above this point because the request does not expose that limit.
17
+ const KINESIS_DEFAULT_MAX_RECORD_BYTES = 1_048_576
18
+
19
+ // The DSM pathway field (`dd-pathway-ctx-base64`) always serializes to a fixed 55 bytes: a
20
+ // 21-char key, a 28-char base64 value, and 6 bytes of JSON framing. Mirrors PATHWAY_HEADER_BYTES
21
+ // in dd-trace/src/datastreams/processor.js. Reserved in the size gate so setDSMCheckpoint never
22
+ // runs for a record that would ship over the cap once the pathway context is attached.
23
+ const DSM_PATHWAY_FIELD_BYTES = 55
24
+
16
25
  class Kinesis extends BaseAwsSdkPlugin {
17
26
  static id = 'kinesis'
18
27
  static peerServicePrecursors = ['streamname']
@@ -237,6 +246,12 @@ class Kinesis extends BaseAwsSdkPlugin {
237
246
  }
238
247
  }
239
248
 
249
+ /**
250
+ * @param {import('../../../dd-trace/src/opentracing/span') | null} span
251
+ * @param {{ Data: Buffer|string|Uint8Array, PartitionKey?: string } | undefined} params
252
+ * @param {string} stream
253
+ * @param {boolean} injectTraceContext
254
+ */
240
255
  injectToMessage (span, params, stream, injectTraceContext) {
241
256
  if (!params) {
242
257
  return
@@ -251,30 +266,40 @@ class Kinesis extends BaseAwsSdkPlugin {
251
266
  }
252
267
  }
253
268
 
254
- const ddInfo = {}
269
+ let ddInfo
255
270
  // For now we only inject to the first message; batches may change later.
256
271
  if (injectTraceContext) {
257
- this.tracer.inject(span, 'text_map', ddInfo)
272
+ ddInfo = this.tracer.inject(span, 'text_map')
258
273
  }
259
274
 
260
- if (this.config.dsmEnabled) {
261
- parsedData._datadog = ddInfo
275
+ const dsmEnabled = this.config.dsmEnabled
276
+ if (!ddInfo && !dsmEnabled) return
277
+
278
+ parsedData._datadog = ddInfo ?? {}
279
+ // Gate before setDSMCheckpoint: a record we can't ship must not record a checkpoint.
280
+ // Reserve the pathway field that DSM appends after the gate.
281
+ let serialized = JSON.stringify(parsedData)
282
+ const reservedBytes = dsmEnabled ? DSM_PATHWAY_FIELD_BYTES : 0
283
+ const partitionKeyBytes = Buffer.byteLength(params.PartitionKey ?? '', 'utf8')
284
+ if (
285
+ Buffer.byteLength(serialized, 'utf8') + partitionKeyBytes + reservedBytes >
286
+ KINESIS_DEFAULT_MAX_RECORD_BYTES
287
+ ) {
288
+ log.info('Payload size too large to pass context')
289
+ return
290
+ }
291
+
292
+ if (dsmEnabled) {
262
293
  const dataStreamsContext = this.setDSMCheckpoint(span, params, stream)
263
- if (dataStreamsContext) {
264
- DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
294
+ ddInfo = DsmPathwayCodec.encode(dataStreamsContext, ddInfo) ?? ddInfo
295
+ if (ddInfo) {
296
+ parsedData._datadog = ddInfo
297
+ serialized = JSON.stringify(parsedData)
265
298
  }
266
299
  }
267
300
 
268
- if (isEmpty(ddInfo)) return
301
+ if (!ddInfo) return
269
302
 
270
- parsedData._datadog = ddInfo
271
- const serialized = JSON.stringify(parsedData)
272
- const byteSize = Buffer.byteLength(serialized, 'utf8')
273
- // Kinesis max payload size is 1 MiB; bail if our context push tipped us over.
274
- if (byteSize >= 1_048_576) {
275
- log.info('Payload size too large to pass context')
276
- return
277
- }
278
303
  params.Data = Buffer.from(serialized, 'utf8')
279
304
  }
280
305