dd-trace 5.78.0 → 5.80.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 (54) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/index.d.ts +11 -4
  3. package/initialize.mjs +10 -10
  4. package/package.json +6 -3
  5. package/packages/datadog-core/src/storage.js +4 -4
  6. package/packages/datadog-esbuild/src/utils.js +5 -1
  7. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -2
  8. package/packages/datadog-instrumentations/src/azure-service-bus.js +43 -36
  9. package/packages/datadog-instrumentations/src/helpers/hook.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -1
  11. package/packages/datadog-instrumentations/src/jest.js +1 -1
  12. package/packages/datadog-instrumentations/src/playwright.js +20 -0
  13. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +3 -2
  14. package/packages/datadog-plugin-azure-service-bus/src/producer.js +14 -5
  15. package/packages/datadog-plugin-jest/src/index.js +1 -6
  16. package/packages/datadog-plugin-jest/src/util.js +46 -15
  17. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -1
  18. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -1
  19. package/packages/datadog-plugin-openai/src/stream-helpers.js +1 -1
  20. package/packages/datadog-shimmer/src/shimmer.js +2 -2
  21. package/packages/dd-trace/src/aiguard/sdk.js +12 -5
  22. package/packages/dd-trace/src/appsec/telemetry/index.js +1 -31
  23. package/packages/dd-trace/src/baggage.js +11 -0
  24. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +1 -1
  25. package/packages/dd-trace/src/config_defaults.js +1 -0
  26. package/packages/dd-trace/src/debugger/devtools_client/session.js +11 -1
  27. package/packages/dd-trace/src/encode/0.4.js +3 -3
  28. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +2 -2
  29. package/packages/dd-trace/src/exporters/agent/writer.js +6 -13
  30. package/packages/dd-trace/src/lambda/runtime/ritm.js +1 -2
  31. package/packages/dd-trace/src/llmobs/index.js +5 -5
  32. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  33. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +1 -0
  34. package/packages/dd-trace/src/llmobs/plugins/openai.js +41 -35
  35. package/packages/dd-trace/src/llmobs/sdk.js +5 -1
  36. package/packages/dd-trace/src/llmobs/span_processor.js +5 -5
  37. package/packages/dd-trace/src/llmobs/tagger.js +31 -17
  38. package/packages/dd-trace/src/msgpack/chunk.js +2 -2
  39. package/packages/dd-trace/src/msgpack/encoder.js +2 -3
  40. package/packages/dd-trace/src/msgpack/index.js +2 -2
  41. package/packages/dd-trace/src/openfeature/flagging_provider.js +5 -3
  42. package/packages/dd-trace/src/opentelemetry/logs/index.js +1 -1
  43. package/packages/dd-trace/src/opentelemetry/logs/logger.js +11 -6
  44. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +1 -1
  45. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -9
  46. package/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js +1 -1
  47. package/packages/dd-trace/src/plugins/database.js +1 -0
  48. package/packages/dd-trace/src/plugins/plugin.js +7 -9
  49. package/packages/dd-trace/src/profiling/exporter_cli.js +7 -6
  50. package/packages/dd-trace/src/remote_config/index.js +11 -1
  51. package/packages/dd-trace/src/require-package-json.js +1 -1
  52. package/packages/dd-trace/src/service-naming/index.js +31 -4
  53. package/packages/dd-trace/src/span_processor.js +9 -9
  54. /package/packages/dd-trace/src/{format.js → span_format.js} +0 -0
@@ -56,6 +56,7 @@ dev,chai,MIT,Copyright 2017 Chai.js Assertion Library
56
56
  dev,eslint,MIT,Copyright JS Foundation and other contributors https://js.foundation
57
57
  dev,eslint-plugin-cypress,MIT,Copyright (c) 2019 Cypress.io
58
58
  dev,eslint-plugin-import,MIT,Copyright 2015 Ben Mosher
59
+ dev,eslint-plugin-jsdoc,BSD-3-Clause,Copyright Gajus Kuizinas
59
60
  dev,eslint-plugin-mocha,MIT,Copyright 2014 Mathias Schreck
60
61
  dev,eslint-plugin-n,MIT,Copyright 2015 Toru Nagashima
