dd-trace 6.4.0 → 6.5.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 (178) 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/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +10 -7
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +20 -7
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  65. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  66. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  67. package/packages/datadog-instrumentations/src/jest.js +26 -0
  68. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  69. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  71. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  72. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  73. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  74. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  75. package/packages/datadog-instrumentations/src/next.js +24 -0
  76. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  77. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  78. package/packages/datadog-instrumentations/src/pg.js +2 -2
  79. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  80. package/packages/datadog-instrumentations/src/process.js +3 -0
  81. package/packages/datadog-instrumentations/src/router.js +18 -8
  82. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  83. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  84. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  85. package/packages/datadog-instrumentations/src/ws.js +2 -1
  86. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  87. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  88. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  89. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  90. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  91. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  92. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  93. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  94. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  95. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  96. package/packages/datadog-plugin-http2/src/client.js +1 -1
  97. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  98. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  99. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  100. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  101. package/packages/datadog-plugin-router/src/index.js +11 -2
  102. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  103. package/packages/datadog-plugin-ws/src/close.js +2 -1
  104. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  105. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  106. package/packages/datadog-plugin-ws/src/server.js +2 -1
  107. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  110. package/packages/dd-trace/src/azure_metadata.js +2 -1
  111. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  112. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  113. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  114. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  115. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  116. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  117. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  119. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  120. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  121. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  122. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  123. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  124. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  127. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  128. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  129. package/packages/dd-trace/src/config/helper.js +1 -0
  130. package/packages/dd-trace/src/config/index.js +10 -0
  131. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  132. package/packages/dd-trace/src/exporter.js +2 -0
  133. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  134. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  135. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  136. package/packages/dd-trace/src/feature-registry.js +10 -3
  137. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  138. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  139. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  140. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  141. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  142. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  143. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  146. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  147. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  149. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  150. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  151. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  153. package/packages/dd-trace/src/llmobs/util.js +32 -0
  154. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  155. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  156. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  157. package/packages/dd-trace/src/openfeature/index.js +0 -2
  158. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  159. package/packages/dd-trace/src/openfeature/register.js +16 -27
  160. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  161. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  162. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  163. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  164. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  165. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  166. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  167. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  168. package/packages/dd-trace/src/proxy.js +84 -7
  169. package/packages/dd-trace/src/span_processor.js +5 -0
  170. package/packages/dd-trace/src/standalone/index.js +0 -14
  171. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  172. package/packages/dd-trace/src/util.js +26 -0
  173. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  174. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  175. package/vendor/dist/protobufjs/index.js +1 -1
  176. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  177. package/vendor/dist/shell-quote/index.js +1 -1
  178. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -197,7 +197,6 @@ class GraphQLExecutePlugin extends TracingPlugin {
197
197
  config: this.config,
198
198
  fields: new Map(),
199
199
  pathCache: new Map(),
200
- collapsedPathCache: this.config.collapse ? { byPath: new Map(), byParent: new Map() } : undefined,
201
200
  abortController,
202
201
  executeSpan: span,
203
202
  plugin: this,
@@ -228,9 +227,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
228
227
  if (!span) return
229
228
 
230
229
  // Synchronous execute() throw (e.g. execute(null, doc)) — error handler
231
- // already tagged the span, just finish it.
230
+ // already tagged the span.
232
231
  if (ctx.error) {
233
- this.#drain(ctx, span)
232
+ if (ctx.ddAborted) {
233
+ span.finish()
234
+ } else {
235
+ this.#finishSpan(ctx, span)
236
+ }
234
237
  return ctx.parentStore
235
238
  }
236
239
 
@@ -239,10 +242,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
239
242
  if (typeof result?.then === 'function') {
240
243
  result.then(
241
244
  (res) => this.#finishSpan(ctx, span, res),
242
- (err) => {
243
- span.setTag('error', err)
244
- this.#drain(ctx, span)
245
- }
245
+ (err) => this.#finishSpan(ctx, span, undefined, err)
246
246
  )
247
247
  } else {
248
248
  this.#finishSpan(ctx, span, result)
@@ -261,8 +261,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
261
261
  }
262
262
  }
263
263
 
