dd-trace 5.67.0 → 5.69.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 (132) hide show
  1. package/LICENSE-3rdparty.csv +6 -4
  2. package/README.md +0 -2
  3. package/ci/init.js +52 -54
  4. package/ext/exporters.d.ts +2 -1
  5. package/ext/exporters.js +2 -1
  6. package/index.d.ts +240 -3
  7. package/initialize.mjs +1 -1
  8. package/package.json +17 -11
  9. package/packages/datadog-core/src/storage.js +14 -13
  10. package/packages/datadog-esbuild/index.js +118 -26
  11. package/packages/datadog-instrumentations/src/aws-sdk.js +42 -4
  12. package/packages/datadog-instrumentations/src/azure-functions.js +1 -1
  13. package/packages/datadog-instrumentations/src/azure-service-bus.js +1 -1
  14. package/packages/datadog-instrumentations/src/cassandra-driver.js +2 -2
  15. package/packages/datadog-instrumentations/src/connect.js +6 -2
  16. package/packages/datadog-instrumentations/src/cucumber.js +31 -6
  17. package/packages/datadog-instrumentations/src/express.js +5 -6
  18. package/packages/datadog-instrumentations/src/fastify.js +3 -3
  19. package/packages/datadog-instrumentations/src/helpers/hook.js +28 -15
  20. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  21. package/packages/datadog-instrumentations/src/helpers/instrument.js +15 -5
  22. package/packages/datadog-instrumentations/src/helpers/register.js +10 -3
  23. package/packages/datadog-instrumentations/src/http2/client.js +1 -0
  24. package/packages/datadog-instrumentations/src/http2/server.js +0 -1
  25. package/packages/datadog-instrumentations/src/ioredis.js +12 -1
  26. package/packages/datadog-instrumentations/src/jest.js +48 -36
  27. package/packages/datadog-instrumentations/src/limitd-client.js +2 -1
  28. package/packages/datadog-instrumentations/src/mocha/main.js +15 -7
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +2 -1
  31. package/packages/datadog-instrumentations/src/oracledb.js +19 -13
  32. package/packages/datadog-instrumentations/src/pg.js +9 -5
  33. package/packages/datadog-instrumentations/src/pino.js +18 -6
  34. package/packages/datadog-instrumentations/src/playwright.js +15 -1
  35. package/packages/datadog-instrumentations/src/sequelize.js +1 -1
  36. package/packages/datadog-instrumentations/src/vitest.js +155 -62
  37. package/packages/datadog-plugin-ai/src/tracing.js +3 -3
  38. package/packages/datadog-plugin-aws-sdk/src/base.js +23 -8
  39. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +2 -2
  40. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +101 -2
  41. package/packages/datadog-plugin-aws-sdk/src/util.js +1 -1
  42. package/packages/datadog-plugin-confluentinc-kafka-javascript/src/index.js +6 -0
  43. package/packages/datadog-plugin-cucumber/src/index.js +4 -56
  44. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +6 -3
  45. package/packages/datadog-plugin-cypress/src/support.js +4 -0
  46. package/packages/datadog-plugin-express/src/code_origin.js +2 -2
  47. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -2
  48. package/packages/datadog-plugin-jest/src/index.js +0 -21
  49. package/packages/datadog-plugin-mocha/src/index.js +3 -57
  50. package/packages/datadog-plugin-mongodb-core/src/index.js +38 -12
  51. package/packages/datadog-plugin-playwright/src/index.js +11 -5
  52. package/packages/datadog-plugin-vitest/src/index.js +5 -1
  53. package/packages/datadog-plugin-ws/src/close.js +1 -1
  54. package/packages/datadog-plugin-ws/src/producer.js +6 -1
  55. package/packages/datadog-plugin-ws/src/receiver.js +6 -1
  56. package/packages/dd-trace/src/aiguard/client.js +25 -0
  57. package/packages/dd-trace/src/aiguard/noop.js +9 -0
  58. package/packages/dd-trace/src/aiguard/sdk.js +173 -0
  59. package/packages/dd-trace/src/aiguard/tags.js +11 -0
  60. package/packages/dd-trace/src/appsec/iast/path-line.js +21 -4
  61. package/packages/dd-trace/src/appsec/iast/security-controls/parser.js +1 -1
  62. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +6 -3
  63. package/packages/dd-trace/src/appsec/stack_trace.js +20 -1
  64. package/packages/dd-trace/src/appsec/telemetry/waf.js +2 -2
  65. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -4
  66. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +11 -3
  67. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +10 -1
  68. package/packages/dd-trace/src/config-helper.js +8 -1
  69. package/packages/dd-trace/src/config.js +92 -304
  70. package/packages/dd-trace/src/config_defaults.js +191 -0
  71. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -1
  72. package/packages/dd-trace/src/datastreams/fnv.js +2 -2
  73. package/packages/dd-trace/src/datastreams/index.js +23 -1
  74. package/packages/dd-trace/src/datastreams/writer.js +3 -2
  75. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  76. package/packages/dd-trace/src/dogstatsd.js +4 -3
  77. package/packages/dd-trace/src/encode/0.4.js +1 -5
  78. package/packages/dd-trace/src/exporter.js +1 -0
  79. package/packages/dd-trace/src/exporters/agent/index.js +3 -2
  80. package/packages/dd-trace/src/exporters/agent/writer.js +1 -1
  81. package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +3 -2
  82. package/packages/dd-trace/src/exporters/common/request.js +2 -1
  83. package/packages/dd-trace/src/exporters/span-stats/index.js +3 -2
  84. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  85. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +15 -4
  86. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +20 -7
  87. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +40 -13
  88. package/packages/dd-trace/src/llmobs/plugins/openai.js +7 -1
  89. package/packages/dd-trace/src/llmobs/tagger.js +8 -0
  90. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  91. package/packages/dd-trace/src/log/index.js +27 -16
  92. package/packages/dd-trace/src/log/log.js +29 -5
  93. package/packages/dd-trace/src/log/writer.js +5 -5
  94. package/packages/dd-trace/src/noop/proxy.js +4 -0
  95. package/packages/dd-trace/src/noop/span.js +1 -0
  96. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  97. package/packages/dd-trace/src/opentracing/span.js +19 -5
  98. package/packages/dd-trace/src/payload-tagging/config/index.js +16 -0
  99. package/packages/dd-trace/src/payload-tagging/index.js +26 -15
  100. package/packages/dd-trace/src/payload-tagging/tagging.js +17 -8
  101. package/packages/dd-trace/src/pkg.js +3 -1
  102. package/packages/dd-trace/src/plugin_manager.js +20 -2
  103. package/packages/dd-trace/src/plugins/ci_plugin.js +97 -3
  104. package/packages/dd-trace/src/plugins/composite.js +3 -0
  105. package/packages/dd-trace/src/plugins/index.js +2 -0
  106. package/packages/dd-trace/src/plugins/plugin.js +67 -0
  107. package/packages/dd-trace/src/plugins/util/git-cache.js +129 -0
  108. package/packages/dd-trace/src/plugins/util/git.js +41 -27
  109. package/packages/dd-trace/src/plugins/util/test.js +56 -27
  110. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  111. package/packages/dd-trace/src/priority_sampler.js +70 -46
  112. package/packages/dd-trace/src/profiler.js +4 -1
  113. package/packages/dd-trace/src/profiling/config.js +73 -42
  114. package/packages/dd-trace/src/profiling/profiler.js +3 -1
  115. package/packages/dd-trace/src/profiling/profilers/events.js +3 -8
  116. package/packages/dd-trace/src/profiling/profilers/space.js +1 -0
  117. package/packages/dd-trace/src/profiling/profilers/wall.js +196 -117
  118. package/packages/dd-trace/src/proxy.js +15 -0
  119. package/packages/dd-trace/src/rate_limiter.js +26 -1
  120. package/packages/dd-trace/src/remote_config/capabilities.js +5 -0
  121. package/packages/dd-trace/src/remote_config/manager.js +3 -2
  122. package/packages/dd-trace/src/sampling_rule.js +124 -2
  123. package/packages/dd-trace/src/span_sampler.js +19 -0
  124. package/packages/dd-trace/src/standalone/product.js +9 -0
  125. package/packages/dd-trace/src/standalone/tracesource.js +16 -1
  126. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +13 -0
  127. package/packages/dd-trace/src/startup-log.js +21 -2
  128. package/packages/dd-trace/src/supported-configurations.json +9 -0
  129. package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
  130. package/packages/dd-trace/src/util.js +1 -1
  131. package/register.js +1 -1
  132. package/version.js +4 -2
