dd-trace 6.2.0 → 6.4.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 (76) hide show
  1. package/README.md +1 -1
  2. package/index.electron.js +3 -0
  3. package/package.json +8 -5
  4. package/packages/datadog-instrumentations/src/ai.js +29 -24
  5. package/packages/datadog-instrumentations/src/cookie.js +7 -1
  6. package/packages/datadog-instrumentations/src/cucumber.js +4 -3
  7. package/packages/datadog-instrumentations/src/express.js +20 -2
  8. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -1
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +30 -54
  11. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  12. package/packages/datadog-instrumentations/src/http/server.js +27 -0
  13. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  14. package/packages/datadog-instrumentations/src/jest.js +10 -4
  15. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +248 -1
  16. package/packages/datadog-instrumentations/src/playwright.js +40 -3
  17. package/packages/datadog-instrumentations/src/router.js +180 -105
  18. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  19. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  20. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  21. package/packages/datadog-plugin-aws-sdk/src/util.js +0 -22
  22. package/packages/datadog-plugin-azure-functions/src/index.js +2 -1
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +5 -5
  24. package/packages/datadog-plugin-graphql/src/execute.js +90 -14
  25. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  26. package/packages/datadog-plugin-graphql/src/request.js +33 -2
  27. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  28. package/packages/datadog-plugin-graphql/src/validate.js +13 -1
  29. package/packages/datadog-plugin-http2/src/server.js +38 -6
  30. package/packages/datadog-plugin-jest/src/util.js +21 -6
  31. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +144 -26
  32. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/utils.js +30 -0
  33. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  34. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  35. package/packages/datadog-shimmer/src/shimmer.js +9 -2
  36. package/packages/dd-trace/index.electron.js +3 -0
  37. package/packages/dd-trace/index.js +2 -37
  38. package/packages/dd-trace/src/appsec/channels.js +1 -0
  39. package/packages/dd-trace/src/appsec/index.js +6 -3
  40. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  41. package/packages/dd-trace/src/bootstrap.js +39 -0
  42. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +23 -16
  43. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +14 -2
  44. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  45. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  46. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +55 -5
  47. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +8 -1
  48. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  49. package/packages/dd-trace/src/config/config-types.d.ts +8 -0
  50. package/packages/dd-trace/src/config/defaults.js +20 -15
  51. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  52. package/packages/dd-trace/src/config/index.js +13 -3
  53. package/packages/dd-trace/src/config/parsers.js +79 -0
  54. package/packages/dd-trace/src/config/supported-configurations.json +17 -0
  55. package/packages/dd-trace/src/constants.js +7 -0
  56. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  57. package/packages/dd-trace/src/feature-registry.js +22 -0
  58. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  59. package/packages/dd-trace/src/llmobs/index.js +10 -2
  60. package/packages/dd-trace/src/llmobs/tagger.js +19 -1
  61. package/packages/dd-trace/src/noop/proxy.js +5 -4
  62. package/packages/dd-trace/src/openfeature/register.js +46 -0
  63. package/packages/dd-trace/src/opentelemetry/span_processor.js +7 -5
  64. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +32 -16
  65. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  66. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  67. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  68. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  69. package/packages/dd-trace/src/plugins/util/test.js +9 -4
  70. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  71. package/packages/dd-trace/src/proxy.js +21 -9
  72. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -11
  73. package/packages/dd-trace/src/standalone/index.js +0 -22
  74. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  75. package/vendor/dist/protobufjs/index.js +1 -1
  76. package/vendor/dist/protobufjs/minimal/index.js +1 -1