264
- #finishSpan (ctx, span, res) {
265
- this.config.hooks.execute(span, ctx.ddArgs, res)
264
+ /**
265
+ * @param {object} ctx
266
+ * @param {import('../../dd-trace/src/opentracing/span')} span
267
+ * @param {import('graphql').ExecutionResult} [res]
268
+ * @param {unknown} [error]
269
+ */
270
+ #finishSpan (ctx, span, res, error) {
271
+ if (error !== undefined) {
272
+ span.setTag('error', error)
273
+ }
266
274
 
267
275
  if (res?.errors?.length) {
268
276
  span.setTag('error', res.errors[0])
@@ -271,17 +279,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
271
279
  }
272
280
  }
273
281
 
274
- this.#drain(ctx, span)
275
- }
276
-
277
- #drain (ctx, span) {
278
- span.finish()
279
282
  if (ctx.ddContextValue) {
280
283
  contexts.delete(ctx.ddContextValue)
281
284
  }
282
285
  if (ctx.ddInstrumentedArgs) {
283
286
  instrumentedArgs.delete(ctx.ddInstrumentedArgs)
284
287
  }
288
+
289
+ this.config.hooks.execute(span, ctx.ddArgs, res)
290
+
291
+ span.finish()
285
292
  }
286
293
 
287
294
  // Public — called from wrapResolve (free function, crosses class boundary).
@@ -307,6 +314,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
307
314
  type: 'graphql',
308
315
  startTime,