@@ -389,7 +389,6 @@ class CypressPlugin {
389
389
 
390
390
  getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile, isDisabled, isQuarantined }) {
391
391
  const testSuiteTags = {
392
- [TEST_COMMAND]: this.command,
393
392
  [TEST_COMMAND]: this.command,
394
393
  [TEST_MODULE]: TEST_FRAMEWORK_NAME
395
394
  }
@@ -482,8 +481,12 @@ class CypressPlugin {
482
481
  this.isEarlyFlakeDetectionEnabled = false
483
482
  this.isKnownTestsEnabled = false
484
483
  } else {
485
- // We use TEST_FRAMEWORK_NAME for the name of the module
486
- this.knownTestsByTestSuite = knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]
484
+ if (knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]) {
485
+ this.knownTestsByTestSuite = knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]
486
+ } else {
487
+ this.isEarlyFlakeDetectionEnabled = false
488
+ this.isKnownTestsEnabled = false
489
+ }
487
490
  }
488
491
  }
489
492
 
@@ -26,6 +26,10 @@ function safeGetRum (window) {
26
26
  }
27
27
 
28
28
  function isNewTest (test) {
29
+ // If for whatever reason the worker does not receive valid known tests, we don't consider it as new
30
+ if (!Array.isArray(knownTestsForSuite)) {
31
+ return false
32
+ }
29
33
  return !knownTestsForSuite.includes(test.fullTitle())
30
34
  }
