dd-trace 6.13.0 → 6.14.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 (201) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  8. package/ci/test-optimization-validation/package-check.js +1 -1
  9. package/ci/test-optimization-validation/report-writer.js +7 -5
  10. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  11. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  12. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  13. package/package.json +13 -10
  14. package/packages/datadog-esbuild/index.js +3 -1
  15. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  16. package/packages/datadog-instrumentations/src/fs.js +24 -20
  17. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  18. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  19. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  20. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  21. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +265 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +251 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +11 -1
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  33. package/packages/datadog-instrumentations/src/jest.js +25 -20
  34. package/packages/datadog-instrumentations/src/knex.js +76 -0
  35. package/packages/datadog-instrumentations/src/mariadb.js +6 -0
  36. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  37. package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
  38. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  39. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  40. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  41. package/packages/datadog-instrumentations/src/net.js +14 -17
  42. package/packages/datadog-instrumentations/src/next.js +212 -62
  43. package/packages/datadog-instrumentations/src/openai.js +42 -40
  44. package/packages/datadog-instrumentations/src/pg.js +15 -0
  45. package/packages/datadog-instrumentations/src/playwright.js +109 -13
  46. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  47. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  48. package/packages/datadog-instrumentations/src/undici.js +53 -4
  49. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +37 -3
  50. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  51. package/packages/datadog-instrumentations/src/vitest-worker.js +71 -14
  52. package/packages/datadog-instrumentations/src/webdriverio.js +965 -6
  53. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  54. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  55. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  56. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  57. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  58. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  59. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  60. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  61. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  62. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  63. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  64. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  65. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  66. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  67. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  68. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  69. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  70. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  71. package/packages/datadog-plugin-jest/src/index.js +0 -6
  72. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  73. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  74. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  75. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  76. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  77. package/packages/datadog-plugin-next/src/index.js +63 -29
  78. package/packages/datadog-plugin-playwright/src/index.js +167 -27
  79. package/packages/datadog-plugin-undici/src/index.js +27 -41
  80. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  81. package/packages/dd-trace/src/aiguard/evaluation.js +2 -1
  82. package/packages/dd-trace/src/aiguard/messages/anthropic.js +8 -1
  83. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  84. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  85. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  86. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  87. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  88. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  89. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  90. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  91. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
  92. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  93. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  94. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +5 -173
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  101. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  102. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +394 -225
  103. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  104. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  105. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  106. package/packages/dd-trace/src/ci-visibility/final-flush.js +2 -1
  107. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  108. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  109. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  110. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
  111. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  112. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  113. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  114. package/packages/dd-trace/src/config/index.js +73 -13
  115. package/packages/dd-trace/src/config/parsers.js +11 -1
  116. package/packages/dd-trace/src/config/stable.js +3 -3
  117. package/packages/dd-trace/src/config/supported-configurations.json +20 -2
  118. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  119. package/packages/dd-trace/src/debugger/config.js +12 -1
  120. package/packages/dd-trace/src/debugger/constants.js +1 -0
  121. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  122. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
  123. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -40
  124. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  125. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  126. package/packages/dd-trace/src/debugger/index.js +23 -3
  127. package/packages/dd-trace/src/dogstatsd.js +510 -76
  128. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  129. package/packages/dd-trace/src/encode/0.4.js +5 -5
  130. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  131. package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
  132. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  133. package/packages/dd-trace/src/exporters/agent/writer.js +24 -7
  134. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  135. package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
  136. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  137. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  138. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  139. package/packages/dd-trace/src/exporters/common/request.js +31 -11
  140. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  141. package/packages/dd-trace/src/exporters/common/writer.js +9 -7
  142. package/packages/dd-trace/src/flush.js +6 -3
  143. package/packages/dd-trace/src/id.js +14 -0
  144. package/packages/dd-trace/src/index.js +5 -2
  145. package/packages/dd-trace/src/llmobs/experiments/dataset.js +2 -1
  146. package/packages/dd-trace/src/llmobs/index.js +32 -32
  147. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  148. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  149. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  150. package/packages/dd-trace/src/llmobs/util.js +0 -33
  151. package/packages/dd-trace/src/llmobs/writers/base.js +49 -32
  152. package/packages/dd-trace/src/log/index.js +7 -1
  153. package/packages/dd-trace/src/{ci-visibility/log-submission → log-submission}/log-submission-plugin.js +48 -19
  154. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  155. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  156. package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -24
  157. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  158. package/packages/dd-trace/src/opentelemetry/metrics/index.js +15 -22
  159. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +14 -4
  160. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +10 -0
  161. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  162. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  163. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  164. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  165. package/packages/dd-trace/src/opentracing/propagation/text_map.js +93 -18
  166. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  167. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  168. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  169. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  170. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  171. package/packages/dd-trace/src/plugins/util/test.js +83 -20
  172. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  173. package/packages/dd-trace/src/plugins/util/web.js +4 -9
  174. package/packages/dd-trace/src/profiling/config.js +1 -0
  175. package/packages/dd-trace/src/profiling/oom.js +7 -2
  176. package/packages/dd-trace/src/profiling/profiler.js +23 -13
  177. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  178. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  179. package/packages/dd-trace/src/proxy.js +75 -5
  180. package/packages/dd-trace/src/remote_config/index.js +319 -101
  181. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  182. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  183. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -1
  184. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -1
  185. package/packages/dd-trace/src/sampling_rule.js +36 -9
  186. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +4 -8
  187. package/packages/dd-trace/src/serverless/vercel.js +53 -20
  188. package/packages/dd-trace/src/serverless.js +10 -3
  189. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  190. package/packages/dd-trace/src/span_stats.js +24 -28
  191. package/packages/dd-trace/src/startup-log.js +3 -2
  192. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  193. package/packages/dd-trace/src/telemetry/send-data.js +18 -16
  194. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  195. package/packages/dd-trace/src/tracer.js +4 -5
  196. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  197. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  198. package/vendor/dist/protobufjs/index.js +1 -1
  199. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  200. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  201. package/packages/dd-trace/src/encode/agentless-json.js +0 -210