309
316
  meta: {
317
+ 'graphql.field.coordinates': `${field.parentTypeName}.${fieldName}`,
310
318
  'graphql.field.name': fieldName,
311
319
  'graphql.field.path': collapsedKey,
312
320
  'graphql.field.type': baseTypeName,
@@ -423,20 +431,35 @@ function wrapResolve (resolve) {
423
431
  return resolve.apply(this, arguments)
424
432
  }
425
433
 
426
- const fieldKey = config.collapse ? buildCachedCollapsedPath(infoPath, rootCtx.collapsedPathCache) : infoPath
434
+ const fieldKey = config.collapse ? pathString : infoPath
435
+ const parentTypeName = info.parentType.name
427
436
  let field = rootCtx.fields.get(fieldKey)
437
+ const collapsedField = field
438
+ if (config.collapse && field !== undefined && field.parentTypeName !== parentTypeName) {
439
+ const parentTypeFields = field.parentTypeFields
440
+ if (parentTypeFields?.parentTypeName === undefined) {
441
+ field = parentTypeFields?.get(parentTypeName)
442
+ } else if (parentTypeFields.parentTypeName === parentTypeName) {
443
+ field = parentTypeFields
444
+ } else {
445
+ field = undefined
446
+ }
447
+ if (field && infoPath.typename === undefined) {
448
+ cacheFieldByPath(rootCtx, infoPath, field)
449
+ }
450
+ }
428
451
  const isFirst = !field
429
452
 
430
453
  if (isFirst) {
431
454
  field = {
432
455
  fieldNode: info.fieldNodes?.[0],
433
456
  fieldName: info.fieldName,
457
+ parentTypeName,
434
458
  returnType: info.returnType,
435
459
  baseTypeName: getBaseTypeName(info.returnType),
436
460
  variableValues: info.variableValues,
437
461
  args,
438
462
  infoPath,
439
- fieldKey,
440
463
  pathString,
441
464
  collapsedKey: collapsedKey ?? pathString,
442
465
  span: null,
@@ -445,7 +468,25 @@ function wrapResolve (resolve) {
445
468
  parentStore: null,
446
469
  currentStore: null,
447
470
  }
448
- rootCtx.fields.set(fieldKey, field)
471
+ if (config.collapse && collapsedField) {
472
+ const parentTypeFields = collapsedField.parentTypeFields
473
+ if (parentTypeFields === undefined) {
474
+ collapsedField.parentTypeFields = field
475
+ } else if (parentTypeFields.parentTypeName === undefined) {
476
+ parentTypeFields.set(parentTypeName, field)
477
+ } else {
478
+ const fieldsByParentType = new Map()
479
+ .set(collapsedField.parentTypeName, collapsedField)
480
+ .set(parentTypeFields.parentTypeName, parentTypeFields)
481
+ .set(parentTypeName, field)
482
+ collapsedField.parentTypeFields = fieldsByParentType
483
+ }
484
+ if (infoPath.typename === undefined) {
485
+ cacheFieldByPath(rootCtx, infoPath, field)
486
+ }
487
+ } else {
488
+ rootCtx.fields.set(fieldKey, field)
489
+ }
449
490
  }
450
491
 
451
492
  // Collapsed siblings still publish updateField (master's contract: one
@@ -592,31 +633,18 @@ function buildCachedPathString (path, cache, collapse) {
592
633
  return pathString
593
634
  }
594
635
 
595
- function buildCachedCollapsedPath (path, cache) {
596
- if (!path) return
597
-
598
- const cached = cache.byPath.get(path)
599
- if (cached !== undefined) return cached
600
-
601
- const segment = typeof path.key === 'string' ? path.key : '*'
602
- const prev = path.prev === undefined
603
- ? undefined
604
- : buildCachedCollapsedPath(path.prev, cache)
605
-
606
- let siblings = cache.byParent.get(prev)
607
- if (siblings === undefined) {
608
- siblings = new Map()
609
- cache.byParent.set(prev, siblings)
610
- }
611
-
612
- let collapsedPath = siblings.get(segment)
613
- if (collapsedPath === undefined) {
614
- collapsedPath = { key: segment, prev }
615
- siblings.set(segment, collapsedPath)
616
- }
617
-
618
- cache.byPath.set(path, collapsedPath)
619
- return collapsedPath
636
+ /**
637
+ * @param {{ hasFieldsByPath?: boolean, fields: Map<string|object, object> }} rootCtx
638
+ * @param {object} path
639
+ * @param {object} field
640
+ */
641
+ function cacheFieldByPath (rootCtx, path, field) {
642
+ // Leaf fields cannot parent resolver spans, so their concrete paths are never read.
643
+ if (field.fieldNode?.selectionSet === undefined) return
644
+
645
+ // Concrete info path objects cannot collide with collapsed path string keys.
646
+ rootCtx.hasFieldsByPath = true
647
+ rootCtx.fields.set(path, field)
620
648
  }
621
649
 
622
650
  // Depth filtering directly on the linked-list node — no array allocation needed.
@@ -640,9 +668,23 @@ function shouldInstrumentNode (config, path) {
640
668
  }
641
669
 
642
670
  function getParentField (rootCtx, field) {
643
- for (let curr = field.fieldKey?.prev; curr; curr = curr.prev) {
644
- const innerField = rootCtx.fields.get(curr)
645
- if (innerField) return innerField
671
+ for (let curr = field.infoPath?.prev; curr; curr = curr.prev) {
672
+ const fieldKey = rootCtx.config.collapse ? rootCtx.pathCache.get(curr) : curr
673
+ const innerField = rootCtx.fields.get(fieldKey)
674
+ if (innerField) {
675
+ if (curr.typename === undefined) {
676
+ if (rootCtx.hasFieldsByPath) {
677
+ const fieldByPath = rootCtx.fields.get(curr)
678
+ if (fieldByPath) return fieldByPath
679
+ }
680
+ return innerField
681
+ }
682
+ if (innerField.parentTypeName === curr.typename) return innerField
683
+
684
+ const parentTypeFields = innerField.parentTypeFields
685
+ if (parentTypeFields.parentTypeName === undefined) return parentTypeFields.get(curr.typename)
686
+ return parentTypeFields
687
+ }
646
688
  }
647
689
 
648
690
  return null
@@ -68,8 +68,6 @@ class GraphQLValidatePlugin extends TracingPlugin {
68
68
  const errors = ctx.result
69
69
  const span = ctx?.currentStore?.span || this.activeSpan
70
70
 
71
- this.config.hooks.validate(span, document, errors)
72
-
73
71
  if (errors?.length) {
74
72
  span.setTag('error', errors[0])
75
73
  for (const err of errors) {
@@ -77,6 +75,8 @@ class GraphQLValidatePlugin extends TracingPlugin {
77
75
  }
78
76
  }
79
77
 
78
+ this.config.hooks.validate(span, document, errors)
79
+
80
80
  span.finish()
81
81
 
82
82
  return ctx.parentStore
@@ -32,7 +32,7 @@ class Http2ClientPlugin extends ClientPlugin {
32
32
  const { authority, options, headers = {} } = message
33
33
  const sessionDetails = extractSessionDetails(authority, options)
34
34
  const path = headers[HTTP2_HEADER_PATH] || '/'
35
- const pathname = path.split(/[?#]/)[0]
35
+ const pathname = path.split(/[?#]/, 1)[0]
36
36
  const method = headers[HTTP2_HEADER_METHOD] || HTTP2_METHOD_GET
37
37
  const base = `${sessionDetails.protocol}//${sessionDetails.host}:${sessionDetails.port}`
38
38
  const uri = `${base}${pathname}`
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const { getSegment } = require('../../../dd-trace/src/util')
3
4
  const LangChainHandler = require('./default')
4
5
 
5
6
  class LangChainEmbeddingHandler extends LangChainHandler {
6
7
  extractProvider (instance) {
7
- return instance.constructor.name.split('Embeddings')[0].toLowerCase()
8
+ return getSegment(instance.constructor.name, 'Embeddings', 0).toLowerCase()
8
9
  }
9
10
 
10
11
  extractModel (instance) {
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const { getSegment } = require('../../../dd-trace/src/util')
3
4
  const LangChainHandler = require('./default')
4
5
 
5
6
  class LangChainLanguageModelHandler extends LangChainHandler {
6
7
  extractProvider (instance) {
7
- return typeof instance._llmType === 'function' && instance._llmType().split('-')[0]
8
+ return typeof instance._llmType === 'function' && getSegment(instance._llmType(), '-', 0)
8
9
  }
9
10
 
10
11
  extractModel (instance) {
@@ -15,10 +15,10 @@ function getTokensFromLlmOutput (result) {
15
15
  for (const tokenNames of [['input', 'prompt'], ['output', 'completion'], ['total']]) {
16
16
  let token = 0
17
17
  for (const tokenName of tokenNames) {
18
- const underScore = `${tokenName}_tokens`
18
+ const underscore = `${tokenName}_tokens`
19
19
  const camelCase = `${tokenName}Tokens`
20
20
 
21
- token = tokenUsage[underScore] || tokenUsage[camelCase] || token
21
+ token = tokenUsage[underscore] || tokenUsage[camelCase] || token
22
22
  }
23
23
 
24
24
  tokens[tokenNames[0]] = token
@@ -1,16 +1,26 @@
1
1
  'use strict'
2
2
 
3
+ const { basename } = require('node:path')
4
+
3
5
  const { storage } = require('../../datadog-core')
4
6
  const id = require('../../dd-trace/src/id')
5
7
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
8
+ const getConfig = require('../../dd-trace/src/config')
9
+ const {
10
+ SCREENSHOT_UPLOAD_RESULT_ERROR,
11
+ SCREENSHOT_UPLOAD_RESULT_UPLOADED,
12
+ getScreenshotCapturedAtMs,
13
+ getScreenshotUploadResult,
14
+ getScreenshotUploadTag,
15
+ } = require('../../dd-trace/src/ci-visibility/test-screenshot')
6
16
 
7
17
  const {
8
18
  finishAllTraceSpans,
9
19
  getTestSuiteCommonTags,
10
20
  getTestSuitePath,
11
21
  isModifiedTest,
22
+ setRumTestCorrelation,
12
23
  TEST_BROWSER_NAME,
13
- TEST_BROWSER_VERSION,
14
24
  TEST_CODE_OWNERS,
15
25
  TEST_EARLY_FLAKE_ABORT_REASON,
16
26
  TEST_EARLY_FLAKE_ENABLED,
@@ -51,6 +61,21 @@ const {
51
61
  const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry')
52
62
  const log = require('../../dd-trace/src/log')
53
63
 
64
+ const PLAYWRIGHT_FAILURE_SCREENSHOT_RE = /^test-failed-\d+\.png$/
65
+
66
+ /**
67
+ * Returns whether an attachment is an automatic Playwright failure screenshot.
68
+ *
69
+ * @param {object} attachment - Playwright test attachment
70
+ * @returns {boolean}
71
+ */
72
+ function isPlaywrightFailureScreenshot (attachment) {
73
+ return attachment?.name === 'screenshot' &&
74
+ attachment.contentType === 'image/png' &&
75
+ typeof attachment.path === 'string' &&
76
+ PLAYWRIGHT_FAILURE_SCREENSHOT_RE.test(basename(attachment.path))
77
+ }
78
+
54
79
  class PlaywrightPlugin extends CiPlugin {
55
80
  static id = 'playwright'
56
81
 
@@ -60,6 +85,8 @@ class PlaywrightPlugin extends CiPlugin {
60
85
  this._testSuiteSpansByTestSuiteAbsolutePath = new Map()
61
86
  this.numFailedTests = 0
62
87
  this.numFailedSuites = 0
88
+ this.pendingTestFinishes = 0
89
+ this.finishSession = undefined
63
90
 
64
91
  this.addSub('ci:playwright:test:is-modified', ({
65
92
  filePath,
@@ -71,6 +98,30 @@ class PlaywrightPlugin extends CiPlugin {
71
98
  onDone(isModified)
72
99
  })
73
100
 
101
+ this.addSub('ci:playwright:session:start', ({ isFailureScreenshotEnabled }) => {
102
+ if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED) return
103
+
104
+ if (!isFailureScreenshotEnabled) {
105
+ log.warn(
106
+ '%s %s',
107
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Playwright screenshot capture is disabled.',
108
+ 'Set Playwright use.screenshot to "only-on-failure", "on-first-failure", or "on".'
109
+ )
110
+ }
111
+ })
112
+
113
+ this.addSub('ci:playwright:session:configuration', ({ isFailureScreenshotEnabled }) => {
114
+ if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED || !isFailureScreenshotEnabled) return
115
+
116
+ if (!this.tracer._exporter?.canUploadTestScreenshots?.()) {
117
+ log.warn(
118
+ '%s %s',
119
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Playwright screenshot upload is not supported',
120
+ 'by the active Test Optimization transport.'
121
+ )
122
+ }
123
+ })
124
+
74
125
  this.addSub('ci:playwright:session:finish', ({
75
126
  status,
76
127
  isEarlyFlakeDetectionEnabled,
@@ -78,42 +129,51 @@ class PlaywrightPlugin extends CiPlugin {
78
129
  isTestManagementTestsEnabled,
79
130
  onDone,
80
131
  }) => {
81
- this.testModuleSpan.setTag(TEST_STATUS, status)
82
- this.testSessionSpan.setTag(TEST_STATUS, status)
132
+ const finishSession = () => {
133
+ this.testModuleSpan.setTag(TEST_STATUS, status)
134
+ this.testSessionSpan.setTag(TEST_STATUS, status)
83
135
 
84
- if (isEarlyFlakeDetectionEnabled) {
85
- this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
86
- }
87
- if (isEarlyFlakeDetectionFaulty) {
88
- this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
89
- }
90
- if (status === 'fail' && this.numFailedSuites > 0) {
91
- let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
92
- if (this.numFailedTests > 0) {
93
- errorMessage += ` Tests failed: ${this.numFailedTests}`
136
+ if (isEarlyFlakeDetectionEnabled) {
137
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
138
+ }
139
+ if (isEarlyFlakeDetectionFaulty) {
140
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
141
+ }
142
+ if (status === 'fail' && this.numFailedSuites > 0) {
143
+ let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
144
+ if (this.numFailedTests > 0) {
145
+ errorMessage += ` Tests failed: ${this.numFailedTests}`
146
+ }
147
+ const error = new Error(errorMessage)
148
+ this.testModuleSpan.setTag('error', error)
149
+ this.testSessionSpan.setTag('error', error)
94
150
  }
95
- const error = new Error(errorMessage)
96
- this.testModuleSpan.setTag('error', error)
97
- this.testSessionSpan.setTag('error', error)
98
- }
99
151
 
100
- if (isTestManagementTestsEnabled) {
101
- this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
152
+ if (isTestManagementTestsEnabled) {
153
+ this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
154
+ }
155
+
156
+ this.testModuleSpan.finish()
157
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
158
+ this.testSessionSpan.finish()
159
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
160
+ finishAllTraceSpans(this.testSessionSpan)
161
+ this.telemetry.count(TELEMETRY_TEST_SESSION, {
162
+ provider: this.ciProviderName,
163
+ autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
164
+ })
165
+ appClosingTelemetry()
166
+ this.tracer._exporter.flush(onDone)
167
+ this.numFailedTests = 0
168
+ this.numFailedSuites = 0
169
+ this.finishSession = undefined
102
170
  }
103
171
 
104
- this.testModuleSpan.finish()
105
- this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
106
- this.testSessionSpan.finish()
107
- this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
108
- finishAllTraceSpans(this.testSessionSpan)
109
- this.telemetry.count(TELEMETRY_TEST_SESSION, {
110
- provider: this.ciProviderName,
111
- autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
112
- })
113
- appClosingTelemetry()
114
- this.tracer._exporter.flush(onDone)
115
- this.numFailedTests = 0
116
- this.numFailedSuites = 0
172
+ if (this.pendingTestFinishes > 0) {
173
+ this.finishSession = finishSession
174
+ } else {
175
+ finishSession()
176
+ }
117
177
  })
118
178
 
119
179
  this.addBind('ci:playwright:test-suite:start', (ctx) => {
@@ -178,42 +238,17 @@ class PlaywrightPlugin extends CiPlugin {
178
238
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
179
239
  })
180
240
 
181
- this.addSub('ci:playwright:test:page-goto', ({
182
- isRumActive,
183
- page,
184
- }) => {
185
- const store = storage('legacy').getStore()
186
- const span = store && store.span
187
- if (!span) {
241
+ this.addSub('ci:playwright:test:page-goto', (ctx) => {
242
+ const activeSpan = storage('legacy').getStore()?.span
243
+ if (!setRumTestCorrelation(ctx, activeSpan)) {
188
244
  log.error('ci:playwright:test:page-goto: test span not found')
189
- return
190
- }
191
-
192
- if (isRumActive) {
193
- span.setTag(TEST_IS_RUM_ACTIVE, 'true')
194
-
195
- if (page) {
196
- const browserVersion = page.context().browser().version()
197
-
198
- if (browserVersion) {
199
- span.setTag(TEST_BROWSER_VERSION, browserVersion)
200
- }
201
-
202
- const url = page.url()
203
- const domain = new URL(url).hostname
204
- page.context().addCookies([{
205
- name: 'datadog-ci-visibility-test-execution-id',
206
- value: span.context().toTraceId(),
207
- domain,
208
- path: '/',
209
- }])
210
- }
211
245
  }
212
246
  })
213
247
 
214
- this.addSub('ci:playwright:worker:report', (serializedTraces) => {
248
+ this.addSub('ci:playwright:worker:report', ({ serializedTraces, screenshots }) => {
215
249
  const traces = JSON.parse(serializedTraces)
216
250
  const formattedTraces = []
251
+ let formattedTestSpan
217
252
 
218
253
  for (const trace of traces) {
219
254
  const formattedTrace = []
@@ -228,6 +263,7 @@ class PlaywrightPlugin extends CiPlugin {
228
263
  formattedSpan.meta[TEST_HAS_DYNAMIC_NAME] = 'true'
229
264
  }
230
265
  if (span.name === 'playwright.test') {
266
+ formattedTestSpan = formattedSpan
231
267
  // TODO: remove this comment
232
268
  // TODO: Let's pass rootDir, repositoryRoot, command, session id and module id as env vars
233
269
  // so we don't need this re-serialization logic. This can be passed just once, since they're unique
@@ -262,9 +298,36 @@ class PlaywrightPlugin extends CiPlugin {
262
298
  formattedTraces.push(formattedTrace)
263
299
  }
264
300
 
265
- for (const trace of formattedTraces) {
266
- this.tracer._exporter.export(trace)
301
+ const exportTraces = () => {
302
+ for (const trace of formattedTraces) {
303
+ this.tracer._exporter.export(trace)
304
+ }
267
305
  }
306
+
307
+ if (!formattedTestSpan || !screenshots) {
308
+ exportTraces()
309
+ return
310
+ }
311
+
312
+ this.pendingTestFinishes++
313
+ const uploadStarted = this.uploadTestScreenshots({
314
+ screenshots,
315
+ traceId: formattedTestSpan.trace_id.toString(10),
316
+ }, (screenshotUploadResult) => {
317
+ const screenshotUploadTag = getScreenshotUploadTag(screenshotUploadResult)
318
+ if (screenshotUploadTag) {
319
+ formattedTestSpan.meta[screenshotUploadTag] = 'true'
320
+ }
321
+ exportTraces()
322
+ this.pendingTestFinishes--
323
+ if (this.pendingTestFinishes === 0 && this.finishSession) {
324
+ this.finishSession()
325
+ }
326
+ })
327
+ if (uploadStarted) return
328
+
329
+ this.pendingTestFinishes--
330
+ exportTraces()
268
331
  })
269
332
 
270
333
  this.addBind('ci:playwright:test:start', (ctx) => {
@@ -480,6 +543,56 @@ class PlaywrightPlugin extends CiPlugin {
480
543
  })
481
544
  }
482
545
 
546
+ /**
547
+ * Uploads automatic failure screenshots for a Playwright test attempt.
548
+ *
549
+ * @param {object} options - Upload options
550
+ * @param {Array<object>} options.screenshots - Playwright test attachments
551
+ * @param {string} options.traceId - Test trace id used as the screenshot key
552
+ * @param {(result: string|undefined) => void} onDone - Completion callback
553
+ * @returns {boolean} Whether at least one upload was started
554
+ */
555
+ uploadTestScreenshots ({ screenshots, traceId }, onDone) {
556
+ const exporter = this.tracer?._exporter
557
+ if (!Array.isArray(screenshots) || !screenshots.length ||
558
+ !exporter?.canUploadTestScreenshots?.() ||
559
+ !exporter.uploadTestScreenshot) {
560
+ return false
561
+ }
562
+
563
+ const screenshotPaths = new Set()
564
+ for (const screenshot of screenshots) {
565
+ if (isPlaywrightFailureScreenshot(screenshot)) {
566
+ screenshotPaths.add(screenshot.path)
567
+ }
568
+ }
569
+ if (!screenshotPaths.size) return false
570
+
571
+ const uploadResults = new Array(screenshotPaths.size)
572
+ let pendingUploads = screenshotPaths.size
573
+ let index = 0
574
+ for (const filePath of screenshotPaths) {
575
+ const resultIndex = index++
576
+ exporter.uploadTestScreenshot({
577
+ filePath,
578
+ traceId,
579
+ idempotencyKey: `${traceId}:${basename(filePath)}`,
580
+ capturedAtMs: getScreenshotCapturedAtMs(filePath, filePath),
581
+ }, (error, uploaded = true) => {
582
+ if (uploaded) {
583
+ uploadResults[resultIndex] = error
584
+ ? SCREENSHOT_UPLOAD_RESULT_ERROR
585
+ : SCREENSHOT_UPLOAD_RESULT_UPLOADED
586
+ }
587
+ pendingUploads--
588
+ if (pendingUploads === 0) {
589
+ onDone(getScreenshotUploadResult(uploadResults))
590
+ }
591
+ })
592
+ }
593
+ return true
594
+ }
595
+
483
596
  // TODO: this runs both in worker and main process (main process: skipped tests that do not go through _runTest)
484
597
  startTestSpan (
485
598
  testName,
@@ -24,7 +24,7 @@ class RouterPlugin extends WebPlugin {
24
24
  let childOf
25
25
  if (context !== undefined) {
26
26
  const middleware = context.middleware
27
- childOf = middleware.length === 0 ? context.span : middleware[middleware.length - 1]
27
+ childOf = middleware.length === 0 ? context.span : middleware.at(-1)
28
28
  } else if (store) {
29
29
  childOf = store.span
30
30
  }
@@ -75,12 +75,21 @@ class RouterPlugin extends WebPlugin {
75
75
  const context = this.#contexts.get(req)
76
76
  if (!context) return
77
77
  const middleware = context.middleware
78
- const span = middleware.length === 0 ? context.span : middleware[middleware.length - 1]
78
+ const span = middleware.length === 0 ? context.span : middleware.at(-1)
79
79
  if (!span) return
80
80
 
81
81
  span.setTag('error', error)
82
82
  })
83
83
 
84
+ this.addSub(`apm:${this.constructor.id}:middleware:repeat`, ({ req, name, error }) => {
85
+ // The middleware span already finished on the first `next`, so record the
86
+ // repeat on the still-live request span instead of a finished one.
87
+ web.root(req)?.addEvent('middleware.next_called_again', {
88
+ 'middleware.name': name || '<anonymous>',
89
+ with_error: Boolean(error && error !== 'route' && error !== 'router'),
90
+ })
91
+ })
92
+
84
93
  this.addSub('apm:http:server:request:finish', ({ req }) => {
85
94
  const context = this.#contexts.get(req)
86
95