31
35
 
@@ -15,7 +15,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
15
15
  this.addSub('apm:express:middleware:enter', ({ req, layer }) => {
16
16
  const tags = layerTags.get(layer)
17
17
  if (!tags) return
18
- web.getContext(req).span?.addTags(tags)
18
+ web.getContext(req)?.span?.addTags(tags)
19
19
  })
20
20
 
21
21
  this.addSub('apm:express:route:added', ({ topOfStackFunc, layer }) => {
@@ -26,7 +26,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
26
26
  this.addSub('apm:router:middleware:enter', ({ req, layer }) => {
27
27
  const tags = layerTags.get(layer)
28
28
  if (!tags) return
29
- web.getContext(req).span?.addTags(tags)
29
+ web.getContext(req)?.span?.addTags(tags)
30
30
  })
31
31
 
32
32
  this.addSub('apm:router:route:added', ({ topOfStackFunc, layer }) => {
@@ -15,8 +15,7 @@ class FastifyCodeOriginForSpansPlugin extends Plugin {
15
15
  this.addSub('apm:fastify:request:handle', ({ req, routeConfig }) => {
16
16
  const tags = routeConfig?.[kCodeOriginForSpansTagsSym]
17
17
  if (!tags) return
18
- const context = web.getContext(req)
19
- context.span?.addTags(tags)
18
+ web.getContext(req)?.span?.addTags(tags)
20
19
  })
21
20
 
22
21
  this.addSub('apm:fastify:route:added', ({ routeOptions, onRoute }) => {
@@ -261,21 +261,6 @@ class JestPlugin extends CiPlugin {
261
261
  }
262
262
  })
263
263
 
264
- this.addSub('ci:jest:worker-report:trace', traces => {
265
- const formattedTraces = JSON.parse(traces).map(trace =>
266
- trace.map(span => ({
267
- ...span,
268
- span_id: id(span.span_id),
269
- trace_id: id(span.trace_id),
270
- parent_id: id(span.parent_id)
271
- }))
272
- )
273
-
274
- formattedTraces.forEach(trace => {
275
- this.tracer._exporter.export(trace)
276
- })
277
- })
278
-
279
264
  this.addSub('ci:jest:worker-report:coverage', data => {
280
265
  const formattedCoverages = JSON.parse(data).map(coverage => ({
281
266
  sessionId: id(coverage.sessionId),
@@ -287,12 +272,6 @@ class JestPlugin extends CiPlugin {
287
272
  })
288
273
  })
289
274
 
290
- this.addSub('ci:jest:worker-report:logs', (logsPayloads) => {
291
- JSON.parse(logsPayloads).forEach(({ testConfiguration, logMessage }) => {
292
- this.tracer._exporter.exportDiLogs(testConfiguration, logMessage)
293
- })
294
- })
295
-
296
275
  this.addSub('ci:jest:test-suite:finish', ({ status, errorMessage, error }) => {
297
276
  this.testSuiteSpan.setTag(TEST_STATUS, status)
298
277
  if (error) {
@@ -22,12 +22,6 @@ const {
22
22
  TEST_IS_RETRY,
23
23
  TEST_EARLY_FLAKE_ENABLED,
24
24
  TEST_EARLY_FLAKE_ABORT_REASON,
25
- TEST_SESSION_ID,
26
- TEST_MODULE_ID,
27
- TEST_MODULE,
28
- TEST_SUITE_ID,
29
- TEST_COMMAND,
30
- TEST_SUITE,
31
25
  MOCHA_IS_PARALLEL,
32
26
  TEST_IS_RUM_ACTIVE,
33
27
  TEST_BROWSER_DRIVER,
@@ -54,32 +48,15 @@ const {
54
48
  TELEMETRY_CODE_COVERAGE_NUM_FILES,
55
49
  TELEMETRY_TEST_SESSION
56
50
  } = require('../../dd-trace/src/ci-visibility/telemetry')
57
- const id = require('../../dd-trace/src/id')
58
- const log = require('../../dd-trace/src/log')
59
51
 
60
52
  const BREAKPOINT_SET_GRACE_PERIOD_MS = 200
61
53
 
62
- function getTestSuiteLevelVisibilityTags (testSuiteSpan) {
63
- const testSuiteSpanContext = testSuiteSpan.context()
64
- const suiteTags = {
65
- [TEST_SUITE_ID]: testSuiteSpanContext.toSpanId(),
66
- [TEST_SESSION_ID]: testSuiteSpanContext.toTraceId(),
67
- [TEST_COMMAND]: testSuiteSpanContext._tags[TEST_COMMAND],
68
- [TEST_MODULE]: 'mocha'
69
- }
70
- if (testSuiteSpanContext._parentId) {
71
- suiteTags[TEST_MODULE_ID] = testSuiteSpanContext._parentId.toString(10)
72
- }
73
- return suiteTags
74
- }
75
-
76
54
  class MochaPlugin extends CiPlugin {
77
55
  static id = 'mocha'
78
56
 
79
57
  constructor (...args) {
80
58
  super(...args)
81
59
 
82
- this._testSuites = new Map()
83
60
  this._testTitleToParams = {}
84
61
  this.sourceRoot = process.cwd()
85
62
 
@@ -88,7 +65,7 @@ class MochaPlugin extends CiPlugin {
88
65
  return
89
66
  }
90
67
  const testSuite = getTestSuitePath(suiteFile, this.sourceRoot)
91
- const testSuiteSpan = this._testSuites.get(testSuite)
68
+ const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuite)
92
69
 
93
70
  if (!coverageFiles.length) {
94
71
  this.telemetry.count(TELEMETRY_CODE_COVERAGE_EMPTY)
@@ -163,7 +140,7 @@ class MochaPlugin extends CiPlugin {
163
140
  const store = storage('legacy').getStore()
164
141
  ctx.parentStore = store
165
142
  ctx.currentStore = { ...store, testSuiteSpan }
166
- this._testSuites.set(testSuite, testSuiteSpan)
143
+ this._testSuiteSpansByTestSuite.set(testSuite, testSuiteSpan)
167
144
  })
168
145
 
169
146
  this.addSub('ci:mocha:test-suite:finish', ({ testSuiteSpan, status }) => {
@@ -436,37 +413,6 @@ class MochaPlugin extends CiPlugin {
436
413
  this.tracer._exporter.flush()
437
414
  })
438
415
 
439
- this.addSub('ci:mocha:worker-report:trace', (traces) => {
440
- const formattedTraces = JSON.parse(traces).map(trace =>
441
- trace.map(span => {
442
- const formattedSpan = {
443
- ...span,
444
- span_id: id(span.span_id),
445
- trace_id: id(span.trace_id),
446
- parent_id: id(span.parent_id)
447
- }
448
- if (formattedSpan.name === 'mocha.test') {
449
- const testSuite = span.meta[TEST_SUITE]
450
- const testSuiteSpan = this._testSuites.get(testSuite)
451
- if (!testSuiteSpan) {
452
- log.warn('Test suite span not found for test span with test suite', testSuite)
453
- return formattedSpan
454
- }
455
- const suiteTags = getTestSuiteLevelVisibilityTags(testSuiteSpan)
456
- formattedSpan.meta = {
457
- ...formattedSpan.meta,
458
- ...suiteTags
459
- }
460
- }
461
- return formattedSpan
462
- })
463
- )
464
-
465
- formattedTraces.forEach(trace => {
466
- this.tracer._exporter.export(trace)
467
- })
468
- })
469
-
470
416
  this.addBind('ci:mocha:global:run', (ctx) => {
471
417
  return ctx.currentStore
472
418
  })
@@ -522,7 +468,7 @@ class MochaPlugin extends CiPlugin {
522
468
  }
523
469
 
524
470
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.sourceRoot)
525
- const testSuiteSpan = this._testSuites.get(testSuite)
471
+ const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuite)
526
472
 
527
473
  extraTags[TEST_SOURCE_FILE] = this.repositoryRoot !== this.sourceRoot && !!this.repositoryRoot
528
474
  ? getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
@@ -2,7 +2,6 @@
2
2
 
3
3
  const { isTrue } = require('../../dd-trace/src/util')
4
4
  const DatabasePlugin = require('../../dd-trace/src/plugins/database')
5
- const coalesce = require('koalas')
6
5
  const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
7
6
 
8
7
  class MongodbCorePlugin extends DatabasePlugin {
@@ -10,23 +9,26 @@ class MongodbCorePlugin extends DatabasePlugin {
10
9
  static component = 'mongodb'
11
10
  // avoid using db.name for peer.service since it includes the collection name
12
11
  // should be removed if one day this will be fixed
12
+ /**
13
+ * @override
14
+ */
13
15
  static peerServicePrecursors = []
14
16
 
17
+ /**
18
+ * @override
19
+ */
15
20
  configure (config) {
16
21
  super.configure(config)
17
22
 
18
23
  const heartbeatFromEnv = getEnvironmentVariable('DD_TRACE_MONGODB_HEARTBEAT_ENABLED')
19
24
 
20
- this.config.heartbeatEnabled = coalesce(
21
- config.heartbeatEnabled,
22
- heartbeatFromEnv && isTrue(heartbeatFromEnv),
25
+ this.config.heartbeatEnabled = config.heartbeatEnabled ??
26
+ (heartbeatFromEnv && isTrue(heartbeatFromEnv)) ??
23
27
  true
24
- )
25
28
  }
26
29
 
27
30
  bindStart (ctx) {
28
31
  const { ns, ops, options = {}, name } = ctx
29
-
30
32
  // heartbeat commands can be disabled if this.config.heartbeatEnabled is false
31
33
  if (!this.config.heartbeatEnabled && isHeartbeat(ops, this.config)) {
32
34
  return
@@ -55,11 +57,18 @@ class MongodbCorePlugin extends DatabasePlugin {
55
57
  return ctx.currentStore
56
58
  }
57
59
 
60
+ /**
61
+ * @override
62
+ */
58
63
  getPeerService (tags) {
59
- const ns = tags['db.name']
64
+ let ns = tags['db.name']
60
65
  if (ns && tags['peer.service'] === undefined) {
66
+ const dotIndex = ns.indexOf('.')
67
+ if (dotIndex !== -1) {
68
+ ns = ns.slice(0, dotIndex)
69
+ }
61
70
  // the mongo ns is either dbName either dbName.collection. So we keep the first part
62
- tags['peer.service'] = ns.split('.', 1)[0]
71
+ tags['peer.service'] = ns
63
72
  }
64
73
  return super.getPeerService(tags)
65
74
  }
@@ -90,21 +99,38 @@ function sanitizeBigInt (data) {
90
99
  return JSON.stringify(data, (_key, value) => typeof value === 'bigint' ? value.toString() : value)
91
100
  }
92
101
 
102
+ function extractQuery (statements) {
103
+ if (statements.length === 1 && statements[0].q) return statements[0].q
104
+
105
+ const extractedQueries = []
106
+ for (let i = 0; i < statements.length; i++) {
107
+ if (statements[i].q) {
108
+ extractedQueries.push(limitDepth(statements[i].q))
109
+ }
110
+ }
111
+
112
+ return extractedQueries
113
+ }
114
+
93
115
  function getQuery (cmd) {
94
- if (!cmd || typeof cmd !== 'object' || Array.isArray(cmd)) return
116
+ if (!cmd || (typeof cmd !== 'object' && !Array.isArray(cmd))) return
117
+
118
+ if (Array.isArray(cmd)) return sanitizeBigInt(extractQuery(cmd))
95
119
  if (cmd.query) return sanitizeBigInt(limitDepth(cmd.query))
96
120
  if (cmd.filter) return sanitizeBigInt(limitDepth(cmd.filter))
97
121
  if (cmd.pipeline) return sanitizeBigInt(limitDepth(cmd.pipeline))
122
+ if (cmd.deletes) return sanitizeBigInt(extractQuery(cmd.deletes))
123
+ if (cmd.updates) return sanitizeBigInt(extractQuery(cmd.updates))
98
124
  }
99
125
 
100
126
  function getResource (plugin, ns, query, operationName) {
101
- const parts = [operationName, ns]
127
+ let resource = `${operationName} ${ns}`
102
128
 
103
129
  if (plugin.config.queryInResourceName && query) {
104
- parts.push(query)
130
+ resource += ` ${query}`
105
131
  }
106
132
 
107
- return parts.join(' ')
133
+ return resource
108
134
  }
109
135
 
110
136
  function truncate (input) {
@@ -17,6 +17,7 @@ const {
17
17
  TEST_IS_NEW,
18
18
  TEST_IS_RETRY,
19
19
  TEST_EARLY_FLAKE_ENABLED,
20
+ TEST_EARLY_FLAKE_ABORT_REASON,
20
21
  TELEMETRY_TEST_SESSION,
21
22
  TEST_RETRY_REASON,
22
23
  TEST_MANAGEMENT_IS_QUARANTINED,
@@ -53,7 +54,7 @@ class PlaywrightPlugin extends CiPlugin {
53
54
  constructor (...args) {
54
55
  super(...args)
55
56
 
56
- this._testSuites = new Map()
57
+ this._testSuiteSpansByTestSuiteAbsolutePath = new Map()
57
58
  this.numFailedTests = 0
58
59
  this.numFailedSuites = 0
59
60
 
@@ -70,6 +71,7 @@ class PlaywrightPlugin extends CiPlugin {
70
71
  this.addSub('ci:playwright:session:finish', ({
71
72
  status,
72
73
  isEarlyFlakeDetectionEnabled,
74
+ isEarlyFlakeDetectionFaulty,
73
75
  isTestManagementTestsEnabled,
74
76
  onDone
75
77
  }) => {
@@ -79,7 +81,9 @@ class PlaywrightPlugin extends CiPlugin {
79
81
  if (isEarlyFlakeDetectionEnabled) {
80
82
  this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
81
83
  }
82
-
84
+ if (isEarlyFlakeDetectionFaulty) {
85
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
86
+ }
83
87
  if (this.numFailedSuites > 0) {
84
88
  let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
85
89
  if (this.numFailedTests > 0) {
@@ -142,7 +146,7 @@ class PlaywrightPlugin extends CiPlugin {
142
146
  ctx.parentStore = store
143
147
  ctx.currentStore = { ...store, testSuiteSpan }
144
148
 
145
- this._testSuites.set(testSuiteAbsolutePath, testSuiteSpan)
149
+ this._testSuiteSpansByTestSuiteAbsolutePath.set(testSuiteAbsolutePath, testSuiteSpan)
146
150
 
147
151
  return ctx.currentStore
148
152
  })
@@ -247,7 +251,9 @@ class PlaywrightPlugin extends CiPlugin {
247
251
  formattedSpan.meta[TEST_COMMAND] = this.command
248
252
  formattedSpan.meta[TEST_MODULE] = this.constructor.id
249
253
  // MISSING _trace.startTime and _trace.ticks - because by now the suite is already serialized
250
- const testSuite = this._testSuites.get(formattedSpan.meta.test_suite_absolute_path)
254
+ const testSuite = this._testSuiteSpansByTestSuiteAbsolutePath.get(
255
+ formattedSpan.meta.test_suite_absolute_path
256
+ )
251
257
  if (testSuite) {
252
258
  formattedSpan.meta[TEST_SUITE_ID] = testSuite.context().toSpanId()
253
259
  }
@@ -391,7 +397,7 @@ class PlaywrightPlugin extends CiPlugin {
391
397
 
392
398
  // TODO: this runs both in worker and main process (main process: skipped tests that do not go through _runTest)
393
399
  startTestSpan (testName, testSuiteAbsolutePath, testSuite, testSourceFile, testSourceLine, browserName) {
394
- const testSuiteSpan = this._testSuites.get(testSuiteAbsolutePath)
400
+ const testSuiteSpan = this._testSuiteSpansByTestSuiteAbsolutePath.get(testSuiteAbsolutePath)
395
401
 
396
402
  const extraTags = {
397
403
  [TEST_SOURCE_START]: testSourceLine
@@ -55,8 +55,12 @@ class VitestPlugin extends CiPlugin {
55
55
  this.taskToFinishTime = new WeakMap()
56
56
 
57
57
  this.addSub('ci:vitest:test:is-new', ({ knownTests, testSuiteAbsolutePath, testName, onDone }) => {
58
+ // if for whatever reason the worker does not receive valid known tests, we don't consider it as new
59
+ if (!knownTests.vitest) {
60
+ return onDone(false)
61
+ }
58
62
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
59
- const testsForThisTestSuite = knownTests[testSuite] || []
63
+ const testsForThisTestSuite = knownTests.vitest[testSuite] || []
60
64
  onDone(!testsForThisTestSuite.includes(testName))
61
65
  })
62
66
 
@@ -60,7 +60,7 @@ class WSClosePlugin extends TracingPlugin {
60
60
  end (ctx) {
61
61
  if (!Object.hasOwn(ctx, 'result')) return
62
62
 
63
- if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext)
63
+ if (ctx.socket.spanContext) ctx.span.addLink({ context: ctx.socket.spanContext })
64
64
 
65
65
  ctx.span.finish()
66
66
  }
@@ -50,7 +50,12 @@ class WSProducerPlugin extends TracingPlugin {
50
50
  end (ctx) {
51
51
  if (!Object.hasOwn(ctx, 'result')) return
52
52
 
53
- if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext, { 'dd.kind': 'resuming' })
53
+ if (ctx.socket.spanContext) {
54
+ ctx.span.addLink({
55
+ context: ctx.socket.spanContext,
56
+ attributes: { 'dd.kind': 'resuming' },
57
+ })
58
+ }
54
59
 
55
60
  ctx.span.finish()
56
61
  return ctx.parentStore
@@ -60,7 +60,12 @@ class WSReceiverPlugin extends TracingPlugin {
60
60
  end (ctx) {
61
61
  if (!Object.hasOwn(ctx, 'result')) return
62
62
 
63
- if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext, { 'dd.kind': 'executed_by' })
63
+ if (ctx.socket.spanContext) {
64
+ ctx.span.addLink({
65
+ context: ctx.socket.spanContext,
66
+ attributes: { 'dd.kind': 'executed_by' },
67
+ })
68
+ }
64
69
 
65
70
  ctx.span.finish()
66
71
  return ctx.parentStore
@@ -0,0 +1,25 @@
1
+ 'use strict'
2
+
3
+ async function executeRequest (body, opts) {
4
+ const postData = JSON.stringify(body)
5
+ const headers = {
6
+ 'Content-Type': 'application/json',
7
+ 'Content-Length': Buffer.byteLength(postData),
8
+ ...opts.headers
9
+ }
10
+
11
+ const response = await fetch(opts.url, {
12
+ method: 'POST',
13
+ headers,
14
+ body: postData,
15
+ signal: AbortSignal.timeout(opts.timeout)
16
+ })
17
+
18
+ const responseBody = await response.json()
19
+ return {
20
+ status: response.status,
21
+ body: responseBody
22
+ }
23
+ }
24
+
25
+ module.exports = executeRequest
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ class NoopAIGuard {
4
+ evaluate (messages, opts) {
5
+ return Promise.resolve({ action: 'ALLOW', reason: 'AI Guard is not enabled' })
6
+ }
7
+ }
8
+
9
+ module.exports = NoopAIGuard
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ const NoopAIGuard = require('./noop')
4
+ const executeRequest = require('./client')
5
+ const {
6
+ AI_GUARD_RESOURCE,
7
+ AI_GUARD_TARGET_TAG_KEY,
8
+ AI_GUARD_REASON_TAG_KEY,
9
+ AI_GUARD_ACTION_TAG_KEY,
10
+ AI_GUARD_BLOCKED_TAG_KEY,
11
+ AI_GUARD_META_STRUCT_KEY,
12
+ AI_GUARD_TOOL_NAME_TAG_KEY
13
+ } = require('./tags')
14
+ const log = require('../log')
15
+
16
+ const ALLOW = 'ALLOW'
17
+
18
+ class AIGuardAbortError extends Error {
19
+ constructor (reason) {
20
+ super(reason)
21
+ this.name = 'AIGuardAbortError'
22
+ this.reason = reason
23
+ }
24
+ }
25
+
26
+ class AIGuardClientError extends Error {
27
+ constructor (message, opts = {}) {
28
+ super(message)
29
+ this.name = 'AIGuardClientError'
30
+ if (opts.errors) {
31
+ this.errors = opts.errors
32
+ }
33
+ if (opts.cause) {
34
+ this.cause = opts.cause
35
+ }
36
+ }
37
+ }
38
+
39
+ class AIGuard extends NoopAIGuard {
40
+ #initialized
41
+ #tracer
42
+ #headers
43
+ #evaluateUrl
44
+ #timeout
45
+ #maxMessagesLength
46
+ #maxContentSize
47
+ #meta
48
+
49
+ constructor (tracer, config) {
50
+ super()
51
+
52
+ if (!config.apiKey || !config.appKey) {
53
+ log.error('AIGuard: missing api and/or app keys, use env DD_API_KEY and DD_APP_KEY')
54
+ this.#initialized = false
55
+ return
56
+ }
57
+ this.#tracer = tracer
58
+ this.#headers = {
59
+ 'DD-API-KEY': config.apiKey,
60
+ 'DD-APPLICATION-KEY': config.appKey,
61
+ }
62
+ const endpoint = config.experimental.aiguard.endpoint || `https://app.${config.site}/api/v2/ai-guard`
63
+ this.#evaluateUrl = `${endpoint}/evaluate`
64
+ this.#timeout = config.experimental.aiguard.timeout
65
+ this.#maxMessagesLength = config.experimental.aiguard.maxMessagesLength
66
+ this.#maxContentSize = config.experimental.aiguard.maxContentSize
67
+ this.#meta = { service: config.service, env: config.env }
68
+ this.#initialized = true
69
+ }
70
+
71
+ #truncate (messages) {
72
+ const size = Math.min(messages.length, this.#maxMessagesLength)
73
+ const result = messages.slice(-size)
74
+
75
+ for (let i = 0; i < size; i++) {
76
+ const message = result[i]
77
+ if (message.content?.length > this.#maxContentSize) {
78
+ result[i] = { ...message, content: message.content.slice(0, this.#maxContentSize) }
79
+ }
80
+ }
81
+ return result
82
+ }
83
+
84
+ #isToolCall (message) {
85
+ return message.tool_calls || message.tool_call_id
86
+ }
87
+
88
+ #getToolName (message, history) {
89
+ // 1. assistant message with tool calls
90
+ if (message.tool_calls) {
91
+ const names = message.tool_calls.map((tool) => tool.function.name)
92
+ return names.length === 0 ? null : names.join(',')
93
+ }
94
+ // 2. assistant message with tool output (search the linked tool call in reverse order)
95
+ const id = message.tool_call_id
96
+ for (let i = history.length - 2; i >= 0; i--) {
97
+ const item = history[i]
98
+ if (item.tool_calls) {
99
+ for (const toolCall of item.tool_calls) {
100
+ if (toolCall.id === id) {
101
+ return toolCall.function.name
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return null
107
+ }
108
+
109
+ evaluate (messages, opts) {
110
+ if (!this.#initialized) {
111
+ return super.evaluate(messages, opts)
112
+ }
113
+ const { block = false } = opts ?? {}
114
+ return this.#tracer.trace(AI_GUARD_RESOURCE, {}, async (span) => {
115
+ const last = messages[messages.length - 1]
116
+ const target = this.#isToolCall(last) ? 'tool' : 'prompt'
117
+ span.setTag(AI_GUARD_TARGET_TAG_KEY, target)
118
+ if (target === 'tool') {
119
+ const name = this.#getToolName(last, messages)
120
+ if (name) {
121
+ span.setTag(AI_GUARD_TOOL_NAME_TAG_KEY, name)
122
+ }
123
+ }
124
+ span.meta_struct = {
125
+ [AI_GUARD_META_STRUCT_KEY]: {
126
+ messages: this.#truncate(messages)
127
+ }
128
+ }
129
+ let response
130
+ try {
131
+ const payload = {
132
+ data: {
133
+ attributes: {
134
+ messages,
135
+ meta: this.#meta,
136
+ }
137
+ }
138
+ }
139
+ response = await executeRequest(
140
+ payload,
141
+ { url: this.#evaluateUrl, headers: this.#headers, timeout: this.#timeout })
142
+ } catch (e) {
143
+ throw new AIGuardClientError('Unexpected error calling AI Guard service', { cause: e })
144
+ }
145
+ if (response.status !== 200) {
146
+ throw new AIGuardClientError(
147
+ `AI Guard service call failed, status ${response.status}`,
148
+ { errors: response.body?.errors })
149
+ }
150
+ let action, reason, blockingEnabled
151
+ try {
152
+ const attr = response.body.data.attributes
153
+ if (!attr.action) {
154
+ throw new Error('Action missing from response')
155
+ }
156
+ action = attr.action
157
+ reason = attr.reason
158
+ blockingEnabled = attr.is_blocking_enabled ?? false
159
+ } catch (e) {
160
+ throw new AIGuardClientError(`AI Guard service returned unexpected response : ${response.body}`, { cause: e })
161
+ }
162
+ span.setTag(AI_GUARD_ACTION_TAG_KEY, action)
163
+ span.setTag(AI_GUARD_REASON_TAG_KEY, reason)
164
+ if (block && blockingEnabled && action !== ALLOW) {
165
+ span.setTag(AI_GUARD_BLOCKED_TAG_KEY, 'true')
166
+ throw new AIGuardAbortError(reason)
167
+ }
168
+ return { action, reason }
169
+ })
170
+ }
171
+ }
172
+
173
+ module.exports = AIGuard
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ AI_GUARD_RESOURCE: 'ai_guard',
5
+ AI_GUARD_TARGET_TAG_KEY: 'ai_guard.target',
6
+ AI_GUARD_TOOL_NAME_TAG_KEY: 'ai_guard.tool_name',
7
+ AI_GUARD_ACTION_TAG_KEY: 'ai_guard.action',
8
+ AI_GUARD_REASON_TAG_KEY: 'ai_guard.reason',
9
+ AI_GUARD_BLOCKED_TAG_KEY: 'ai_guard.blocked',
10
+ AI_GUARD_META_STRUCT_KEY: 'ai_guard'
11
+ }