@@ -1,9 +1,18 @@
1
1
  'use strict'
2
2
 
3
3
  const { storage } = require('../../datadog-core')
4
- const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
4
+ const { CLIENT_PORT_KEY, SVC_SRC_KEY } = require('../../dd-trace/src/constants')
5
5
  const DatabasePlugin = require('../../dd-trace/src/plugins/database')
6
6
 
7
+ /**
8
+ * @typedef {{
9
+ * database?: string,
10
+ * host?: string,
11
+ * port?: number,
12
+ * user?: string
13
+ * }} ConnectionConfig
14
+ */
15
+
7
16
  class MySQLPlugin extends DatabasePlugin {
8
17
  static id = 'mysql'
9
18
  static system = 'mysql'
@@ -33,13 +42,7 @@ class MySQLPlugin extends DatabasePlugin {
33
42
  startTime: ctx.startTime,
34
43
  type: 'sql',
35
44
  kind: 'client',
36
- meta: {
37
- 'db.type': this.system,
38
- 'db.user': ctx.conf.user,
39
- 'db.name': ctx.conf.database,
40
- 'out.host': ctx.conf.host,
41
- [CLIENT_PORT_KEY]: ctx.conf.port,
42
- },
45
+ meta: connectionMeta(this.system, ctx.conf),
43
46
  }, ctx)
44
47
  })
45
48
 
@@ -51,6 +54,20 @@ class MySQLPlugin extends DatabasePlugin {
51
54
  this.addError(ctx.error, span)
52
55
  }
53
56
  span.setTag(`${this.component}.pool.wait_time`, ctx.poolWaitTime)
57
+ if (ctx.connectionConfig !== undefined) {
58
+ span.addTags(connectionMeta(this.system, ctx.connectionConfig))
59
+ if (typeof this.config.service === 'function') {
60
+ const service = this.serviceName({
61
+ pluginConfig: this.config,
62
+ dbConfig: ctx.connectionConfig,
63
+ system: this.system,
64
+ })
65
+ if (service.name) {
66
+ this.setServiceName(span, service.name)
67
+ span.setTag(SVC_SRC_KEY, service.source)
68
+ }
69
+ }
70
+ }
54
71
  this.finish(ctx)