@@ -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,209 @@ 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`)
43
89
  // Bound per name so express and a bare router keep independent guards.
44
90
  const publishError = createErrorPublisher(errorChannel)
45
91
 
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
- }
92
+ function wrapNext (req, originalNext) {
93
+ // Per layer dispatch, N per request. Named `next`/arity-1 mirrors the
94
+ // router continuation so wrapCallback skips its name/length rewrite.
95
+ let published = false
96
+ return shimmer.wrapCallback(originalNext, original => function next (error) {
97
+ // A handler that calls `next()` and then rejects (`next(); await bg()`)
98
+ // makes the host call this continuation twice. Publish once so the second
99
+ // pass cannot tag the already-finished span's parent with a late error.
100
+ if (!published) {
101
+ published = true
102
+
103
+ if (error && error !== 'route' && error !== 'router') {
104
+ publishError({ req, error })
105
+ }
64
106
 
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
107
+ nextChannel.publish({ req })
108
+ finishChannel.publish({ req })
109
+ }
82
110
 
83
- return wrapped
111
+ original.apply(this, arguments)
112
+ })
84
113
  }
85
114
 
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)
115
+ // Every host dispatch turns a synchronous throw into `next(error)`, and the
116
+ // hosts that await the handler (router >=2, express 5, express 4 +
117
+ // express-async-errors) do the same for a rejected promise. Passing
118
+ // `wrappedNext` through captures both without a tracer-side try/catch; only
119
+ // `exit` needs the `finally`. express 4's native dispatch converts only the
120
+ // synchronous throw — exactly what the pre-refactor handle wrap caught.
121
+ function wrapLayerRequest (originalRequest) {
122
+ return function (req, res, next) {
123
+ if (!enterChannel.hasSubscribers) return originalRequest.call(this, req, res, next)
89
124
 
90
- const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
125
+ const meta = getLayerMeta(this)
126
+ if (meta === undefined || this.handle.length > 3) return originalRequest.call(this, req, res, next)
91
127
 
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
- }
101
-
102
- enterChannel.publish({ name, req, route, layer })
128
+ const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
129
+ enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
103
130
 
104
131
  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
132
+ return originalRequest.call(this, req, res, wrappedNext)
112
133
  } finally {
113
134
  exitChannel.publish({ req })
114
135
  }
115
136
  }
116
137
  }
117
138
 
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)
139
+ function wrapLayerError (originalError) {
140
+ return function (error, req, res, next) {
141
+ if (!enterChannel.hasSubscribers) return originalError.call(this, error, req, res, next)
142
+
143
+ const meta = getLayerMeta(this)
144
+ if (meta === undefined || this.handle.length !== 4) return originalError.call(this, error, req, res, next)
121
145
 
122
146
  const wrappedNext = typeof next === 'function' ? wrapNext(req, next) : next
147
+ enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, this), layer: this })
123
148
 
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
- }
149
+ try {
150
+ return originalError.call(this, error, req, res, wrappedNext)
151
+ } finally {
152
+ exitChannel.publish({ req })
132
153
  }
154
+ }
155
+ }
133
156
 
134
- enterChannel.publish({ name, req, route, layer })
157
+ // express <4.6.0 has no `Layer` prototype dispatch: the router invokes
158
+ // `layer.handle` directly and routes errors by its arity. There the handle is
159
+ // replaced in place, with the arity preserved so the host still routes
160
+ // correctly. Newer express, express 5, and the router package keep `handle`
161
+ // pristine and are traced through the prototype wraps above.
162
+ function wrapLegacyHandle (layer, original) {
163
+ // `annotateLayer` always runs first in `wrapStack`, so the captured meta is
164
+ // never undefined here (unlike the prototype wraps, where `this` can be any
165
+ // layer the host dispatches).
166
+ const meta = getLayerMeta(layer)
167
+ const wrapped = shimmer.wrapFunction(original, inner => function (...args) {
168
+ if (!enterChannel.hasSubscribers) return inner.apply(this, args)
169
+
170
+ const isErrorHandler = original.length === 4
171
+ const req = args[isErrorHandler ? 1 : 0]
172
+ const nextIndex = isErrorHandler ? 3 : 2
173
+ if (typeof args[nextIndex] === 'function') args[nextIndex] = wrapNext(req, args[nextIndex])
174
+
175
+ enterChannel.publish({ name: meta.name, req, route: resolveLayerRoute(meta, layer), layer })
135
176
 
136
177
  try {
137
- return original.call(this, error, req, res, wrappedNext)
138
- } catch (caught) {
139
- publishError({ req, error: caught })
178
+ return inner.apply(this, args)
179
+ } catch (error) {
180
+ // Unlike the prototype hosts, this router catches a synchronous throw
181
+ // outside the layer and calls its own `next(error)`, never `wrappedNext`.
182
+ // Mirror `wrapNext` here so the throwing layer still tags its error and
183
+ // finishes, rather than lingering on the stack until request finish.
184
+ publishError({ req, error })
140
185
  nextChannel.publish({ req })
141
186
  finishChannel.publish({ req })
142
187
 
143
- throw caught
188
+ throw error
144
189
  } finally {
145
190
  exitChannel.publish({ req })
146
191
  }
147
- }
192
+ })
193
+ Object.defineProperty(wrapped, 'length', { value: original.length, configurable: true })
194
+ return wrapped
148
195
  }
149
196
 
197
+ return { wrapLayerRequest, wrapLayerError, wrapLegacyHandle }
198
+ }
199
+
200
+ /**
201
+ * @param {{ handle_request?: unknown, handleRequest?: unknown }} layer
202
+ */
203
+ function hasLayerDispatch (layer) {
204
+ return typeof layer.handle_request === 'function' || typeof layer.handleRequest === 'function'
205
+ }
206
+
207
+ // TODO: Move this function to a shared file between Express and Router
208
+ /**
209
+ * @param {string} name Channel namespace (`apm:<name>:middleware:*`).
210
+ * @param {((pattern: string | RegExp) => RegExp | undefined) | undefined} compile
211
+ * Host-resolved path-to-regexp compile adapter, or undefined when the host
212
+ * instance ships no path-to-regexp. Captured here so each express/router
213
+ * instance keeps the dialect it actually loaded.
214
+ * @param {((layer: object, original: Function) => Function) | undefined} [wrapLegacyHandle]
215
+ * Fallback that replaces `layer.handle` for hosts without a `Layer` prototype
216
+ * dispatch (express <4.6.0). Omitted for hosts that always ship one.
217
+ */
218
+ function createWrapRouterMethod (name, compile, wrapLegacyHandle) {
219
+ const routeAddedChannel = channel(`apm:${name}:route:added`)
220
+
150
221
  function wrapStack (layers, matchers) {
151
222
  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
- }
223
+ annotateLayer(layer, matchers)
157
224
 
158
- setLayerMatchers(layer, matchers)
225
+ if (wrapLegacyHandle !== undefined && !hasLayerDispatch(layer)) {
226
+ if (layer.__handle) { // express-async-errors
227
+ layer.__handle = wrapLegacyHandle(layer, layer.__handle)
228
+ } else {
229
+ layer.handle = wrapLegacyHandle(layer, layer.handle)
230
+ }
231
+ }
159
232
 
160
233
  if (layer.route) {
161
234
  for (const method of METHODS) {
@@ -169,21 +242,6 @@ function createWrapRouterMethod (name, compile) {
169
242
  }
170
243
  }
171
244
 
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
245
  function extractMatchers (fn) {
188
246
  const arg = Array.isArray(fn) ? fn.flat(Infinity) : [fn]
189
247
 
@@ -304,6 +362,15 @@ addHook({ name: 'router', versions: ['>=1 <2'] }, Router => {
304
362
  return Router
305
363
  })
306
364
 
365
+ addHook({ name: 'router', file: 'lib/layer.js', versions: ['>=1 <2'] }, Layer => {
366
+ const { wrapLayerRequest, wrapLayerError } = createLayerDispatchWrappers('router')
367
+
368
+ shimmer.wrap(Layer.prototype, 'handle_request', wrapLayerRequest)
369
+ shimmer.wrap(Layer.prototype, 'handle_error', wrapLayerError)
370
+
371
+ return Layer
372
+ })
373
+
307
374
  const queryParserReadCh = channel('datadog:query:read:finish')
308
375
 
309
376
  addHook({ name: 'router', versions: ['>=2'] }, Router => {
@@ -365,6 +432,14 @@ function wrapHandleRequest (original) {
365
432
  addHook({
366
433
  name: 'router', file: 'lib/layer.js', versions: ['>=2'],
367
434
  }, Layer => {
435
+ const { wrapLayerRequest, wrapLayerError } = createLayerDispatchWrappers('router')
436
+
437
+ // `handleRequest` carries two concerns: the middleware dispatch span and the
438
+ // param-start publish (`wrapHandleRequest`). Wrap the dispatch first so it
439
+ // sits inner and param-start still fires before `middleware:enter`, matching
440
+ // the order from when the handle itself was wrapped.
441
+ shimmer.wrap(Layer.prototype, 'handleRequest', wrapLayerRequest)
442
+ shimmer.wrap(Layer.prototype, 'handleError', wrapLayerError)
368
443
  shimmer.wrap(Layer.prototype, 'handleRequest', wrapHandleRequest)
369
444
  return Layer
370
445
  })
@@ -404,4 +479,4 @@ addHook({
404
479
  return router
405
480
  })
406
481
 
407
- module.exports = { createWrapRouterMethod }
482
+ module.exports = { createWrapRouterMethod, createLayerDispatchWrappers }
@@ -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
 
@@ -2,7 +2,6 @@
2
2
  const { DsmPathwayCodec, getHeadersSize } = 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
  class Sns extends BaseAwsSdkPlugin {
8
7
  static id = 'sns'
@@ -71,6 +70,12 @@ class Sns extends BaseAwsSdkPlugin {
71
70
  }
72
71
  }
73
72
 
73
+ /**
74
+ * @param {import('../../../dd-trace/src/opentracing/span') | null} span
75
+ * @param {{ Message?: string, MessageAttributes?: Record<string, object> }} params
76
+ * @param {string | undefined} topicArn
77
+ * @param {boolean} injectTraceContext
78
+ */
74
79
  injectToMessage (span, params, topicArn, injectTraceContext) {
75
80
  if (!params.MessageAttributes) {
76
81
  params.MessageAttributes = {}
@@ -79,37 +84,32 @@ class Sns extends BaseAwsSdkPlugin {
79
84
  return
80
85
  }
81
86
 
82
- const ddInfo = {}
87
+ let ddInfo
83
88
  // for now, we only want to inject to the first message, this may change for batches in the future
84
89
  if (injectTraceContext) {
85
- this.tracer.inject(span, 'text_map', ddInfo)
86
- // add ddInfo before checking DSM so we can include DD attributes in payload size
87
- params.MessageAttributes._datadog = {
88
- DataType: 'Binary',
89
- BinaryValue: ddInfo,
90
- }
90
+ ddInfo = this.tracer.inject(span, 'text_map')
91
91
  }
92
92
 
93
93
  if (this.config.dsmEnabled) {
94
- if (!params.MessageAttributes._datadog) {
95
- params.MessageAttributes._datadog = {
96
- DataType: 'Binary',
97
- BinaryValue: ddInfo,
98
- }
94
+ // Add the placeholder before the checkpoint so its payload size includes DD attributes.
95
+ params.MessageAttributes._datadog = {
96
+ DataType: 'Binary',
97
+ BinaryValue: ddInfo ?? {},
99
98
  }
100
99
 
101
100
  const dataStreamsContext = this.setDSMCheckpoint(span, params, topicArn)
102
- DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
101
+ ddInfo = DsmPathwayCodec.encode(dataStreamsContext, ddInfo) ?? ddInfo
103
102
  }
104
103
 
105
- if (isEmpty(ddInfo)) {
106
- if (params.MessageAttributes._datadog) {
107
- // let's avoid adding any additional information to payload if we failed to inject
108
- delete params.MessageAttributes._datadog
109
- }
110
- } else {
104
+ if (ddInfo) {
111
105
  // BINARY types are automatically base64 encoded
112
- params.MessageAttributes._datadog.BinaryValue = Buffer.from(JSON.stringify(ddInfo))
106
+ params.MessageAttributes._datadog = {
107
+ DataType: 'Binary',
108
+ BinaryValue: Buffer.from(JSON.stringify(ddInfo)),
109
+ }
110
+ } else if (params.MessageAttributes._datadog) {
111
+ // let's avoid adding any additional information to payload if we failed to inject
112
+ delete params.MessageAttributes._datadog
113
113
  }
114
114
  }
115
115
 
@@ -3,7 +3,7 @@
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
5
  const { DsmPathwayCodec, getHeadersSize } = require('../../../dd-trace/src/datastreams')
6
- const { extractQueueMetadata, isEmpty } = require('../util')
6
+ const { extractQueueMetadata } = require('../util')
7
7
 
8
8
  /**
9
9
  * @typedef {{
@@ -322,6 +322,12 @@ class Sqs extends BaseAwsSdkPlugin {
322
322
  }
323
323
  }
324
324
 
325
+ /**
326
+ * @param {import('../../../dd-trace/src/opentracing/span') | null} span
327
+ * @param {{ MessageBody?: string, MessageAttributes?: Record<string, object> } | undefined} params
328
+ * @param {string} queueUrl
329
+ * @param {boolean} injectTraceContext
330
+ */
325
331
  injectToMessage (span, params, queueUrl, injectTraceContext) {
326
332
  if (!params) {
327
333
  params = {}
@@ -333,10 +339,10 @@ class Sqs extends BaseAwsSdkPlugin {
333
339
  return
334
340
  }
335
341
 
336
- const ddInfo = {}
342
+ let ddInfo
337
343
  // For now we only inject to the first message; batches may change later.
338
344
  if (injectTraceContext) {
339
- this.tracer.inject(span, 'text_map', ddInfo)
345
+ ddInfo = this.tracer.inject(span, 'text_map')
340
346
  }
341
347
 
342
348
  if (this.config.dsmEnabled) {
@@ -344,19 +350,19 @@ class Sqs extends BaseAwsSdkPlugin {
344
350
  // matches the on-wire payload, then update with the encoded context.
345
351
  params.MessageAttributes._datadog = {
346
352
  DataType: 'String',
347
- StringValue: JSON.stringify(ddInfo),
353
+ StringValue: JSON.stringify(ddInfo ?? {}),
348
354
  }
349
355
  const dataStreamsContext = this.setDSMCheckpoint(span, params, queueUrl)
350
- if (dataStreamsContext) {
351
- DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
356
+ ddInfo = DsmPathwayCodec.encode(dataStreamsContext, ddInfo) ?? ddInfo
357
+ if (ddInfo) {
352
358
  params.MessageAttributes._datadog.StringValue = JSON.stringify(ddInfo)
353
- } else if (isEmpty(ddInfo)) {
359
+ } else {
354
360
  delete params.MessageAttributes._datadog
355
361
  }
356
362
  return
357
363
  }
358
364
 
359
- if (isEmpty(ddInfo)) return
365
+ if (!ddInfo) return
360
366
 
361
367
  params.MessageAttributes._datadog = {
362
368
  DataType: 'String',
@@ -143,31 +143,9 @@ const extractQueueMetadata = queueURL => {
143
143
  return { queueName, arn }
144
144
  }
145
145
 
146
- /**
147
- * Returns true when `obj` has no own enumerable properties. The
148
- * `for-in` loop with an early return is the only allocation-free shape
149
- * for this check; benchmarks pin it as 1.3-1.4x faster than
150
- * `Object.keys(obj).length === 0` across small / medium / large
151
- * objects, and this is the hot path on every AWS messaging send.
152
- *
153
- * Callers in this package only pass plain objects they construct
154
- * locally, so prototype-enumerable keys are not a concern here. Do not
155
- * reuse this helper on caller-supplied objects without revisiting that
156
- * assumption.
157
- *
158
- * @param {object} obj
159
- * @returns {boolean}
160
- */
161
- const isEmpty = obj => {
162
- // eslint-disable-next-line no-unreachable-loop
163
- for (const _ in obj) return false
164
- return true
165
- }
166
-
167
146
  module.exports = {
168
147
  generatePointerHash,
169
148
  encodeValue,
170
149
  extractPrimaryKeys,
171
150
  extractQueueMetadata,
172
- isEmpty,
173
151
  }
@@ -65,6 +65,7 @@ class AzureFunctionsPlugin extends TracingPlugin {
65
65
  service: this.serviceName(),
66
66
  type: 'serverless',
67
67
  meta,
68
+ childOf: null,
68
69
  }, ctx)
69
70
 
70
71
  if (isMessagingService) {
@@ -142,7 +143,7 @@ function mapTriggerTag (methodName) {
142
143
  // message & messages & batch with cardinality of 1 == applicationProperties
143
144
  // messages with cardinality of many == applicationPropertiesArray
144
145
  function setSpanLinks (triggerType, tracer, span, ctx) {
145
- const cardinality = ctx.invocationContext.options.trigger.cardinality
146
+ const cardinality = ctx.invocationContext.options.trigger.cardinality ?? 'one'
146
147
  const triggerMetadata = ctx.invocationContext.triggerMetadata
147
148
  const isServiceBus = triggerType === 'ServiceBus'
148
149
 
@@ -1835,13 +1835,13 @@ class CypressPlugin {
1835
1835
  }
1836
1836
  this.activeTestSpan.setTag(TEST_STATUS, testStatus)
1837
1837
 
1838
- // Save the test status to know if it has passed all retries
1839
- if (this.testStatuses[testName]) {
1840
- this.testStatuses[testName].push(testStatus)
1838
+ const testIdentifier = `${testSuite}\0${testName}`
1839
+ let testStatuses = this.testStatuses[testIdentifier]
1840
+ if (testStatuses) {
1841
+ testStatuses.push(testStatus)
1841
1842
  } else {
1842
- this.testStatuses[testName] = [testStatus]
1843
+ testStatuses = this.testStatuses[testIdentifier] = [testStatus]
1843
1844
  }
1844
- const testStatuses = this.testStatuses[testName]
1845
1845
  const activeSpanTags = this.activeTestSpan.context().getTags()
1846
1846
 
1847
1847
  if (error) {