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
@@ -5,7 +5,7 @@ const dc = require('dc-polyfill')
5
5
  const { storage } = require('../../datadog-core')
6
6
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
7
7
  const GraphQLParsePlugin = require('./parse')
8
- const { extractErrorIntoSpanEvent, getOperation, getSignature } = require('./utils')
8
+ const { extractErrorIntoSpanEvent, getOperation, getSignature, isApolloHealthCheck } = require('./utils')
9
9
 
10
10
  const legacyStorage = storage('legacy')
11
11
 
@@ -26,12 +26,24 @@ const contexts = new WeakMap()
26
26
  const instrumentedArgs = new WeakSet()
27
27
 
28
28
  const patchedResolvers = new WeakSet()
29
- const patchedTypes = new WeakSet()
29
+
30
+ // Visited types per caller-owned schema. The walk reaches union members and
31
+ // interface implementations through the schema (`getTypes`/`getPossibleTypes`),
32
+ // so it differs per schema: a global guard would stop the second schema at any
33
+ // type the first already walked and leave its own implementations unwrapped.
34
+ // `patchedResolvers` keeps wrapping idempotent, so re-walking a shared type is
35
+ // safe and this set only terminates cycles.
36
+ const walkedTypes = new WeakMap()
30
37
 
31
38
  // Module-level fast path: skip the resolver-side WeakMap lookup entirely
32
39
  // when depth=0 disables resolver instrumentation.
33
40
  let depthDisabled = false
34
41
 