55
72
  })
56
73
  }
@@ -62,13 +79,7 @@ class MySQLPlugin extends DatabasePlugin {
62
79
  resource: ctx.sql,
63
80
  type: 'sql',
64
81
  kind: 'client',
65
- meta: {
66
- 'db.type': this.system,
67
- 'db.user': ctx.conf.user,
68
- 'db.name': ctx.conf.database,
69
- 'out.host': ctx.conf.host,
70
- [CLIENT_PORT_KEY]: ctx.conf.port,
71
- },
82
+ meta: connectionMeta(this.system, ctx.conf),
72
83
  }, ctx)
73
84
 
74
85
  if (ctx.poolWaitTime !== undefined) {
@@ -81,4 +92,19 @@ class MySQLPlugin extends DatabasePlugin {
81
92
  }
82
93
  }
83
94
 
95
+ /**
96
+ * @param {string} system
97
+ * @param {ConnectionConfig} config
98
+ * @returns {Record<string, string|number|undefined>}
99
+ */
100
+ function connectionMeta (system, config) {
101
+ return {
102
+ 'db.type': system,
103
+ 'db.user': config.user,
104
+ 'db.name': config.database,
105
+ 'out.host': config.host,
106
+ [CLIENT_PORT_KEY]: config.port,
107
+ }
108
+ }
109
+
84
110
  module.exports = MySQLPlugin
