dd-trace 5.111.0 → 5.112.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 (77) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/index.d.ts +21 -1
  3. package/package.json +32 -29
  4. package/packages/datadog-instrumentations/src/ai.js +45 -0
  5. package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
  6. package/packages/datadog-instrumentations/src/child_process.js +3 -3
  7. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +102 -43
  9. package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
  10. package/packages/datadog-instrumentations/src/fastify.js +27 -8
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  12. package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  16. package/packages/datadog-instrumentations/src/jest.js +713 -63
  17. package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
  18. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
  19. package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
  20. package/packages/datadog-instrumentations/src/mongodb.js +74 -0
  21. package/packages/datadog-instrumentations/src/mongoose.js +4 -5
  22. package/packages/datadog-instrumentations/src/playwright.js +13 -9
  23. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
  24. package/packages/datadog-instrumentations/src/vitest-main.js +28 -1
  25. package/packages/datadog-instrumentations/src/vitest-util.js +5 -0
  26. package/packages/datadog-instrumentations/src/vitest-worker.js +104 -34
  27. package/packages/datadog-plugin-ai/src/index.js +1 -1
  28. package/packages/datadog-plugin-ai/src/tracing.js +66 -3
  29. package/packages/datadog-plugin-ai/src/utils.js +17 -4
  30. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
  31. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
  32. package/packages/datadog-plugin-child_process/src/index.js +13 -2
  33. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
  34. package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
  35. package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
  36. package/packages/datadog-plugin-cucumber/src/index.js +14 -23
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
  38. package/packages/datadog-plugin-cypress/src/index.js +47 -2
  39. package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
  40. package/packages/datadog-plugin-graphql/src/execute.js +21 -10
  41. package/packages/datadog-plugin-graphql/src/index.js +17 -1
  42. package/packages/datadog-plugin-graphql/src/utils.js +9 -2
  43. package/packages/datadog-plugin-graphql/src/validate.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +30 -14
  45. package/packages/datadog-plugin-mocha/src/index.js +39 -14
  46. package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
  47. package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
  48. package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +3 -2
  50. package/packages/datadog-plugin-vitest/src/index.js +51 -9
  51. package/packages/dd-trace/src/aiguard/index.js +9 -14
  52. package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
  53. package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
  54. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
  55. package/packages/dd-trace/src/appsec/graphql.js +9 -6
  56. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
  57. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
  58. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
  59. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
  60. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
  61. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
  62. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
  63. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  64. package/packages/dd-trace/src/config/supported-configurations.json +37 -0
  65. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  66. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
  67. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
  68. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
  69. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
  70. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
  71. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
  72. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  73. package/packages/dd-trace/src/plugin_manager.js +16 -0
  74. package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
  75. package/packages/dd-trace/src/plugins/index.js +2 -0
  76. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  77. package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