61
62
  dev,eslint-plugin-promise,ISC,jden and other contributors
package/index.d.ts CHANGED
@@ -654,6 +654,13 @@ declare namespace tracer {
654
654
  * @default false
655
655
  */
656
656
  enabled?: boolean
657
+ /**
658
+ * Timeout in milliseconds for OpenFeature provider initialization.
659
+ * If configuration is not received within this time, initialization fails.
660
+ *
661
+ * @default 30000
662
+ */
663
+ initializationTimeoutMs?: number
657
664
  }
658
665
  };
659
666
 
@@ -3011,15 +3018,15 @@ declare namespace tracer {
3011
3018
  label: string,
3012
3019
 
3013
3020
  /**
3014
- * The type of evaluation metric, one of 'categorical' or 'score'
3021
+ * The type of evaluation metric, one of 'categorical', 'score', or 'boolean'
3015
3022
  */
3016
- metricType: 'categorical' | 'score',
3023
+ metricType: 'categorical' | 'score' | 'boolean',
3017
3024
 
3018
3025
  /**
3019
3026
  * The value of the evaluation metric.
3020
- * Must be string for 'categorical' metrics and number for 'score' metrics.
3027
+ * Must be string for 'categorical' metrics, number for 'score' metrics, and boolean for 'boolean' metrics.
3021
3028
  */
3022
- value: string | number,
3029
+ value: string | number | boolean,
3023
3030
 
3024
3031
  /**
3025
3032
  * An object of string key-value pairs to tag the evaluation metric with.
package/initialize.mjs CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * This file serves one of two purposes, depending on how it's used.
3
- *
4
- * If used with --import, it will import init.js and register the loader hook.
5
- * If used with --loader, it will act as the loader hook, except that it will
6
- * also import init.js inside the source code of the entrypoint file.
7
- *
8
- * The result is that no matter how this file is used, so long as it's with
9
- * one of the two flags, the tracer will always be initialized, and the loader
10
- * hook will always be active for ESM support.
11
- */
2
+ * This file serves one of two purposes, depending on how it's used.
3
+ *
4
+ * If used with --import, it will import init.js and register the loader hook.
5
+ * If used with --loader, it will act as the loader hook, except that it will
6
+ * also import init.js inside the source code of the entrypoint file.
7
+ *
8
+ * The result is that no matter how this file is used, so long as it's with
9
+ * one of the two flags, the tracer will always be initialized, and the loader
10
+ * hook will always be active for ESM support.
11
+ */
12
12
 
13
13
  /* eslint n/no-unsupported-features/node-builtins: ['error', { ignores: ['module.register'] }] */
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.78.0",
3
+ "version": "5.80.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -46,6 +46,8 @@
46
46
  "test:llmobs:sdk:ci": "nyc --no-clean --include \"packages/dd-trace/src/llmobs/**/*.js\" -- npm run test:llmobs:sdk",
47
47
  "test:llmobs:plugins": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/llmobs/plugins/@($(echo $PLUGINS))/*.spec.js\"",
48
48
  "test:llmobs:plugins:ci": "yarn services && nyc --no-clean --include \"packages/dd-trace/src/llmobs/**/*.js\" -- npm run test:llmobs:plugins",
49
+ "test:openfeature": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/openfeature/*.spec.js\"",
50
+ "test:openfeature:ci": "nyc --no-clean --include \"packages/dd-trace/src/openfeature/**/*.js\" -- npm run test:openfeature",
49
51
  "test:plugins": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/datadog-plugin-@($(echo $PLUGINS))/test/**/@($(echo ${SPEC:-'*'})).spec.js\"",
50
52
  "test:plugins:ci": "yarn services && nyc --no-clean --include \"packages/datadog-plugin-@($(echo $PLUGINS))/src/**/*.js\" -- npm run test:plugins",
51
53
  "test:plugins:ci:flaky": "yarn services && nyc --no-clean --include \"packages/datadog-plugin-@($(echo $PLUGINS))/src/**/*.js\" -- npm run test:plugins -- --bail --retries 2",
@@ -125,7 +127,7 @@
125
127
  "@datadog/native-appsec": "10.3.0",
126
128
  "@datadog/native-iast-taint-tracking": "4.0.0",
127
129
  "@datadog/native-metrics": "3.1.1",
128
- "@datadog/openfeature-node-server": "0.1.0-preview.15",
130
+ "@datadog/openfeature-node-server": "^0.2.0",
129
131
  "@datadog/pprof": "5.12.0",
130
132
  "@datadog/sketches-js": "2.1.1",
131
133
  "@datadog/wasm-js-rewriter": "5.0.1",
@@ -165,7 +167,7 @@
165
167
  "@eslint/js": "^9.39.0",
166
168
  "@msgpack/msgpack": "^3.1.2",
167
169
  "@openfeature/core": "^1.9.0",
168
- "@openfeature/server-sdk": "^1.20.0",
170
+ "@openfeature/server-sdk": "~1.20.0",
169
171
  "@stylistic/eslint-plugin": "^5.5.0",
170
172
  "@types/chai": "^4.3.16",
171
173
  "@types/mocha": "^10.0.10",
@@ -180,6 +182,7 @@
180
182
  "eslint": "^9.39.0",
181
183
  "eslint-plugin-cypress": "^5.2.0",
182
184
  "eslint-plugin-import": "^2.32.0",
185
+ "eslint-plugin-jsdoc": "^61.1.12",
183
186
  "eslint-plugin-mocha": "^11.2.0",
184
187
  "eslint-plugin-n": "^17.23.1",
185
188
  "eslint-plugin-promise": "^7.2.1",
@@ -15,7 +15,7 @@ const { AsyncLocalStorage } = require('async_hooks')
15
15
  class DatadogStorage extends AsyncLocalStorage {
16
16
  /**
17
17
  *
18
- * @param store {Store}
18
+ * @param {Store} [store]
19
19
  * @override
20
20
  */
21
21
  enterWith (store) {
@@ -47,7 +47,7 @@ class DatadogStorage extends AsyncLocalStorage {
47
47
  * retrieved through `getHandle()` can also be passed in to be used as the
48
48
  * key. This is useful if you've stashed a handle somewhere and want to
49
49
  * retrieve the store with it.
50
- * @param {{}} [handle]
50
+ * @param {object} [handle]
51
51
  * @returns {Store | undefined}
52
52
  * @override
53
53
  */
@@ -87,7 +87,7 @@ class DatadogStorage extends AsyncLocalStorage {
87
87
 
88
88
  /**
89
89
  * This is the map from handles to real stores, used in the class above.
90
- * @type {WeakMap<WeakKey, Store>}
90
+ * @type {WeakMap<WeakKey, Store|undefined>}
91
91
  */
92
92
  const stores = new WeakMap()
93
93
 
@@ -101,7 +101,7 @@ const storages = Object.create(null)
101
101
 
102
102
  /**
103
103
  *
104
- * @param namespace {string} the namespace to use
104
+ * @param {string} namespace The namespace to use
105
105
  * @returns {DatadogStorage}
106
106
  */
107
107
  function storage (namespace) {
@@ -65,7 +65,11 @@ function getSource (url, { format }) {
65
65
  /**
66
66
  * Generates the pieces of code for the proxy module before the path
67
67
  *
68
- * @param {Object} moduleData { path, internal, context, excludeDefault }
68
+ * @param {object} moduleData
69
+ * @param {string} moduleData.path
70
+ * @param {boolean} moduleData.internal
71
+ * @param {object} moduleData.context
72
+ * @param {boolean} moduleData.excludeDefault
69
73
  * @returns {Promise<Map>}
70
74
  */
71
75
  async function processModule ({ path, internal, context, excludeDefault }) {
@@ -34,10 +34,11 @@ function wrapRequest (send) {
34
34
  }
35
35
  }
36
36
 
37
- function wrapDeserialize (deserialize, channelSuffix) {
37
+ function wrapDeserialize (deserialize, channelSuffix, responseIndex = 0) {
38
38
  const headersCh = channel(`apm:aws:response:deserialize:${channelSuffix}`)
39
39
 
40
- return function (response) {
40
+ return function () {
41
+ const response = arguments[responseIndex]
41
42
  if (headersCh.hasSubscribers) {
42
43
  headersCh.publish({ headers: response.headers })
43
44
  }
@@ -66,6 +67,12 @@ function wrapSmithySend (send) {
66
67
 
67
68
  if (typeof command.deserialize === 'function') {
68
69
  shimmer.wrap(command, 'deserialize', deserialize => wrapDeserialize(deserialize, channelSuffix))
70
+ } else if (this.config?.protocol?.deserializeResponse) {
71
+ shimmer.wrap(
72
+ this.config.protocol,
73
+ 'deserializeResponse',
74
+ deserializeResponse => wrapDeserialize(deserializeResponse, channelSuffix, 2)
75
+ )
69
76
  }
70
77
 
71
78
  const ctx = {
@@ -8,57 +8,64 @@ const shimmer = require('../../datadog-shimmer')
8
8
  const dc = require('dc-polyfill')
9
9
 
10
10
  const producerCh = dc.tracingChannel('apm:azure-service-bus:send')
11
+ const isItDefault = new WeakSet()
11
12
 
12
- addHook({ name: '@azure/service-bus', versions: ['>=7.9.2'], patchDefault: false }, (obj) => {
13
+ addHook({ name: '@azure/service-bus', versions: ['>=7.9.2'] }, (obj) => {
13
14
  const ServiceBusClient = obj.ServiceBusClient
14
- let didItShim = false
15
15
  shimmer.wrap(ServiceBusClient.prototype, 'createSender',
16
16
  createSender => function (queueOrTopicName) {
17
17
  const sender = createSender.apply(this, arguments)
18
- if (didItShim) return sender
19
18
  const senderPrototype = sender.constructor.prototype
20
19
  const senderSenderPrototype = sender._sender.constructor.prototype
21
- shimmer.wrap(senderPrototype, 'scheduleMessages', scheduleMessages =>
22
- function (msg, scheduledEnqueueTimeUtc) {
23
- const functionName = scheduleMessages.name
20
+
21
+ if (!isItDefault.has(senderPrototype)) {
22
+ isItDefault.add(senderPrototype)
23
+
24
+ shimmer.wrap(senderPrototype, 'scheduleMessages', scheduleMessages =>
25
+ function (msg, scheduledEnqueueTimeUtc) {
26
+ const functionName = scheduleMessages.name
27
+ const config = this._context.config
28
+ const entityPath = this._entityPath
29
+ return producerCh.tracePromise(
30
+ scheduleMessages,
31
+ { config, entityPath, functionName, msg, scheduledEnqueueTimeUtc },
32
+ this, ...arguments
33
+ )
34
+ })
35
+
36
+ shimmer.wrap(senderPrototype, 'createMessageBatch', createMessageBatch => async function () {
37
+ const batch = await createMessageBatch.apply(this, arguments)
38
+ shimmer.wrap(batch, 'tryAddMessage', tryAddMessage => function (msg) {
39
+ const functionName = tryAddMessage.name
40
+ const config = this._context.config
41
+ return producerCh.tracePromise(
42
+ tryAddMessage, { config, functionName, batch, msg }, this, ...arguments)
43
+ })
44
+ return batch
45
+ })
46
+ }
47
+
48
+ if (!isItDefault.has(senderSenderPrototype)) {
49
+ isItDefault.add(senderSenderPrototype)
50
+
51
+ shimmer.wrap(senderSenderPrototype, 'send', send => function (msg) {
52
+ const functionName = send.name
24
53
  const config = this._context.config
25
- const entityPath = this._entityPath
54
+ const entityPath = this.entityPath
26
55
  return producerCh.tracePromise(
27
- scheduleMessages,
28
- { config, entityPath, functionName, msg, scheduledEnqueueTimeUtc },
29
- this, ...arguments
56
+ send, { config, entityPath, functionName, msg }, this, ...arguments
30
57
  )
31
58
  })
32
59
 
33
- shimmer.wrap(senderPrototype, 'createMessageBatch', createMessageBatch => async function () {
34
- const batch = await createMessageBatch.apply(this, arguments)
35
- shimmer.wrap(batch.constructor.prototype, 'tryAddMessage', tryAddMessage => function (msg) {
36
- const functionName = tryAddMessage.name
60
+ shimmer.wrap(senderSenderPrototype, 'sendBatch', sendBatch => function (msg) {
61
+ const functionName = sendBatch.name
37
62
  const config = this._context.config
63
+ const entityPath = this.entityPath
38
64
  return producerCh.tracePromise(
39
- tryAddMessage, { config, functionName, batch, msg }, this, ...arguments)
65
+ sendBatch, { config, entityPath, functionName, msg }, this, ...arguments
66
+ )
40
67
  })
41
- return batch
42
- })
43
-
44
- shimmer.wrap(senderSenderPrototype, 'send', send => function (msg) {
45
- const functionName = send.name
46
- const config = this._context.config
47
- const entityPath = this.entityPath
48
- return producerCh.tracePromise(
49
- send, { config, entityPath, functionName, msg }, this, ...arguments
50
- )
51
- })
52
-
53
- shimmer.wrap(senderSenderPrototype, 'sendBatch', sendBatch => function (msg) {
54
- const functionName = sendBatch.name
55
- const config = this._context.config
56
- const entityPath = this.entityPath
57
- return producerCh.tracePromise(
58
- sendBatch, { config, entityPath, functionName, msg }, this, ...arguments
59
- )
60
- })
61
- didItShim = true
68
+ }
62
69
  return sender
63
70
  })
64
71
  return obj
@@ -8,6 +8,7 @@ const ritm = require('../../../dd-trace/src/ritm')
8
8
  * In practice, `modules` is always an array with a single entry.
9
9
  *
10
10
  * @param {string[]} modules list of modules to hook into
11
+ * @param {object} hookOptions hook options
11
12
  * @param {Function} onrequire callback to be executed upon encountering module
12
13
  */
13
14
  function Hook (modules, hookOptions, onrequire) {
@@ -28,7 +28,8 @@ exports.tracingChannel = function (name) {
28
28
  * @param {string[]} args.versions array of semver range strings
29
29
  * @param {string} [args.file='index.js'] path to file within package to instrument
30
30
  * @param {string} [args.filePattern] pattern to match files within package to instrument
31
- * @param Function hook
31
+ * @param {boolean} [args.patchDefault] whether to patch the default export
32
+ * @param {(moduleExports: unknown, version: string) => unknown} hook
32
33
  */
33
34
  exports.addHook = function addHook ({ name, versions, file, filePattern, patchDefault }, hook) {
34
35
  if (typeof name === 'string') {
@@ -1110,7 +1110,7 @@ function jestAdapterWrapper (jestAdapter, jestVersion) {
1110
1110
  * Child processes do not each request ITR configuration, so the jest's parent process
1111
1111
  * needs to pass them the configuration. This is done via _ddTestCodeCoverageEnabled, which
1112
1112
  * controls whether coverage is reported.
1113
- */
1113
+ */
1114
1114
  if (environment.testEnvironmentOptions?._ddTestCodeCoverageEnabled) {
1115
1115
  const root = environment.repositoryRoot || environment.rootDir
1116
1116
 
@@ -481,6 +481,15 @@ function dispatcherRunWrapper (run) {
481
481
 
482
482
  function dispatcherRunWrapperNew (run) {
483
483
  return function (testGroups) {
484
+ // Filter out disabled tests from testGroups before they get scheduled
485
+ if (isTestManagementTestsEnabled) {
486
+ testGroups.forEach(group => {
487
+ group.tests = group.tests.filter(test => !test._ddIsDisabled)
488
+ })
489
+ // Remove empty groups
490
+ testGroups = testGroups.filter(group => group.tests.length > 0)
491
+ }
492
+
484
493
  if (!this._allTests) {
485
494
  // Removed in https://github.com/microsoft/playwright/commit/1e52c37b254a441cccf332520f60225a5acc14c7
486
495
  // Not available from >=1.44.0
@@ -893,6 +902,9 @@ addHook({
893
902
  if (testProperties.disabled) {
894
903
  test._ddIsDisabled = true
895
904
  test.expectedStatus = 'skipped'
905
+ // setting test.expectedStatus to 'skipped' does not work for every case,
906
+ // so we need to filter out disabled tests in dispatcherRunWrapperNew,
907
+ // so they don't get to the workers
896
908
  continue
897
909
  }
898
910
  if (testProperties.quarantined) {
@@ -1270,6 +1282,10 @@ function generateSummaryWrapper (generateSummary) {
1270
1282
  const {
1271
1283
  _requireFile: testSuiteAbsolutePath,
1272
1284
  location: { line: testSourceLine },
1285
+ _ddIsNew: isNew,
1286
+ _ddIsDisabled: isDisabled,
1287
+ _ddIsModified: isModified,
1288
+ _ddIsQuarantined: isQuarantined
1273
1289
  } = test
1274
1290
  const browserName = getBrowserNameFromProjects(sessionProjects, test)
1275
1291
 
@@ -1278,6 +1294,10 @@ function generateSummaryWrapper (generateSummary) {
1278
1294
  testSuiteAbsolutePath,
1279
1295
  testSourceLine,
1280
1296
  browserName,
1297
+ isNew,
1298
+ isDisabled,
1299
+ isModified,
1300
+ isQuarantined
1281
1301
  })
1282
1302
  }
1283
1303
  }
@@ -27,8 +27,9 @@ const PROVIDER = {
27
27
  * Coerce the chunks into a single response body.
28
28
  *
29
29
  * @param {Array<{ chunk: { bytes: Buffer } }>} chunks
30
- * @param {string} provider
31
- * @returns {Object}
30
+ * @param {string} modelProvider
31
+ * @param {string} modelName
32
+ * @returns {Generation | Record<never, never>}
32
33
  */
33
34
  function extractTextAndResponseReasonFromStream (chunks, modelProvider, modelName) {
34
35
  const modelProviderUpper = modelProvider.toUpperCase()
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
4
4
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
5
+ const spanContexts = new WeakMap()
5
6
 
6
7
  class AzureServiceBusProducerPlugin extends ProducerPlugin {
7
8
  static get id () { return 'azure-service-bus' }
@@ -36,7 +37,12 @@ class AzureServiceBusProducerPlugin extends ProducerPlugin {
36
37
  }
37
38
 
38
39
  if (batchLinksAreEnabled()) {
39
- ctx.batch._spanContexts.push(span.context())
40
+ const spanContext = spanContexts.get(ctx.batch)
41
+ if (spanContext) {
42
+ spanContext.push(span.context())
43
+ } else {
44
+ spanContexts.set(ctx.batch, [span.context()])
45
+ }
40
46
  injectTraceContext(this.tracer, span, ctx.msg)
41
47
  }
42
48
  }
@@ -47,9 +53,12 @@ class AzureServiceBusProducerPlugin extends ProducerPlugin {
47
53
  if (isBatch) {
48
54
  span.setTag('messaging.batch.message_count', messages.count)
49
55
  if (batchLinksAreEnabled()) {
50
- messages._spanContexts.forEach(spanContext => {
51
- span.addLink(spanContext)
52
- })
56
+ const contexts = spanContexts.get(messages)
57
+ if (contexts) {
58
+ for (const spanContext of contexts) {
59
+ span.addLink(spanContext)
60
+ }
61
+ }
53
62
  }
54
63
  } else if (Array.isArray(messages)) {
55
64
  span.setTag('messaging.batch.message_count', messages.length)
@@ -64,7 +73,7 @@ class AzureServiceBusProducerPlugin extends ProducerPlugin {
64
73
  }
65
74
 
66
75
  asyncEnd (ctx) {
67
- super.finish()
76
+ super.finish(ctx)
68
77
  }
69
78
  }
70
79
 
@@ -282,11 +282,7 @@ class JestPlugin extends CiPlugin {
282
282
  log.warn('"ci:jest:test-suite:finish": no span found for test suite absolute path %s', testSuiteAbsolutePath)
283
283
  return
284
284
  }
285
- const hasStatus = testSuiteSpan.context()._tags[TEST_STATUS]
286
- if (!hasStatus) {
287
- // The status may have been set in 'ci:jest:test-suite:error'
288
- testSuiteSpan.setTag(TEST_STATUS, status)
289
- }
285
+ testSuiteSpan.setTag(TEST_STATUS, status)
290
286
  if (error) {
291
287
  testSuiteSpan.setTag('error', error)
292
288
  testSuiteSpan.setTag(TEST_STATUS, 'fail')
@@ -323,7 +319,6 @@ class JestPlugin extends CiPlugin {
323
319
  } else if (errorMessage) {
324
320
  runningTestSuiteSpan.setTag('error', new Error(errorMessage))
325
321
  }
326
- runningTestSuiteSpan.setTag(TEST_STATUS, 'fail')
327
322
  })
328
323
 
329
324
  /**
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { readFileSync } = require('fs')
4
- const { parse, extract } = require('jest-docblock')
4
+ const { parse } = require('jest-docblock')
5
5
 
6
6
  const { getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
7
7
  const log = require('../../dd-trace/src/log')
@@ -61,29 +61,60 @@ function getJestTestName (test, shouldStripSeed = false) {
61
61
  return testName
62
62
  }
63
63
 
64
+ const globalDocblockRegExp = /^\s*(\/\*\*?(.|\r?\n)*?\*\/)/
65
+ const MAX_COMMENTS_CHECKED = 10
66
+
64
67
  function isMarkedAsUnskippable (test) {
65
- let docblocks
68
+ let testSource
66
69
 
67
70
  try {
68
- const testSource = readFileSync(test.path, 'utf8')
69
- docblocks = parse(extract(testSource))
71
+ testSource = readFileSync(test.path, 'utf8')
70
72
  } catch {
71
- // If we have issues parsing the file, we'll assume no unskippable was passed
72
73
  return false
73
74
  }
74
75
 
75
- // docblocks were correctly parsed but it does not include a @datadog block
76
- if (!docblocks?.datadog) {
77
- return false
78
- }
76
+ const re = globalDocblockRegExp
77
+ re.lastIndex = 0
78
+ let commentsChecked = 0
79
+
80
+ while (testSource.length) {
81
+ const match = re.exec(testSource)
82
+ if (!match) break
83
+ const comment = match[1]
84
+
85
+ let docblocks
86
+ try {
87
+ docblocks = parse(comment)
88
+ } catch {
89
+ // Skip unparsable comment and continue scanning
90
+ if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
91
+ return false
92
+ }
93
+ continue
94
+ }
79
95
 
80
- try {
81
- return JSON.parse(docblocks.datadog).unskippable
82
- } catch {
83
- // If the @datadog block comment is present but malformed, we'll run the suite
84
- log.warn('@datadog block comment is malformed.')
85
- return true
96
+ if (docblocks?.datadog) {
97
+ try {
98
+ // @ts-expect-error The datadog type is defined by us and may only be a string.
99
+ return JSON.parse(docblocks.datadog).unskippable
100
+ } catch {
101
+ // If the @datadog block comment is present but malformed, we'll run the suite
102
+ log.warn('@datadog block comment is malformed.')
103
+ return true
104
+ }
105
+ }
106
+
107
+ if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
108
+ return false
109
+ }
110
+
111
+ // To stop as soon as no doc blocks are found, slice the source. That way the
112
+ // regexp works by using the `^` anchor. Without it, it would continue
113
+ // scanning the rest of the file.
114
+ testSource = testSource.slice(match[0].length)
86
115
  }
116
+
117
+ return false
87
118
  }
88
119
 
89
120
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
@@ -28,7 +28,8 @@ class KafkajsConsumerPlugin extends ConsumerPlugin {
28
28
  * @property {string} topic
29
29
  * @property {number} partition
30
30
  * @property {number} offset
31
- *
31
+ */
32
+ /**
32
33
  * @typedef {object} CommitEventItem
33
34
  * @property {string} groupId
34
35
  * @property {string} topic
@@ -25,6 +25,8 @@ class KafkajsProducerPlugin extends ProducerPlugin {
25
25
  * @property {string} topic
26
26
  * @property {number} partition
27
27
  * @property {number} offset
28
+ */
29
+ /**
28
30
  *
29
31
  * @typedef {object} ProducerResponseItem
30
32
  * @property {string} topic
@@ -49,7 +51,7 @@ class KafkajsProducerPlugin extends ProducerPlugin {
49
51
 
50
52
  /**
51
53
  *
52
- * @param {ProducerResponseItem[]} commitList
54
+ * @param {{ result: ProducerResponseItem[] }} ctx
53
55
  * @returns {void}
54
56
  */
55
57
  commit (ctx) {
@@ -111,7 +111,7 @@ function constructChatCompletionResponseFromStreamedChunks (chunks, n) {
111
111
  * Constructs the entire response from a stream of OpenAI responses chunks.
112
112
  * The responses API uses event-based streaming with delta chunks.
113
113
  * @param {Array<Record<string, any>>} chunks
114
- * @returns {Record<string, any>}
114
+ * @returns {Record<string, any>|undefined}
115
115
  */
116
116
  function constructResponseResponseFromStreamedChunks (chunks) {
117
117
  // The responses API streams events with different types:
@@ -91,7 +91,7 @@ function wrapFunction (original, wrapper) {
91
91
  * @param {Record<string | symbol, unknown> | Function | undefined} target - The target
92
92
  * object.
93
93
  * @param {string | symbol} name - The property key of the method to wrap.
94
- * @param {(original: Function) => (...args) => any} wrapper - The wrapper function.
94
+ * @param {(original: Function) => (...args: unknown[]) => any} wrapper - The wrapper function.
95
95
  * @param {{ replaceGetter?: boolean }} [options] - If `replaceGetter` is set to
96
96
  * true, the getter is accessed and the getter is replaced with one that just
97
97
  * returns the earlier retrieved value. Use with care! This may only be done in
@@ -214,7 +214,7 @@ function wrap (target, name, wrapper, options) {
214
214
  * Record<string | symbol, unknown> |
215
215
  * Function} targets - The target objects.
216
216
  * @param {Array<string | symbol> | string | symbol} names - The property keys of the methods to wrap.
217
- * @param {(original: Function) => (...args) => any} wrapper - The wrapper function.
217
+ * @param {(original: Function) => (...args: unknown[]) => any} wrapper - The wrapper function.
218
218
  */
219
219
  function massWrap (targets, names, wrapper) {
220
220
  targets = toArray(targets)
@@ -138,10 +138,11 @@ class AIGuard extends NoopAIGuard {
138
138
  span.setTag(AI_GUARD_TOOL_NAME_TAG_KEY, name)
139
139
  }
140
140
  }
141
+ const metaStruct = {
142
+ messages: this.#truncate(messages)
143
+ }
141
144
  span.meta_struct = {
142
- [AI_GUARD_META_STRUCT_KEY]: {
143
- messages: this.#truncate(messages)
144
- }
145
+ [AI_GUARD_META_STRUCT_KEY]: metaStruct
145
146
  }
146
147
  let response
147
148
  try {
@@ -166,7 +167,7 @@ class AIGuard extends NoopAIGuard {
166
167
  `AI Guard service call failed, status ${response.status}`,
167
168
  { errors: response.body?.errors })
168
169
  }
169
- let action, reason, blockingEnabled
170
+ let action, reason, tags, blockingEnabled
170
171
  try {
171
172
  const attr = response.body.data.attributes
172
173
  if (!attr.action) {
@@ -174,6 +175,7 @@ class AIGuard extends NoopAIGuard {
174
175
  }
175
176
  action = attr.action
176
177
  reason = attr.reason
178
+ tags = attr.tags
177
179
  blockingEnabled = attr.is_blocking_enabled ?? false
178
180
  } catch (e) {
179
181
  appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1)
@@ -182,7 +184,12 @@ class AIGuard extends NoopAIGuard {
182
184
  const shouldBlock = block && blockingEnabled && action !== ALLOW
183
185
  appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { action, error: false, block: shouldBlock }).inc(1)
184
186
  span.setTag(AI_GUARD_ACTION_TAG_KEY, action)
185
- span.setTag(AI_GUARD_REASON_TAG_KEY, reason)
187
+ if (reason) {
188
+ span.setTag(AI_GUARD_REASON_TAG_KEY, reason)
189
+ }
190
+ if (tags?.length > 0) {
191
+ metaStruct.attack_categories = tags
192
+ }
186
193
  if (shouldBlock) {
187
194
  span.setTag(AI_GUARD_BLOCKED_TAG_KEY, 'true')
188
195
  throw new AIGuardAbortError(reason)