42
+ // Initial key for the per-operation variables-filter cache. A unique sentinel
43
+ // so the first #filterVariables call never falsely matches, even when the
44
+ // operation's variableValues is undefined.
45
+ const NO_VARIABLES_CACHED = Symbol('noVariablesCached')
46
+
35
47
  class AbortError extends Error {
36
48
  constructor (message) {
37
49
  super(message)
@@ -111,6 +123,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
111
123
  const name = operation?.name?.value
112
124
  const source = this.config.source && docSource
113
125
 
126
+ // Apollo Server may execute a cached document without parsing it first.
127
+ // Match the full gateway operation here so caller-owned AST transformations
128
+ // cannot suppress execute/resolver AppSec and IAST channels.
129
+ if (name === '__ApolloServiceHealthCheck__' &&
130
+ document.definitions.length === 1 &&
131
+ isApolloHealthCheck(operation)) {
132
+ ctx.ddSkipped = true
133
+ return ctx.currentStore
134
+ }
135
+
114
136
  ctx.collapse = this.config.collapse
115
137
 
116
138
  const signature = getSignature(document, name, type, this.config.signature)
@@ -165,9 +187,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
165
187
 
166
188
  const schema = args.schema
167
189
  if (schema) {
168
- wrapFields(schema._queryType)
169
- wrapFields(schema._mutationType)
170
- wrapFields(schema._subscriptionType)
190
+ wrapFields(schema._queryType, schema)
191
+ wrapFields(schema._mutationType, schema)
192
+ wrapFields(schema._subscriptionType, schema)
171
193
  }
172
194
 
173
195
  const rootCtx = {
@@ -179,6 +201,10 @@ class GraphQLExecutePlugin extends TracingPlugin {
179
201
  abortController,
180
202
  executeSpan: span,
181
203
  plugin: this,
204
+ // graphql.resolve variable tags: memoize the filtered result for the
205
+ // last-seen variableValues object (see #filterVariables).
206
+ filteredVariablesKey: NO_VARIABLES_CACHED,
207
+ filteredVariables: undefined,
182
208
  }
183
209
  ctx.ddRootCtx = rootCtx
184
210
  if (isWeakMapKey(contextValue)) {
@@ -291,7 +317,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
291
317
  field.span = span
292
318
 
293
319
  if (fieldNode && this.config.variables && fieldNode.arguments) {
294
- const variables = this.config.variables(variableValues)
320
+ const variables = this.#filterVariables(rootCtx, variableValues)
295
321
  for (const arg of fieldNode.arguments) {
296
322
  if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) {
297
323
  const name = arg.value.name.value
@@ -303,6 +329,27 @@ class GraphQLExecutePlugin extends TracingPlugin {
303
329
  return span
304
330
  }
305
331
 
332
+ // Memoize the user variables filter against the last-seen variableValues
333
+ // object. graphql hands every resolver in one execute the same coerced
334
+ // variableValues object, so all arg-bearing fields hit the identity fast
335
+ // path and the filter runs once per operation. A nested execute() sharing
336
+ // the same object contextValue reuses the outer rootCtx but carries its own
337
+ // variableValues; comparing by identity recomputes for it (and any later
338
+ // fields on that inner object reuse the slot), so each field's tags stay
339
+ // correct. A single slot beats a WeakMap here: no per-operation allocation,
340
+ // and the common single-object case is a bare `===` (see the microbenchmark
341
+ // numbers in the commit body).
342
+ #filterVariables (rootCtx, variableValues) {
343
+ if (rootCtx.filteredVariablesKey === variableValues) {
344
+ return rootCtx.filteredVariables
345
+ }
346
+
347
+ const filtered = this.config.variables(variableValues)
348
+ rootCtx.filteredVariablesKey = variableValues
349
+ rootCtx.filteredVariables = filtered
350
+ return filtered
351
+ }
352
+
306
353
  // Public — called from wrapResolve. endTime reflects when the resolver
307
354
  // actually completed, not when the field record was created.
308
355
  finishResolveSpan (span, field, error, result, endTime) {
@@ -432,15 +479,44 @@ function wrapResolve (resolve) {
432
479
  return resolveAsync
433
480
  }
434
481
 
435
- function wrapFields (type) {
436
- if (!type?._fields || patchedTypes.has(type)) return
482
+ function wrapFields (type, schema) {
483
+ if (!type || !markWalked(schema, type)) return
484
+
485
+ const tag = type[Symbol.toStringTag]
437
486
 
438
- patchedTypes.add(type)
487
+ // Union types (e.g. Apollo Federation's `_Entity`) hold their members on
488
+ // `_types`, not `_fields`. Their member object types are reachable only here,
489
+ // so descend into each to wrap the entity resolvers a `_entities` query runs.
490
+ if (tag === 'GraphQLUnionType') {
491
+ for (const member of type.getTypes()) wrapFields(member, schema)
492
+ return
493
+ }
494
+
495
+ if (type._fields) {
496
+ for (const field of Object.values(type._fields)) {
497
+ wrapFieldResolve(field)
498
+ wrapFieldType(field, schema)
499
+ }
500
+ }
501
+
502
+ // Interface implementations carry their own resolvers and are reachable only
503
+ // through `getPossibleTypes`; an interface return type alone never wraps them.
504
+ if (schema && tag === 'GraphQLInterfaceType') {
505
+ for (const impl of schema.getPossibleTypes(type)) wrapFields(impl, schema)
506
+ }
507
+ }
439
508
 
440
- for (const field of Object.values(type._fields)) {
441
- wrapFieldResolve(field)
442
- wrapFieldType(field)
509
+ // Marks the guard on entry so recursive types (a field looping back to its own
510
+ // type, an interface an implementation returns) terminate the walk.
511
+ function markWalked (schema, type) {
512
+ let walked = walkedTypes.get(schema)
513
+ if (walked === undefined) {
514
+ walked = new WeakSet()
515
+ walkedTypes.set(schema, walked)
443
516
  }
517
+ if (walked.has(type)) return false
518
+ walked.add(type)
519
+ return true
444
520
  }
445
521
 
446
522
  function wrapFieldResolve (field) {
@@ -448,13 +524,13 @@ function wrapFieldResolve (field) {
448
524
  field.resolve = wrapResolve(field.resolve)
449
525
  }
450
526
 
451
- function wrapFieldType (field) {
527
+ function wrapFieldType (field, schema) {
452
528
  if (!field?.type) return
453
529
 
454
530
  let unwrapped = field.type
455
531
  while (unwrapped.ofType) unwrapped = unwrapped.ofType
456
532
 
457
- wrapFields(unwrapped)
533
+ wrapFields(unwrapped, schema)
458
534
  }
459
535
 
460
536
  // Runs the resolver inside `store`, including any code after an internal
@@ -1,9 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { isApolloHealthCheckSource } = require('./utils')
4
5
 
5
6
  const documentSources = new WeakMap()
6
7
 
8
+ // Documents produced by parsing an Apollo Gateway health-check poll. Populated
9
+ // here (parse owns the document lifecycle, like documentSources) and read by the
10
+ // validate plugin. Execute independently verifies the operation for cached docs.
11
+ const healthCheckDocuments = new WeakSet()
12
+
7
13
  class GraphQLParsePlugin extends TracingPlugin {
8
14
  static id = 'graphql'
9
15
  static operation = 'parser'
@@ -12,6 +18,14 @@ class GraphQLParsePlugin extends TracingPlugin {
12
18
  bindStart (ctx) {
13
19
  const source = ctx.arguments?.[0]
14
20
 
21
+ // Apollo Gateway polls every subgraph with a fixed health-check query.
22
+ // Mark its document after parsing so validation can skip the same poll.
23
+ if (isApolloHealthCheckSource(source?.body ?? source)) {
24
+ ctx.ddHealthCheck = true
25
+ ctx.ddSkipped = true
26
+ return ctx.currentStore
27
+ }
28
+
15
29
  this.startSpan('graphql.parse', {
16
30
  service: this.config.service,
17
31
  type: 'graphql',
@@ -24,8 +38,14 @@ class GraphQLParsePlugin extends TracingPlugin {
24
38
  }
25
39
 
26
40
  end (ctx) {
27
- const source = ctx.ddSource
28
41
  const document = ctx.result
42
+
43
+ if (ctx.ddHealthCheck) {
44
+ if (document) healthCheckDocuments.add(document)
45
+ return ctx.parentStore
46
+ }
47
+
48
+ const source = ctx.ddSource
29
49
  const span = ctx?.currentStore?.span || this.activeSpan
30
50
 
31
51
  let docSource
@@ -51,5 +71,6 @@ class GraphQLParsePlugin extends TracingPlugin {
51
71
  }
52
72
 
53
73
  GraphQLParsePlugin.documentSources = documentSources
74
+ GraphQLParsePlugin.healthCheckDocuments = healthCheckDocuments
54
75
 
55
76
  module.exports = GraphQLParsePlugin
@@ -1,7 +1,24 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
- const { extractErrorIntoSpanEvent, getCachedRequestOperation } = require('./utils')
4
+ const { extractErrorIntoSpanEvent, getCachedRequestOperation, isApolloHealthCheckSource } = require('./utils')
5
+
6
+ /**
7
+ * @typedef {object} GraphQLRequestStore
8
+ * @property {import('../../dd-trace/src/opentracing/span')} [span]
9
+ * @property {import('../../dd-trace/src/opentracing/span')} [graphqlRequestSpan]
10
+ * @property {string} [graphqlRequestOperationName]
11
+ * @property {unknown} [graphqlRequestSource]
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} GraphQLRequestContext
16
+ * @property {unknown[]} [arguments]
17
+ * @property {GraphQLRequestStore} [currentStore]
18
+ * @property {GraphQLRequestStore} [parentStore]
19
+ * @property {boolean} [ddSkipped]
20
+ * @property {{ errors?: import('graphql').GraphQLError[] }} [result]
21
+ */
5
22
 
6
23
  // Top-level GraphQL request span for drivers that funnel every operation
7
24
  // through a single entry point but parse/validate/execute internally (mercurius
@@ -25,9 +42,18 @@ class GraphQLRequestPlugin extends TracingPlugin {
25
42
  static kind = 'server'
26
43
  static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request'
27
44
 
45
+ /**
46
+ * @param {GraphQLRequestContext} ctx
47
+ */
28
48
  bindStart (ctx) {
29
49
  // fastifyGraphQl(source, context, variables, operationName)
30
50
  const source = ctx.arguments?.[0]
51
+
52
+ if (isApolloHealthCheckSource(source)) {
53
+ ctx.ddSkipped = true
54
+ return ctx.currentStore
55
+ }
56
+
31
57
  const operationName = ctx.arguments?.[3]
32
58
 
33
59
  // `source` is the request text on the common path, but mercurius also
@@ -72,7 +98,12 @@ class GraphQLRequestPlugin extends TracingPlugin {
72
98
  return ctx.currentStore
73
99
  }
74
100
 
101
+ /**
102
+ * @param {GraphQLRequestContext} ctx
103
+ */
75
104
  asyncEnd (ctx) {
105
+ if (ctx.ddSkipped) return ctx.parentStore
106
+
76
107
  /* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */
77
108
  const span = ctx?.currentStore?.span || this.activeSpan
78
109
  /* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */
@@ -82,7 +113,7 @@ class GraphQLRequestPlugin extends TracingPlugin {
82
113
  if (result?.errors?.length) {
83
114
  span.setTag('error', result.errors[0])
84
115
  for (const error of result.errors) {
85
- extractErrorIntoSpanEvent(this._tracerConfig, span, error)
116
+ extractErrorIntoSpanEvent(this.config, span, error)
86
117
  }
87
118
  }
88
119
 
@@ -228,6 +228,46 @@ function extractErrorIntoSpanEvent (config, span, exc) {
228
228
  span.addEvent('dd.graphql.query.error', attributes, Date.now())
229
229
  }
230
230
 
231
+ // Apollo Gateway's fixed subgraph health-check query, sent verbatim on every
232
+ // poll interval. See https://github.com/apollographql/federation
233
+ // `HEALTH_CHECK_QUERY`.
234
+ const HEALTH_CHECK_QUERY = 'query __ApolloServiceHealthCheck__ { __typename }'
235
+
236
+ /**
237
+ * Matches the raw query string before it is parsed (the only input parse has).
238
+ *
239
+ * @param {unknown} source Raw query string or a graphql `Source` body.
240
+ * @returns {boolean}
241
+ */
242
+ function isApolloHealthCheckSource (source) {
243
+ return source === HEALTH_CHECK_QUERY
244
+ }
245
+
246
+ /**
247
+ * Matches Apollo's parsed health-check operation exactly for cached documents.
248
+ *
249
+ * @param {import('graphql').OperationDefinitionNode | undefined} operation
250
+ * @returns {boolean}
251
+ */
252
+ function isApolloHealthCheck (operation) {
253
+ const selections = operation?.selectionSet?.selections
254
+ if (operation?.operation !== 'query' ||
255
+ operation.name?.value !== '__ApolloServiceHealthCheck__' ||
256
+ operation.variableDefinitions?.length ||
257
+ operation.directives?.length ||
258
+ selections?.length !== 1) {
259
+ return false
260
+ }
261
+
262
+ const selection = selections[0]
263
+ return selection.kind === 'Field' &&
264
+ selection.name?.value === '__typename' &&
265
+ selection.alias === undefined &&
266
+ selection.selectionSet === undefined &&
267
+ selection.arguments?.length === 0 &&
268
+ selection.directives?.length === 0
269
+ }
270
+
231
271
  let tools
232
272
 
233
273
  function getSignature (document, operationName, operationType, calculate) {
@@ -261,5 +301,7 @@ module.exports = {
261
301
  getCachedRequestOperation,
262
302
  getOperation,
263
303
  getSignature,
304
+ isApolloHealthCheck,
305
+ isApolloHealthCheckSource,
264
306
  refineRequestSpan,
265
307
  }
@@ -3,7 +3,7 @@
3
3
  const { storage } = require('../../datadog-core')
4
4
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
5
5
  const GraphQLParsePlugin = require('./parse')
6
- const { extractErrorIntoSpanEvent, refineRequestSpan } = require('./utils')
6
+ const { extractErrorIntoSpanEvent, isApolloHealthCheck, refineRequestSpan } = require('./utils')
7
7
 
8
8
  const legacyStorage = storage('legacy')
9
9
 
@@ -15,6 +15,16 @@ class GraphQLValidatePlugin extends TracingPlugin {
15
15
  bindStart (ctx) {
16
16
  // validate(schema, documentAST, rules, options, typeInfo)
17
17
  const document = ctx.arguments?.[1]
18
+
19
+ // Verify the marked document in case the caller transformed its AST after parsing.
20
+ if (document &&
21
+ GraphQLParsePlugin.healthCheckDocuments.has(document) &&
22
+ document.definitions?.length === 1 &&
23
+ isApolloHealthCheck(document.definitions[0])) {
24
+ ctx.ddSkipped = true
25
+ return ctx.currentStore
26
+ }
27
+
18
28
  const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
19
29
  const source = this.config.source && document && docSource
20
30
 
@@ -52,6 +62,8 @@ class GraphQLValidatePlugin extends TracingPlugin {
52
62
  }
53
63
 
54
64
  end (ctx) {
65
+ if (ctx.ddSkipped) return ctx.parentStore
66
+
55
67
  const document = ctx.ddDocument
56
68
  const errors = ctx.result
57
69
  const span = ctx?.currentStore?.span || this.activeSpan
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- // Plugin temporarily disabled. See https://github.com/DataDog/dd-trace-js/issues/312
4
-
5
3
  const ServerPlugin = require('../../dd-trace/src/plugins/server')
6
4
  const web = require('../../dd-trace/src/plugins/util/web')
7
5
  const { COMPONENT, SVC_SRC_KEY } = require('../../dd-trace/src/constants')
@@ -10,6 +8,7 @@ class Http2ServerPlugin extends ServerPlugin {
10
8
  constructor (tracer, config) {
11
9
  super(tracer, config)
12
10
  this.addBind('apm:http2:server:response:emit', this.bindEmit)
11
+ this.addSub('apm:http2:server:request:adopt', this.adopt)
13
12
  }
14
13
 
15
14
  static id = 'http2'
@@ -47,15 +46,35 @@ class Http2ServerPlugin extends ServerPlugin {
47
46
 
48
47
  const context = web.getContext(req)
49
48
 
50
- if (!context.instrumented) {
51
- context.res.writeHead = web.wrapWriteHead(context)
52
- context.instrumented = true
53
- }
49
+ // A mixed server adopts the real request off this stream later; key the
50
+ // context on the stream now so that lookup resolves. Skipped for the common
51
+ // single-listener request, which never adopts.
52
+ if (ctx.adoptable) web.linkContextToStream(req.stream, context)
53
+
54
+ instrumentWriteHead(context)
54
55
 
55
56
  return ctx.currentStore
56
57
  }
57
58
 
59
+ // A mixed server (raw-stream + 'request' listeners) creates the span from the
60
+ // 'stream' event with a throwaway adapter. When the compatibility layer then
61
+ // synthesizes the real request/response off the same stream, point the shared
62
+ // context at them so `web.setFramework`/`web.setRoute` from the user's
63
+ // 'request' handler resolve to this span and the finish `hooks.request`
64
+ // receives the real objects instead of the adapter.
65
+ adopt (ctx) {
66
+ const context = web.patch(ctx.req)
67
+ context.req = ctx.req
68
+ context.res = ctx.res
69
+ instrumentWriteHead(context)
70
+ }
71
+
58
72
  bindEmit (ctx) {
73
+ // Both the compatibility response and the core-API stream emit 'close'
74
+ // exactly once, so the span is finished from a single source. `web.js`
75
+ // bypasses its `finished` idempotency guard for stream-backed requests
76
+ // (`!req.stream`); that bypass is harmless here only because of this
77
+ // single-finish property.
59
78
  if (ctx.eventName !== 'close') return ctx.currentStore
60
79
 
61
80
  const { req } = ctx
@@ -78,4 +97,17 @@ class Http2ServerPlugin extends ServerPlugin {
78
97
  }
79
98
  }
80
99
 
100
+ // The core stream API has no `res.writeHead`; CORS preflight tagging only
101
+ // applies to the compatibility response that exposes it. Runs once per context:
102
+ // the mixed path calls it again from `adopt` once the real response is in place.
103
+ /**
104
+ * @param {{ res: { writeHead?: Function }, instrumented?: boolean }} context
105
+ */
106
+ function instrumentWriteHead (context) {
107
+ if (!context.instrumented && typeof context.res.writeHead === 'function') {
108
+ context.res.writeHead = web.wrapWriteHead(context)
109
+ context.instrumented = true
110
+ }
111
+ }
112
+
81
113
  module.exports = Http2ServerPlugin
@@ -122,28 +122,43 @@ function isMarkedAsUnskippable (test) {
122
122
  return false
123
123
  }
124
124
 
125
- function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
125
+ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRootDir) {
126
126
  const unskippableSuites = {}
127
127
  const forcedToRunSuites = {}
128
128
 
129
129
  const skippedSuites = []
130
130
  const suitesToRun = []
131
+ const normalizedSkippableSuites = new Set(skippableSuites.map(suite => suite.replaceAll('\\', '/')))
131
132
 
132
133
  for (const test of originalTests) {
133
134
  const relativePath = getTestSuitePath(test.path, rootDir)
134
- const shouldBeSkipped = skippableSuites.includes(relativePath)
135
+ const testRootDir = test?.context?.config?.rootDir || fallbackRootDir
136
+ let fallbackRelativePath
137
+ let skippedSuite = normalizedSkippableSuites.has(relativePath) ? relativePath : undefined
138
+ if (testRootDir && testRootDir !== rootDir) {
139
+ fallbackRelativePath = getTestSuitePath(test.path, testRootDir)
140
+ if (skippedSuite === undefined && normalizedSkippableSuites.has(fallbackRelativePath)) {
141
+ skippedSuite = fallbackRelativePath
142
+ }
143
+ }
135
144
  if (isMarkedAsUnskippable(test)) {
136
145
  suitesToRun.push(test)
137
146
  unskippableSuites[relativePath] = true
138
- if (shouldBeSkipped) {
147
+ if (fallbackRelativePath !== undefined) {
148
+ unskippableSuites[fallbackRelativePath] = true
149
+ }
150
+ if (skippedSuite !== undefined) {
139
151
  forcedToRunSuites[relativePath] = true
152
+ if (fallbackRelativePath !== undefined) {
153
+ forcedToRunSuites[fallbackRelativePath] = true
154
+ }
140
155
  }
141
156
  continue
142
157
  }
143
- if (shouldBeSkipped) {
144
- skippedSuites.push(relativePath)
145
- } else {
158
+ if (skippedSuite === undefined) {
146
159
  suitesToRun.push(test)
160
+ } else {
161
+ skippedSuites.push(skippedSuite)
147
162
  }
148
163
  }
149
164