@@ -28,10 +28,12 @@ class NetTCPPlugin extends ClientPlugin {
28
28
  const host = ctx.options.host || 'localhost'
29
29
  const port = ctx.options.port || 0
30
30
  const family = ctx.options.family || 4
31
+ let resource = host
32
+ if (port) resource += `:${port}`
31
33
 
32
34
  this.startSpan('tcp.connect', {
33
35
  service: this.config.service,
34
- resource: [host, port].filter(Boolean).join(':'),
36
+ resource,
35
37
  kind: 'client',
36
38
  meta: {
37
39
  'tcp.remote.host': host,
@@ -11,20 +11,53 @@ const errorPages = new Set(['/404', '/500', '/_error', '/_not-found', '/_not-fou
11
11
  const reusedNextRequestStores = new WeakSet()
12
12
  const nextParentRoutes = new WeakMap()
13
13
 
14
+ /**
15
+ * @typedef {Record<string, unknown> & {
16
+ * span: import('../../dd-trace/src/opentracing/span'),
17
+ * req: import('node:http').IncomingMessage,
18
+ * backgroundRevalidationRequest?: import('node:http').IncomingMessage
19
+ * }} NextRequestStore
20
+ *
21
+ * @typedef {object} NextRequest
22
+ * @property {unknown} [error]
23
+ *
24
+ * @typedef {object} NextRequestContext
25
+ * @property {import('node:http').IncomingMessage} req
26
+ * @property {import('node:http').ServerResponse} res
27
+ * @property {NextRequest} [nextRequest]
28
+ * @property {boolean} [finishOnResponse]
29
+ * @property {NextRequestStore} [currentStore]
30
+ * @property {unknown} [error]
31
+ *
32
+ * @typedef {object} NextErrorContext
33
+ * @property {import('../../dd-trace/src/opentracing/span')} [span]
34
+ * @property {NextRequestStore} [currentStore]
35
+ * @property {import('node:http').IncomingMessage} [req]
36
+ * @property {unknown} error
37
+ */
38
+
14
39
  class NextPlugin extends ServerPlugin {
15
40
  static id = 'next'
16
41
 
17
42
  constructor (...args) {
18
43
  super(...args)
44
+ this.addBind('apm:next:request:background-revalidation', req => ({
45
+ ...storage('legacy').getStore(),
46
+ span: undefined,
47
+ backgroundRevalidationRequest: req,
48
+ }))
19
49
  this.addSub('apm:next:page:load', message => this.pageLoad(message))
20
50
  }
21
51
 
22
- bindStart ({ req, res }) {
52
+ /** @param {NextRequestContext} ctx */
53
+ bindStart (ctx) {
54
+ const { req } = ctx
23
55
  const store = storage('legacy').getStore()
24
56
  const parentSpan = store?.span
25
57
  if (parentSpan?._integrationName === this.constructor.id) {
26
58
  const reusedStore = { ...store, span: parentSpan, req }
27
59
  reusedNextRequestStores.add(reusedStore)
60
+ if (ctx.finishOnResponse) ctx.currentStore = reusedStore
28
61
  return reusedStore
29
62
  }
30
63
 
@@ -34,17 +67,19 @@ class NextPlugin extends ServerPlugin {
34
67
  let serviceSource = this.config.service ? 'opt.plugin' : schemaServiceSource
35
68
  if (!serviceName || serviceName === this.tracer._service) serviceSource = undefined
36
69
 
70
+ const tags = {
71
+ [COMPONENT]: this.constructor.id,
72
+ 'service.name': serviceName,
73
+ 'resource.name': req.method,
74
+ 'span.type': 'web',
75
+ 'span.kind': 'server',
76
+ 'http.method': req.method,
77
+ }
78
+ if (serviceSource !== undefined) tags[SVC_SRC_KEY] = serviceSource
79
+
37
80
  const span = this.tracer.startSpan(this.operationName(), {
38
81
  childOf,
39
- tags: {
40
- [COMPONENT]: this.constructor.id,
41
- 'service.name': serviceName,
42
- 'resource.name': req.method,
43
- 'span.type': 'web',
44
- 'span.kind': 'server',
45
- 'http.method': req.method,
46
- ...(serviceSource === undefined ? undefined : { [SVC_SRC_KEY]: serviceSource }),
47
- },
82
+ tags,
48
83
  integrationName: this.constructor.id,
49
84
  })
50
85
 
@@ -52,31 +87,31 @@ class NextPlugin extends ServerPlugin {
52
87
 
53
88
  analyticsSampler.sample(span, this.config.measured, true)
54
89
 
55
- const isHttpParent = parentSpan?._integrationName === 'http'
56
- const httpParentSpan = isHttpParent ? parentSpan : undefined
57
- const httpParentReq = isHttpParent ? web.getRequest(parentSpan) : undefined
58
- return { ...store, span, req, httpParentSpan, httpParentReq }
90
+ const httpParentSpan = parentSpan?._integrationName === 'http' ? parentSpan : undefined
91
+ const currentStore = { ...store, span, req, httpParentSpan }
92
+ if (ctx.finishOnResponse) ctx.currentStore = currentStore
93
+ return currentStore
59
94
  }
60
95
 
61
- error ({ span, error }) {
62
- if (!span) {
63
- const store = storage('legacy').getStore()
64
- if (!store) return
96
+ /** @param {NextErrorContext} ctx */
97
+ error ({ currentStore, span, req, error }) {
98
+ const store = currentStore ?? storage('legacy').getStore()
99
+ if (req && store?.backgroundRevalidationRequest === req) return
65
100
 
66
- span = store.span
67
- }
68
-
69
- this.addError(error, span)
101
+ span ||= store?.span
102
+ if (span) this.addError(error, span)
70
103
  }
71
104
 
72
- finish ({ req, res, nextRequest = {} }) {
73
- const store = storage('legacy').getStore()
105
+ /** @param {NextRequestContext} ctx */
106
+ finish (ctx) {
107
+ const { req, res, nextRequest = {} } = ctx
108
+ const store = ctx.currentStore ?? storage('legacy').getStore()
74
109
 
75
110
  if (!store) return
76
111
  if (reusedNextRequestStores.has(store)) return
77
112
 
78
113
  const span = store.span
79
- const error = span.context().getTag('error')
114
+ const error = ctx.error ?? span.context().getTag('error')
80
115
  const requestError = req.error || nextRequest.error
81
116
 
82
117
  if (requestError) {
@@ -107,7 +142,7 @@ class NextPlugin extends ServerPlugin {
107
142
 
108
143
  if (!store) return
109
144
 
110
- const { span, req, httpParentReq, httpParentSpan } = store
145
+ const { span, req, httpParentSpan } = store
111
146
 
112
147
  // safeguard against missing req in complicated timeout scenarios
113
148
  if (!req) return
@@ -138,10 +173,9 @@ class NextPlugin extends ServerPlugin {
138
173
  'resource.name': `${req.method} ${page}`.trim(),
139
174
  'next.page': page,
140
175
  })
141
- const routeRequest = httpParentReq || req
142
- web.setRouteOrEndpointTag(routeRequest)
176
+ web.setRouteOrEndpointTag(req)
143
177
  setHttpParentRoute(httpParentSpan, req.method, page, isStatic)
144
- web.setRoute(routeRequest, page)
178
+ web.setRoute(req, page)
145
179
  }
146
180
 
147
181
  configure (config) {
@@ -13,6 +13,12 @@ const {
13
13
  getScreenshotUploadResult,
14
14
  getScreenshotUploadTag,
15
15
  } = require('../../dd-trace/src/ci-visibility/test-screenshot')
16
+ const {
17
+ VIDEO_UPLOAD_RESULT_ERROR,
18
+ VIDEO_UPLOAD_RESULT_UPLOADED,
19
+ getVideoUploadResult,
20
+ getVideoUploadTag,
21
+ } = require('../../dd-trace/src/ci-visibility/test-video')
16
22
 
17
23
  const {
18
24
  finishAllTraceSpans,
@@ -62,6 +68,8 @@ const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetr
62
68
  const log = require('../../dd-trace/src/log')
63
69
 
64
70
  const PLAYWRIGHT_FAILURE_SCREENSHOT_RE = /^test-failed-\d+\.png$/
71
+ const PLAYWRIGHT_VIDEO_CONTENT_TYPES = new Set(['video/mp4', 'video/webm'])
72
+ const noop = () => {}
65
73
 
66
74
  /**
67
75
  * Returns whether an attachment is an automatic Playwright failure screenshot.
@@ -76,10 +84,23 @@ function isPlaywrightFailureScreenshot (attachment) {
76
84
  PLAYWRIGHT_FAILURE_SCREENSHOT_RE.test(basename(attachment.path))
77
85
  }
78
86
 
87
+ /**
88
+ * Returns whether an attachment is a Playwright test video.
89
+ *
90
+ * @param {object} attachment - Playwright test attachment
91
+ * @returns {boolean}
92
+ */
93
+ function isPlaywrightFailureVideo (attachment) {
94
+ return attachment?.name === 'video' &&
95
+ PLAYWRIGHT_VIDEO_CONTENT_TYPES.has(attachment.contentType) &&
96
+ typeof attachment.path === 'string'
97
+ }
98
+
79
99
  class PlaywrightPlugin extends CiPlugin {
80
100
  static id = 'playwright'
81
101
 
82
102
  #isFinalizingAfterError = false
103
+ #finishPendingTestFinishes
83
104
  #pendingTestFinishCallbacks = new Map()
84
105
 
85
106
  constructor (...args) {
@@ -89,7 +110,6 @@ class PlaywrightPlugin extends CiPlugin {
89
110
  this.numFailedTests = 0
90
111
  this.numFailedSuites = 0
91
112
  this.pendingTestFinishes = 0
92
- this.finishSession = undefined
93
113
 
94
114
  this.addSub('ci:playwright:test:is-modified', ({
95
115
  filePath,
@@ -101,29 +121,60 @@ class PlaywrightPlugin extends CiPlugin {
101
121
  onDone(isModified)
102
122
  })
103
123
 
104
- this.addSub('ci:playwright:session:start', ({ isFailureScreenshotEnabled }) => {
124
+ this.addSub('ci:playwright:session:start', ({
125
+ isFailureScreenshotEnabled,
126
+ isFailureVideoEnabled,
127
+ isFailureVideoUploadSupported,
128
+ }) => {
105
129
  this.#isFinalizingAfterError = false
106
- if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED) return
130
+ const testOptimizationConfig = getConfig().testOptimization
107
131
 
108
- if (!isFailureScreenshotEnabled) {
132
+ if (testOptimizationConfig.DD_TEST_FAILURE_SCREENSHOTS_ENABLED && !isFailureScreenshotEnabled) {
109
133
  log.warn(
110
134
  '%s %s',
111
135
  'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Playwright screenshot capture is disabled.',
112
136
  'Set Playwright use.screenshot to "only-on-failure", "on-first-failure", or "on".'
113
137
  )
114
138
  }
139
+ if (testOptimizationConfig.DD_TEST_FAILURE_VIDEOS_ENABLED && !isFailureVideoUploadSupported) {
140
+ log.warn(
141
+ '%s %s',
142
+ 'DD_TEST_FAILURE_VIDEOS_ENABLED is true, but Playwright video upload requires Playwright 1.38.0 or later.',
143
+ 'Upgrade Playwright to enable failure video uploads.'
144
+ )
145
+ } else if (testOptimizationConfig.DD_TEST_FAILURE_VIDEOS_ENABLED && !isFailureVideoEnabled) {
146
+ log.warn(
147
+ '%s %s',
148
+ 'DD_TEST_FAILURE_VIDEOS_ENABLED is true, but Playwright video capture is disabled.',
149
+ 'Set Playwright use.video to "retain-on-failure" or "on".'
150
+ )
151
+ }
115
152
  })
116
153
 
117
- this.addSub('ci:playwright:session:configuration', ({ isFailureScreenshotEnabled }) => {
118
- if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED || !isFailureScreenshotEnabled) return
154
+ this.addSub('ci:playwright:session:configuration', ({
155
+ isFailureScreenshotEnabled,
156
+ isFailureVideoEnabled,
157
+ isFailureVideoUploadSupported,
158
+ }) => {
159
+ const testOptimizationConfig = getConfig().testOptimization
119
160
 
120
- if (!this.tracer._exporter?.canUploadTestScreenshots?.()) {
161
+ if (testOptimizationConfig.DD_TEST_FAILURE_SCREENSHOTS_ENABLED && isFailureScreenshotEnabled &&
162
+ !this.tracer._exporter?.canUploadTestScreenshots?.()) {
121
163
  log.warn(
122
164
  '%s %s',
123
165
  'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Playwright screenshot upload is not supported',
124
166
  'by the active Test Optimization transport.'
125
167
  )
126
168
  }
169
+ if (testOptimizationConfig.DD_TEST_FAILURE_VIDEOS_ENABLED && isFailureVideoEnabled &&
170
+ isFailureVideoUploadSupported &&
171
+ !this.tracer._exporter?.canUploadTestVideos?.()) {
172
+ log.warn(
173
+ '%s %s',
174
+ 'DD_TEST_FAILURE_VIDEOS_ENABLED is true, but Playwright video upload is not supported',
175
+ 'by the active Test Optimization transport.'
176
+ )
177
+ }
127
178
  })
128
179
 
129
180
  this.addSub('ci:playwright:session:finish', ({
@@ -140,13 +191,16 @@ class PlaywrightPlugin extends CiPlugin {
140
191
  testSuiteSpan.setTag(TEST_STATUS, 'fail')
141
192
  testSuiteSpan.setTag('error', error)
142
193
  }
143
- for (const [finishTest, abortController] of this.#pendingTestFinishCallbacks) {
144
- finishTest(SCREENSHOT_UPLOAD_RESULT_ERROR)
145
- abortController.abort()
194
+ for (const [finishTest, pendingTestFinish] of this.#pendingTestFinishCallbacks) {
195
+ finishTest(
196
+ pendingTestFinish.hasScreenshot ? SCREENSHOT_UPLOAD_RESULT_ERROR : undefined,
197
+ pendingTestFinish.hasVideo ? VIDEO_UPLOAD_RESULT_ERROR : undefined
198
+ )
199
+ pendingTestFinish.abortController.abort()
146
200
  }
147
201
  }
148
202
 
149
- const finishSession = () => {
203
+ const finishSession = (flushDone) => {
150
204
  this.testModuleSpan.setTag(TEST_STATUS, status)
151
205
  this.testSessionSpan.setTag(TEST_STATUS, status)
152
206
 
@@ -173,7 +227,6 @@ class PlaywrightPlugin extends CiPlugin {
173
227
  this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
174
228
  }
175
229
 
176
- this.tracer._exporter.exportDeferredTestSuiteSpans?.()
177
230
  this.testModuleSpan.finish()
178
231
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
179
232
  this.testSessionSpan.finish()
@@ -184,16 +237,16 @@ class PlaywrightPlugin extends CiPlugin {
184
237
  autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
185
238
  })
186
239
  appClosingTelemetry()
187
- this.tracer._exporter.flush(onDone)
240
+ this.tracer._exporter.flush(flushDone)
188
241
  this.numFailedTests = 0
189
242
  this.numFailedSuites = 0
190
- this.finishSession = undefined
191
243
  }
192
244
 
193
245
  if (this.pendingTestFinishes > 0) {
194
- this.finishSession = finishSession
246
+ this.#finishPendingTestFinishes = () => this.tracer._exporter.flush(onDone)
247
+ finishSession(noop)
195
248
  } else {
196
- finishSession()
249
+ finishSession(onDone)
197
250
  }
198
251
  })
199
252
 
@@ -255,7 +308,6 @@ class PlaywrightPlugin extends CiPlugin {
255
308
  this.numFailedSuites++
256
309
  }
257
310
 
258
- this.tracer._exporter.deferTestSuiteSpan?.(testSuiteSpan)
259
311
  testSuiteSpan.finish()
260
312
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
261
313
  })
@@ -267,7 +319,7 @@ class PlaywrightPlugin extends CiPlugin {
267
319
  }
268
320
  })
269
321
 
270
- this.addSub('ci:playwright:worker:report', ({ serializedTraces, screenshots }) => {
322
+ this.addSub('ci:playwright:worker:report', ({ serializedTraces, screenshots, videos }) => {
271
323
  const traces = JSON.parse(serializedTraces)
272
324
  const formattedTraces = []
273
325
  let formattedTestSpan
@@ -326,33 +378,70 @@ class PlaywrightPlugin extends CiPlugin {
326
378
  }
327
379
  }
328
380
 
329
- if (!formattedTestSpan || !screenshots || this.#isFinalizingAfterError) {
381
+ if (!formattedTestSpan || (!screenshots && !videos) || this.#isFinalizingAfterError) {
330
382
  exportTraces()
331
383
  return
332
384
  }
333
385
 
334
386
  this.pendingTestFinishes++
335
387
  const abortController = new AbortController()
336
- const finishTest = (screenshotUploadResult) => {
388
+ const finishTest = (screenshotUploadResult, videoUploadResult) => {
337
389
  if (!this.#pendingTestFinishCallbacks.delete(finishTest)) return
338
390
 
339
391
  const screenshotUploadTag = getScreenshotUploadTag(screenshotUploadResult)
340
392
  if (screenshotUploadTag) {
341
393
  formattedTestSpan.meta[screenshotUploadTag] = 'true'
342
394
  }
395
+ const videoUploadTag = getVideoUploadTag(videoUploadResult)
396
+ if (videoUploadTag) {
397
+ formattedTestSpan.meta[videoUploadTag] = 'true'
398
+ }
343
399
  exportTraces()
344
400
  this.pendingTestFinishes--
345
- if (this.pendingTestFinishes === 0 && this.finishSession) {
346
- this.finishSession()
401
+ if (this.pendingTestFinishes === 0 && this.#finishPendingTestFinishes) {
402
+ const finishPendingTestFinishes = this.#finishPendingTestFinishes
403
+ this.#finishPendingTestFinishes = undefined
404
+ finishPendingTestFinishes()
347
405
  }
348
406
  }
349
- this.#pendingTestFinishCallbacks.set(finishTest, abortController)
350
- const uploadStarted = this.uploadTestScreenshots({
407
+ const pendingTestFinish = {
408
+ abortController,
409
+ hasScreenshot: false,
410
+ hasVideo: false,
411
+ }
412
+ this.#pendingTestFinishCallbacks.set(finishTest, pendingTestFinish)
413
+ let pendingMedia = 0
414
+ let screenshotUploadResult
415
+ let videoUploadResult
416
+ const finishMedia = () => {
417
+ pendingMedia--
418
+ if (pendingMedia === 0) finishTest(screenshotUploadResult, videoUploadResult)
419
+ }
420
+ const screenshotUploadStarted = this.uploadTestScreenshots({
351
421
  screenshots,
352
422
  traceId: formattedTestSpan.trace_id.toString(10),
353
423
  signal: abortController.signal,
354
- }, finishTest)
355
- if (uploadStarted) return
424
+ }, (uploadResult) => {
425
+ screenshotUploadResult = uploadResult
426
+ finishMedia()
427
+ })
428
+ if (screenshotUploadStarted) {
429
+ pendingMedia++
430
+ pendingTestFinish.hasScreenshot = true
431
+ }
432
+ const videoUploadStarted = this.uploadTestVideos({
433
+ videos,
434
+ traceId: formattedTestSpan.trace_id.toString(10),
435
+ signal: abortController.signal,
436
+ }, (uploadResult) => {
437
+ videoUploadResult = uploadResult
438
+ finishMedia()
439
+ })
440
+ if (videoUploadStarted) {
441
+ pendingMedia++
442
+ pendingTestFinish.hasVideo = true
443
+ }
444
+ if (pendingMedia > 0) return
356
445
 
357
446
  finishTest()
358
447
  })
@@ -630,7 +719,58 @@ class PlaywrightPlugin extends CiPlugin {
630
719
  }
631
720
  pendingUploads--
632
721
  if (pendingUploads === 0) {
633
- onDone(getScreenshotUploadResult(uploadResults))
722
+ const uploadResult = getScreenshotUploadResult(uploadResults)
723
+ queueMicrotask(() => onDone(uploadResult))
724
+ }
725
+ })
726
+ }
727
+ return true
728
+ }
729
+
730
+ /**
731
+ * Uploads failure videos for a Playwright test attempt.
732
+ *
733
+ * @param {object} options - Upload options
734
+ * @param {Array<object>} options.videos - Playwright test attachments
735
+ * @param {string} options.traceId - Test trace id used as the video key
736
+ * @param {AbortSignal} options.signal - Signal used to cancel uploads during error finalization
737
+ * @param {(result: string|undefined) => void} onDone - Completion callback
738
+ * @returns {boolean} Whether at least one upload was started
739
+ */
740
+ uploadTestVideos ({ videos, traceId, signal }, onDone) {
741
+ const exporter = this.tracer?._exporter
742
+ if (!Array.isArray(videos) || !videos.length ||
743
+ !exporter?.canUploadTestVideos?.() || !exporter.uploadTestVideo) {
744
+ return false
745
+ }
746
+
747
+ const videoPaths = new Set()
748
+ for (const video of videos) {
749
+ if (isPlaywrightFailureVideo(video)) videoPaths.add(video.path)
750
+ }
751
+ if (!videoPaths.size) return false
752
+
753
+ const uploadResults = new Array(videoPaths.size)
754
+ let pendingUploads = videoPaths.size
755
+ let index = 0
756
+ for (const filePath of videoPaths) {
757
+ const resultIndex = index++
758
+ exporter.uploadTestVideo({
759
+ filePath,
760
+ traceId,
761
+ idempotencyKey: `${traceId}:${basename(filePath)}`,
762
+ capturedAtMs: Date.now(),
763
+ signal,
764
+ }, (error, uploaded = true) => {
765
+ if (uploaded) {
766
+ uploadResults[resultIndex] = error
767
+ ? VIDEO_UPLOAD_RESULT_ERROR
768
+ : VIDEO_UPLOAD_RESULT_UPLOADED
769
+ }
770
+ pendingUploads--
771
+ if (pendingUploads === 0) {
772
+ const uploadResult = getVideoUploadResult(uploadResults)
773
+ queueMicrotask(() => onDone(uploadResult))
634
774
  }
635
775
  })
636
776
  }