@@ -0,0 +1,452 @@
1
+ 'use strict'
2
+
3
+ const { isMap, isRegExp } = require('node:util').types
4
+
5
+ const DatabasePlugin = require('../../dd-trace/src/plugins/database')
6
+
7
+ class MongodbCoreQueryPlugin extends DatabasePlugin {
8
+ static id = 'mongodb-core'
9
+ static component = 'mongodb'
10
+ // avoid using db.name for peer.service since it includes the collection name
11
+ // should be removed if one day this will be fixed
12
+ /**
13
+ * @override
14
+ */
15
+ static peerServicePrecursors = []
16
+
17
+ /**
18
+ * @override
19
+ */
20
+ configure (config) {
21
+ super.configure(config)
22
+
23
+ this.config.heartbeatEnabled = config.heartbeatEnabled ??
24
+ this._tracerConfig.DD_TRACE_MONGODB_HEARTBEAT_ENABLED
25
+ this.config.obfuscateQuery = normaliseObfuscateQuery(
26
+ config.obfuscateQuery ?? this._tracerConfig.DD_TRACE_MONGODB_OBFUSCATE_QUERY
27
+ )
28
+ }
29
+
30
+ bindStart (ctx) {
31
+ const { ns, ops, options = {}, name } = ctx
32
+ // heartbeat commands can be disabled if this.config.heartbeatEnabled is false
33
+ if (!this.config.heartbeatEnabled && isHeartbeat(ops, this.config)) {
34
+ return
35
+ }
36
+ const query = getQuery(ops, this.config.obfuscateQuery)
37
+ const resource = truncate(getResource(this, ns, query, name))
38
+ const serviceResult = this.serviceName({ pluginConfig: this.config })
39
+ const span = this.startSpan(this.operationName(), {
40
+ service: serviceResult,
41
+ resource,
42
+ type: 'mongodb',
43
+ kind: 'client',
44
+ meta: {
45
+ // this is not technically correct since it includes the collection but we changing will break customer stuff
46
+ 'db.name': ns,
47
+ 'mongodb.query': query,
48
+ 'out.host': options.host,
49
+ 'out.port': options.port,
50
+ },
51
+ }, ctx)
52
+ const comment = this.injectDbmComment(span, ops.comment, serviceResult.name)
53
+ if (comment) {
54
+ ops.comment = comment
55
+ }
56
+
57
+ return ctx.currentStore
58
+ }
59
+
60
+ /**
61
+ * @override
62
+ */
63
+ getPeerService (tags) {
64
+ let ns = tags['db.name']
65
+ if (ns && tags['peer.service'] === undefined) {
66
+ const dotIndex = ns.indexOf('.')
67
+ if (dotIndex !== -1) {
68
+ ns = ns.slice(0, dotIndex)
69
+ }
70
+ // the mongo ns is either dbName either dbName.collection. So we keep the first part
71
+ tags['peer.service'] = ns
72
+ }
73
+ return super.getPeerService(tags)
74
+ }
75
+
76
+ injectDbmComment (span, comment, serviceName) {
77
+ const dbmTraceComment = this.createDbmComment(span, serviceName)
78
+
79
+ if (!dbmTraceComment) {
80
+ return comment
81
+ }
82
+
83
+ if (comment) {
84
+ // if the command already has a comment, append the dbm trace comment
85
+ if (typeof comment === 'string') {
86
+ comment += `,${dbmTraceComment}`
87
+ } else if (Array.isArray(comment)) {
88
+ comment.push(dbmTraceComment)
89
+ } // do nothing if the comment is not a string or an array
90
+ } else {
91
+ comment = dbmTraceComment
92
+ }
93
+
94
+ return comment
95
+ }
96
+ }
97
+
98
+ const MAX_DEPTH = 10
99
+ const MAX_QUERY_LENGTH = 10_000
100
+
101
+ function extractQuery (statements) {
102
+ if (statements.length === 1 && statements[0].q) return statements[0].q
103
+
104
+ const extractedQueries = []
105
+ for (let i = 0; i < statements.length; i++) {
106
+ if (statements[i].q) {
107
+ extractedQueries.push(statements[i].q)
108
+ }
109
+ }
110
+
111
+ return extractedQueries
112
+ }
113
+
114
+ /**
115
+ * @param {Record<string, unknown> | unknown[] | undefined} cmd
116
+ * @param {'none' | 'types' | 'redact'} mode
117
+ */
118
+ function getQuery (cmd, mode) {
119
+ if (!cmd || (typeof cmd !== 'object' && !Array.isArray(cmd))) return
120
+
121
+ if (Array.isArray(cmd)) return sanitiseAndStringify(extractQuery(cmd), mode)
122
+ if (cmd.query) return sanitiseAndStringify(cmd.query, mode)
123
+ if (cmd.filter) return sanitiseAndStringify(cmd.filter, mode)
124
+ if (cmd.pipeline) return sanitiseAndStringify(cmd.pipeline, mode)
125
+ if (cmd.deletes) return sanitiseAndStringify(extractQuery(cmd.deletes), mode)
126
+ if (cmd.updates) return sanitiseAndStringify(extractQuery(cmd.updates), mode)
127
+ }
128
+
129
+ function getResource (plugin, ns, query, operationName) {
130
+ let resource = `${operationName} ${ns}`
131
+
132
+ if (plugin.config.queryInResourceName && query) {
133
+ resource += ` ${query}`
134
+ }
135
+
136
+ return resource
137
+ }
138
+
139
+ function truncate (input) {
140
+ return input.length > MAX_QUERY_LENGTH ? input.slice(0, MAX_QUERY_LENGTH) : input
141
+ }
142
+
143
+ // Depth doubles as the cycle bound: a cycle pushes past MAX_DEPTH and bails,
144
+ // after which the slow path catches it via its ancestor stack.
145
+ /** @param {unknown} input */
146
+ function canStringifyDirect (input) {
147
+ if (input === null ||
148
+ typeof input !== 'object' ||
149
+ ArrayBuffer.isView(input) ||
150
+ input._bsontype !== undefined ||
151
+ isRegExp(input) ||
152
+ isMap(input) ||
153
+ typeof input.toJSON === 'function') {
154
+ return false
155
+ }
156
+ return canStringifyDirectWalk(input, 1)
157
+ }
158
+
159
+ /**
160
+ * @param {Record<string, unknown> | unknown[]} value
161
+ * @param {number} depth
162
+ */
163
+ function canStringifyDirectWalk (value, depth) {
164
+ if (depth > MAX_DEPTH) return false
165
+ const children = Array.isArray(value) ? value : Object.values(value)
166
+ for (const child of children) {
167
+ if (child === null ||
168
+ typeof child === 'string' ||
169
+ typeof child === 'number' ||
170
+ typeof child === 'boolean') {
171
+ continue
172
+ }
173
+ if (typeof child !== 'object' ||
174
+ ArrayBuffer.isView(child) ||
175
+ child._bsontype !== undefined ||
176
+ isRegExp(child) ||
177
+ isMap(child) ||
178
+ typeof child.toJSON === 'function') {
179
+ return false
180
+ }
181
+ if (!canStringifyDirectWalk(child, depth + 1)) return false
182
+ }
183
+ return true
184
+ }
185
+
186
+ /**
187
+ * @param {Record<string, unknown> | unknown[]} input
188
+ * @param {'none' | 'types' | 'redact'} mode
189
+ */
190
+ function sanitiseAndStringify (input, mode) {
191
+ if (mode === 'none') {
192
+ if (canStringifyDirect(input)) return JSON.stringify(input)
193
+ return buildNone(input, [])
194
+ }
195
+ if (mode === 'redact') return buildRedact(input, [])
196
+ return buildTypes(input, [])
197
+ }
198
+
199
+ const REDACT_LEAF = '"?"'
200
+
201
+ /**
202
+ * @param {RegExp} value
203
+ * @returns {string}
204
+ */
205
+ function stringifyRegExp (value) {
206
+ return `{"$regex":${JSON.stringify(value.source)},"$options":${JSON.stringify(value.flags)}}`
207
+ }
208
+
209
+ /**
210
+ * @param {Record<string, unknown> | unknown[]} value
211
+ * @param {object[]} ancestors
212
+ * @returns {string | undefined}
213
+ */
214
+ function buildNone (value, ancestors) {
215
+ // ArrayBuffer views (Buffer, every TypedArray, DataView) and Binary BSON
216
+ // wrappers redact at the leaf; the walker neither recurses into the bytes
217
+ // nor invokes any custom conversion.
218
+ const bsontype = value._bsontype
219
+ if (ArrayBuffer.isView(value) || bsontype === 'Binary' ||
220
+ ancestors.length >= MAX_DEPTH || ancestors.includes(value)) {
221
+ return REDACT_LEAF
222
+ }
223
+
224
+ if (isRegExp(value)) return stringifyRegExp(value)
225
+
226
+ // Mirror JSON.stringify's contract: when `toJSON` is present, walk its
227
+ // result (wrappers like Timestamp / Decimal128 expand to a small object,
228
+ // ObjectId / Date flatten to a primitive).
229
+ if (typeof value.toJSON === 'function') {
230
+ const json = value.toJSON()
231
+ if (json === value) return REDACT_LEAF
232
+ // JSON.stringify keeps a null result as null (an invalid Date's toJSON
233
+ // returns null); only function / symbol / undefined results drop the key.
234
+ if (json === null) return 'null'
235
+ if (typeof json !== 'object') return classifyLeafForNone(json)
236
+ // A wrapper that exposes binary state through toJSON (Buffer-backed
237
+ // class with WeakMap state, etc.) returns a TypedArray here. Re-screen
238
+ // before the per-key walk would expand it element by element.
239
+ if (ArrayBuffer.isView(json) || json._bsontype === 'Binary') return REDACT_LEAF
240
+ value = json
241
+ } else if (bsontype !== undefined) {
242
+ return REDACT_LEAF
243
+ }
244
+
245
+ // The driver serializes a Map via its entries; mirror that as a document so
246
+ // the tag matches the wire shape.
247
+ if (isMap(value)) value = Object.fromEntries(value)
248
+
249
+ ancestors.push(value)
250
+
251
+ let result
252
+ if (Array.isArray(value)) {
253
+ result = '['
254
+ let sep = ''
255
+ for (let i = 0; i < value.length; i++) {
256
+ // JSON.stringify renders unsupported leaves (function, symbol, undefined) as null in arrays.
257
+ result += sep + (classifyForNone(value[i], ancestors) ?? 'null')
258
+ sep = ','
259
+ }
260
+ result += ']'
261
+ } else {
262
+ result = '{'
263
+ let sep = ''
264
+ for (const key of Object.keys(value)) {
265
+ const childResult = classifyForNone(value[key], ancestors)
266
+ if (childResult === undefined) continue
267
+ result += sep + JSON.stringify(key) + ':' + childResult
268
+ sep = ','
269
+ }
270
+ result += '}'
271
+ }
272
+ ancestors.pop()
273
+ return result
274
+ }
275
+
276
+ /**
277
+ * @param {unknown} child
278
+ * @param {object[]} ancestors
279
+ * @returns {string | undefined}
280
+ */
281
+ function classifyForNone (child, ancestors) {
282
+ if (typeof child !== 'object') return classifyLeafForNone(child)
283
+ if (child === null) return 'null'
284
+ return buildNone(child, ancestors)
285
+ }
286
+
287
+ /**
288
+ * @param {unknown} leaf
289
+ * @returns {string | undefined}
290
+ */
291
+ function classifyLeafForNone (leaf) {
292
+ // Implicit `undefined` for function / symbol / undefined matches the
293
+ // contract callers rely on: JSON.stringify drops those property values
294
+ // inside objects and writes `null` in arrays.
295
+ switch (typeof leaf) {
296
+ case 'string': return JSON.stringify(leaf)
297
+ case 'number': return Number.isFinite(leaf) ? String(leaf) : 'null'
298
+ case 'boolean': return leaf ? 'true' : 'false'
299
+ case 'bigint': return `"${String(leaf)}"`
300
+ }
301
+ }
302
+
303
+ /**
304
+ * @param {Record<string, unknown> | unknown[]} value
305
+ * @param {object[]} ancestors
306
+ */
307
+ function buildRedact (value, ancestors) {
308
+ const bsontype = value._bsontype
309
+ if (ArrayBuffer.isView(value) || bsontype === 'Binary' || isRegExp(value) ||
310
+ ancestors.length >= MAX_DEPTH || ancestors.includes(value)) {
311
+ return REDACT_LEAF
312
+ }
313
+
314
+ // Mirror JSON.stringify: when `toJSON` is present, walk its result (which
315
+ // wrappers like Timestamp / Decimal128 expand to `{$timestamp: "..."}` etc).
316
+ // A primitive, null, or self-reference collapses to the sentinel — master's
317
+ // `value === original` short-circuit.
318
+ if (typeof value.toJSON === 'function') {
319
+ const json = value.toJSON()
320
+ if (typeof json !== 'object' || json === null || json === value) return REDACT_LEAF
321
+ // Re-screen: toJSON can return a TypedArray or Binary BSON wrapper.
322
+ if (ArrayBuffer.isView(json) || json._bsontype === 'Binary') return REDACT_LEAF
323
+ value = json
324
+ } else if (bsontype !== undefined) {
325
+ return REDACT_LEAF
326
+ }
327
+
328
+ if (isMap(value)) value = Object.fromEntries(value)
329
+
330
+ ancestors.push(value)
331
+
332
+ let result
333
+ if (Array.isArray(value)) {
334
+ result = '['
335
+ let sep = ''
336
+ for (let i = 0; i < value.length; i++) {
337
+ result += sep + classifyForRedact(value[i], ancestors)
338
+ sep = ','
339
+ }
340
+ result += ']'
341
+ } else {
342
+ result = '{'
343
+ let sep = ''
344
+ for (const key of Object.keys(value)) {
345
+ result += sep + JSON.stringify(key) + ':' + classifyForRedact(value[key], ancestors)
346
+ sep = ','
347
+ }
348
+ result += '}'
349
+ }
350
+ ancestors.pop()
351
+ return result
352
+ }
353
+
354
+ /**
355
+ * @param {unknown} child
356
+ * @param {object[]} ancestors
357
+ */
358
+ function classifyForRedact (child, ancestors) {
359
+ if (typeof child !== 'object' || child === null) return REDACT_LEAF
360
+ return buildRedact(child, ancestors)
361
+ }
362
+
363
+ const TYPE_OBJECT = '"object"'
364
+ const TYPE_NULL = '"null"'
365
+ const TYPE_BY_TYPEOF = {
366
+ string: '"string"',
367
+ number: '"number"',
368
+ boolean: '"boolean"',
369
+ bigint: '"bigint"',
370
+ undefined: '"undefined"',
371
+ }
372
+
373
+ /**
374
+ * @param {Record<string, unknown> | unknown[]} value
375
+ * @param {object[]} ancestors
376
+ */
377
+ function buildTypes (value, ancestors) {
378
+ const bsontype = value._bsontype
379
+ if (ArrayBuffer.isView(value) || bsontype === 'Binary' || isRegExp(value) ||
380
+ ancestors.length >= MAX_DEPTH || ancestors.includes(value)) {
381
+ return TYPE_OBJECT
382
+ }
383
+
384
+ if (typeof value.toJSON === 'function') {
385
+ const json = value.toJSON()
386
+ if (typeof json !== 'object' ||
387
+ json === null ||
388
+ json === value ||
389
+ ArrayBuffer.isView(json) ||
390
+ json._bsontype === 'Binary') {
391
+ return TYPE_OBJECT
392
+ }
393
+ value = json
394
+ } else if (bsontype !== undefined) {
395
+ return TYPE_OBJECT
396
+ }
397
+
398
+ if (isMap(value)) value = Object.fromEntries(value)
399
+
400
+ ancestors.push(value)
401
+
402
+ let result
403
+ if (Array.isArray(value)) {
404
+ result = '['
405
+ let sep = ''
406
+ for (let i = 0; i < value.length; i++) {
407
+ // JSON.stringify renders unsupported leaves (function, symbol) as null in arrays.
408
+ result += sep + (classifyForTypes(value[i], ancestors) ?? 'null')
409
+ sep = ','
410
+ }
411
+ result += ']'
412
+ } else {
413
+ result = '{'
414
+ let sep = ''
415
+ for (const key of Object.keys(value)) {
416
+ const childResult = classifyForTypes(value[key], ancestors)
417
+ if (childResult === undefined) continue
418
+ result += sep + JSON.stringify(key) + ':' + childResult
419
+ sep = ','
420
+ }
421
+ result += '}'
422
+ }
423
+ ancestors.pop()
424
+ return result
425
+ }
426
+
427
+ /**
428
+ * @param {unknown} child
429
+ * @param {object[]} ancestors
430
+ */
431
+ function classifyForTypes (child, ancestors) {
432
+ if (typeof child !== 'object') return TYPE_BY_TYPEOF[typeof child]
433
+ if (child === null) return TYPE_NULL
434
+ return buildTypes(child, ancestors)
435
+ }
436
+
437
+ /** @param {unknown} value */
438
+ function normaliseObfuscateQuery (value) {
439
+ if (value === 'types' || value === 'redact') return value
440
+ return 'none'
441
+ }
442
+
443
+ function isHeartbeat (ops, config) {
444
+ // Check if it's a heartbeat command https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.md
445
+ return (
446
+ ops &&
447
+ typeof ops === 'object' &&
448
+ (ops.hello === 1 || ops.helloOk === true || ops.ismaster === 1 || ops.isMaster === 1)
449
+ )
450
+ }
451
+
452
+ module.exports = MongodbCoreQueryPlugin
@@ -87,7 +87,7 @@ class PlaywrightPlugin extends CiPlugin {
87
87
  if (isEarlyFlakeDetectionFaulty) {
88
88
  this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
89
89
  }
90
- if (this.numFailedSuites > 0) {
90
+ if (status === 'fail' && this.numFailedSuites > 0) {
91
91
  let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
92
92
  if (this.numFailedTests > 0) {
93
93
  errorMessage += ` Tests failed: ${this.numFailedTests}`
@@ -113,6 +113,7 @@ class PlaywrightPlugin extends CiPlugin {
113
113
  appClosingTelemetry()
114
114
  this.tracer._exporter.flush(onDone)
115
115
  this.numFailedTests = 0
116
+ this.numFailedSuites = 0
116
117
  })
117
118
 
118
119
  this.addBind('ci:playwright:test-suite:start', (ctx) => {
@@ -408,7 +409,7 @@ class PlaywrightPlugin extends CiPlugin {
408
409
  }
409
410
  stepSpan.finish(stepStartTime + stepDuration)
410
411
  }
411
- if (testStatus === 'fail') {
412
+ if (finalStatus === 'fail') {
412
413
  this.numFailedTests++
413
414
  }
414
415
 
@@ -67,6 +67,7 @@ class VitestPlugin extends CiPlugin {
67
67
  testCommand: this.command,
68
68
  repositoryRoot: this.repositoryRoot,
69
69
  codeOwnersEntries: this.codeOwnersEntries,
70
+ testEnvironmentMetadata: this.testEnvironmentMetadata,
70
71
  })
71
72
  })
72
73
 
@@ -192,7 +193,7 @@ class VitestPlugin extends CiPlugin {
192
193
  return ctx.currentStore
193
194
  })
194
195
 
195
- this.addSub('ci:vitest:test:pass', ({ span, task, finalStatus, earlyFlakeAbortReason }) => {
196
+ this.addSub('ci:vitest:test:pass', ({ span, task, finalStatus, earlyFlakeAbortReason, promises }) => {
196
197
  if (span) {
197
198
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', this.getTestTelemetryTags(span))
198
199
  span.setTag(TEST_STATUS, 'pass')
@@ -202,8 +203,21 @@ class VitestPlugin extends CiPlugin {
202
203
  if (earlyFlakeAbortReason) {
203
204
  span.setTag(TEST_EARLY_FLAKE_ABORT_REASON, earlyFlakeAbortReason)
204
205
  }
205
- span.finish(this.taskToFinishTime.get(task))
206
- finishAllTraceSpans(span)
206
+ const finish = () => {
207
+ span.finish(this.taskToFinishTime.get(task))
208
+ finishAllTraceSpans(span)
209
+ }
210
+
211
+ if (finalStatus) {
212
+ if (promises && this.diBreakpointHitPromise) {
213
+ promises.hitBreakpointPromise = this.waitForPreparedDiBreakpointHit().then(finish)
214
+ return
215
+ }
216
+ finish()
217
+ this.cancelDiBreakpointHitWait()
218
+ return
219
+ }
220
+ finish()
207
221
  }
208
222
  })
209
223
 
@@ -212,6 +226,7 @@ class VitestPlugin extends CiPlugin {
212
226
  duration,
213
227
  error,
214
228
  shouldSetProbe,
229
+ shouldWaitForHitProbe,
215
230
  promises,
216
231
  hasFailedAllRetries,
217
232
  attemptToFixFailed,
@@ -227,7 +242,8 @@ class VitestPlugin extends CiPlugin {
227
242
  const { file, line, stackIndex, setProbePromise } = probeInformation
228
243
  this.runningTestProbe = { file, line }
229
244
  this.testErrorStackIndex = stackIndex
230
- promises.setProbePromise = setProbePromise
245
+ this.prepareDiBreakpointHitWait()
246
+ promises.setProbePromise = this.waitForDiOperation(setProbePromise)
231
247
  }
232
248
  }
233
249
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', this.getTestTelemetryTags(span))
@@ -248,12 +264,31 @@ class VitestPlugin extends CiPlugin {
248
264
  if (earlyFlakeAbortReason) {
249
265
  span.setTag(TEST_EARLY_FLAKE_ABORT_REASON, earlyFlakeAbortReason)
250
266
  }
251
- if (duration) {
252
- span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
253
- } else {
254
- span.finish() // `duration` is empty for retries, so we'll use clock time
267
+ const finish = () => {
268
+ if (duration) {
269
+ span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
270
+ } else {
271
+ span.finish() // `duration` is empty for retries, so we'll use clock time
272
+ }
273
+ finishAllTraceSpans(span)
274
+ }
275
+
276
+ if (!shouldSetProbe && finalStatus && promises && this.diBreakpointHitPromise) {
277
+ promises.hitBreakpointPromise = this.waitForPreparedDiBreakpointHit().then(finish)
278
+ return
279
+ }
280
+ finish()
281
+ if (shouldWaitForHitProbe) {
282
+ this.prepareDiBreakpointHitWait()
283
+ } else if (!shouldSetProbe) {
284
+ this.cancelDiBreakpointHitWait()
285
+ }
286
+ })
287
+
288
+ this.addSub('ci:vitest:test:di:wait', ({ promises }) => {
289
+ if (this.di) {
290
+ promises.hitBreakpointPromise = this.waitForDiBreakpointHits()
255
291
  }
256
- finishAllTraceSpans(span)
257
292
  })
258
293
 
259
294
  this.addSub('ci:vitest:test:skip', ({
@@ -289,6 +324,7 @@ class VitestPlugin extends CiPlugin {
289
324
  const {
290
325
  codeOwnersEntries,
291
326
  repositoryRoot,
327
+ testEnvironmentMetadata,
292
328
  requestErrorTags,
293
329
  testSuiteAbsolutePath,
294
330
  frameworkVersion,
@@ -298,6 +334,12 @@ class VitestPlugin extends CiPlugin {
298
334
 
299
335
  const testCommand = ctx.testCommand || 'vitest run'
300
336
  const { testSessionId, testModuleId } = ctx
337
+ if (testEnvironmentMetadata) {
338
+ this.testEnvironmentMetadata = {
339
+ ...this.testEnvironmentMetadata,
340
+ ...testEnvironmentMetadata,
341
+ }
342
+ }
301
343
  this._setRepositoryRoot(repositoryRoot, codeOwnersEntries)
302
344
  this.command = testCommand
303
345
  this.frameworkVersion = frameworkVersion
@@ -2,15 +2,11 @@
2
2
 
3
3
  const log = require('../log')
4
4
  const { incomingHttpRequestStart } = require('./channels')
5
- const openaiIntegration = require('./integrations/openai')
6
- const vercelAiIntegration = require('./integrations/vercel-ai')
5
+ const integrations = require('./integrations')
7
6
  const AIGuard = require('./sdk')
8
7
 
9
8
  let isEnabled = false
10
9
  let aiguard
11
- let block
12
- let disableOpenAIIntegration
13
- let disableVercelAiIntegration
14
10
 
15
11
  function onIncomingHttpRequestStart () {
16
12
  // No-op: subscribing ensures the HTTP plugin spreads req onto the store
@@ -21,31 +17,30 @@ function enable (tracer, config) {
21
17
 
22
18
  try {
23
19
  aiguard = new AIGuard(tracer, config)
24
- block = config.experimental?.aiguard?.block !== false
20
+ const block = config.experimental?.aiguard?.block !== false
25
21
 
26
22
  incomingHttpRequestStart.subscribe(onIncomingHttpRequestStart)
27
- disableOpenAIIntegration = openaiIntegration.enable(aiguard, block)
28
- disableVercelAiIntegration = vercelAiIntegration.enable(aiguard, block)
23
+ integrations.enable(aiguard, block)
29
24
 
30
25
  isEnabled = true
31
26
  } catch (err) {
32
27
  log.error('AIGuard: unexpected error during initialization: %s', err.message)
33
- disable()
28
+ reset()
34
29
  }
35
30
  }
36
31
 
37
32
  function disable () {
38
33
  if (!isEnabled) return
39
34
 
35
+ reset()
36
+ }
37
+
38
+ function reset () {
40
39
  incomingHttpRequestStart.unsubscribe(onIncomingHttpRequestStart)
41
- disableOpenAIIntegration?.()
42
- disableVercelAiIntegration?.()
40
+ integrations.disable()
43
41
 
44
42
  aiguard = undefined
45
43
  isEnabled = false
46
- block = false
47
- disableOpenAIIntegration = undefined
48
- disableVercelAiIntegration = undefined
49
44
  }
50
45
 
51
46
  module.exports = { enable, disable }
@@ -0,0 +1,34 @@
1
+ 'use strict'
2
+
3
+ const openai = require('./openai')
4
+ const vercelAi = require('./vercel-ai')
5
+
6
+ let isEnabled = false
7
+
8
+ /**
9
+ * Enables all AI Guard provider integrations.
10
+ *
11
+ * @param {object} aiguard
12
+ * @param {boolean} block
13
+ */
14
+ function enable (aiguard, block) {
15
+ if (isEnabled) return
16
+
17
+ openai.enable(aiguard, block)
18
+ vercelAi.enable(aiguard, block)
19
+
20
+ isEnabled = true
21
+ }
22
+
23
+ function disable () {
24
+ vercelAi.disable()
25
+ openai.disable()
26
+ isEnabled = false
27
+ }
28
+
29
+ module.exports = {
30
+ enable,
31
+ disable,
32
+ openai,
33
+ vercelAi,
34
+ }