dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -2,6 +2,7 @@
2
2
 
3
3
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
4
4
  const { storage } = require('../../datadog-core')
5
+ const { writeDatadogParentId, writeDatadogTraceId } = require('../../dd-trace/src/carrier')
5
6
 
6
7
  const {
7
8
  TEST_STATUS,
@@ -13,7 +14,6 @@ const {
13
14
  getTestSuiteCommonTags,
14
15
  getTestLevelsMetadataTags,
15
16
  getTestSessionName,
16
- getIsFaultyEarlyFlakeDetection,
17
17
  TEST_SOURCE_FILE,
18
18
  TEST_IS_RETRY,
19
19
  TEST_CODE_OWNERS,
@@ -107,19 +107,6 @@ class VitestPlugin extends CiPlugin {
107
107
  })
108
108
  })
109
109
 
110
- this.addSub('ci:vitest:is-early-flake-detection-faulty', ({
111
- knownTests,
112
- testFilepaths,
113
- onDone,
114
- }) => {
115
- const isFaulty = getIsFaultyEarlyFlakeDetection(
116
- testFilepaths.map(testFilepath => getTestSuitePath(testFilepath, this.repositoryRoot)),
117
- knownTests,
118
- this.libraryConfig.earlyFlakeDetectionFaultyThreshold
119
- )
120
- onDone(isFaulty)
121
- })
122
-
123
110
  this.addBind('ci:vitest:test:start', (ctx) => {
124
111
  const {
125
112
  testName,
@@ -424,12 +411,13 @@ class VitestPlugin extends CiPlugin {
424
411
  this._setRepositoryRoot(repositoryRoot, codeOwnersEntries)
425
412
  this.command = testCommand
426
413
  this.frameworkVersion = frameworkVersion
427
- const testSessionSpanContext = testSessionId && testModuleId
428
- ? this.tracer.extract('text_map', {
429
- 'x-datadog-trace-id': testSessionId,
430
- 'x-datadog-parent-id': testModuleId,
431
- })
432
- : undefined
414
+ let testSessionSpanContext
415
+ if (testSessionId && testModuleId) {
416
+ const carrier = /** @type {Record<string, unknown>} */ ({})
417
+ writeDatadogTraceId(carrier, testSessionId)
418
+ writeDatadogParentId(carrier, testModuleId)
419
+ testSessionSpanContext = this.tracer.extract('text_map', carrier)
420
+ }
433
421
 
434
422
  const trimmedCommand = DD_MAJOR < 6 ? this.command : 'vitest run'
435
423
  // test suites run in a different process, so they also need to init the metadata dictionary
@@ -535,6 +523,7 @@ class VitestPlugin extends CiPlugin {
535
523
  this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_FINISHED, 'suite', { library: coverageLibrary })
536
524
  this.telemetry.distribution(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeFiles.length)
537
525
  }
526
+ this.tracer._exporter.deferTestSuiteSpan?.(testSuiteSpan)
538
527
  testSuiteSpan.finish()
539
528
  finishAllTraceSpans(testSuiteSpan)
540
529
  }
@@ -578,6 +567,7 @@ class VitestPlugin extends CiPlugin {
578
567
  this.addSub('ci:vitest:session:finish', ({
579
568
  status,
580
569
  error,
570
+ isTestSessionFinalizationError,
581
571
  testCodeCoverageLinesTotal,
582
572
  isEarlyFlakeDetectionEnabled,
583
573
  isEarlyFlakeDetectionFaulty,
@@ -600,6 +590,9 @@ class VitestPlugin extends CiPlugin {
600
590
  this.testSessionSpan.setTag(TEST_STATUS, status)
601
591
  this.testModuleSpan.setTag(TEST_STATUS, status)
602
592
  if (error) {
593
+ if (isTestSessionFinalizationError) {
594
+ this.tracer._exporter.setDeferredTestSuiteError?.(error)
595
+ }
603
596
  this.testModuleSpan.setTag('error', error)
604
597
  this.testSessionSpan.setTag('error', error)
605
598
  }
@@ -629,6 +622,7 @@ class VitestPlugin extends CiPlugin {
629
622
  if (vitestPool) {
630
623
  this.testSessionSpan.setTag(VITEST_POOL, vitestPool)
631
624
  }
625
+ this.tracer._exporter.exportDeferredTestSuiteSpans?.()
632
626
  this.testModuleSpan.finish()
633
627
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
634
628
  this.testSessionSpan.finish()
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { readDatadogTraceId, readTraceparent } = require('../../dd-trace/src/carrier')
3
4
  const DatadogSpanContext = require('../../dd-trace/src/opentracing/span_context')
4
5
 
5
6
  const TRACE_ID_UPPER_TAG = '_dd.p.tid'
@@ -38,7 +39,7 @@ function createWebSocketSpanContext (spanContext) {
38
39
  * @returns {boolean}
39
40
  */
40
41
  function hasTraceHeaders (headers) {
41
- return !!(headers && (headers['x-datadog-trace-id'] || headers.traceparent))
42
+ return !!(headers && (readDatadogTraceId(headers) || readTraceparent(headers)))
42
43
  }
43
44
 
44
45
  /**
@@ -6,7 +6,6 @@ const fs = require('node:fs')
6
6
  const instrumentations = require('../datadog-instrumentations/src/helpers/instrumentations')
7
7
  const extractPackageAndModulePath = require('../datadog-instrumentations/src/helpers/extract-package-and-module-path')
8
8
  const hooks = require('../datadog-instrumentations/src/helpers/hooks')
9
- const { matchesOptionalPeerFile } = require('../datadog-instrumentations/src/helpers/optional-peer-bundler')
10
9
  const { isESMFile } = require('../datadog-esbuild/src/utils')
11
10
  const log = require('./src/log')
12
11
 
@@ -137,16 +136,6 @@ class DatadogWebpackPlugin {
137
136
 
138
137
  const normalizedResource = resource.replaceAll('\\', '/')
139
138
 
140
- // Rewrite optional-peer loads so installed peers get bundled and survive relocation
141
- // (#8980); absent peers stay opaque, so a build that does not opt into the feature does
142
- // not follow their dependency chain (#8635).
143
- if (matchesOptionalPeerFile(normalizedResource)) {
144
- createData.loaders ||= []
145
- createData.loaders.push({ loader: require.resolve('./src/optional-peer-loader') })
146
- log.debug('INLINE: optional-peer loader applied to %s', normalizedResource)
147
- return
148
- }
149
-
150
139
  if (!resource.includes('node_modules')) {
151
140
  return
152
141
  }
@@ -1,4 +1,3 @@
1
1
  'use strict'
2
2
 
3
- require('./src/openfeature/register')
4
3
  module.exports = require('./src/bootstrap')
@@ -17,8 +17,10 @@ module.exports = {
17
17
  * Fetches agent information from the /info endpoint
18
18
  * @param {URL} url - The agent URL
19
19
  * @param {Function} callback - Callback function with signature (err, agentInfo)
20
+ * @param {{ deadline?: number, signal?: AbortSignal }} [options] - Request finalization options
21
+ * @param {Function} [makeRequest] - Request implementation
20
22
  */
21
- function fetchAgentInfo (url, callback) {
23
+ function fetchAgentInfo (url, callback, options = {}, makeRequest = request) {
22
24
  const urlKey = url.href
23
25
 
24
26
  if (cachedUrl !== null && cachedUrl !== urlKey) {
@@ -29,10 +31,9 @@ function fetchAgentInfo (url, callback) {
29
31
  return process.nextTick(callback, null, cachedData)
30
32
  }
31
33
 
32
- request('', {
33
- path: '/info',
34
- url,
35
- }, (err, res) => {
34
+ options.path = '/info'
35
+ options.url = url
36
+ makeRequest('', options, (err, res) => {
36
37
  if (err) {
37
38
  return callback(err)
38
39
  }
@@ -1,5 +1,27 @@
1
1
  'use strict'
2
2
 
3
+ const tracerVersion = require('../../../../package.json').version
4
+ const { AIGuardClientError } = require('./errors')
5
+ const { parseEvaluationResponse } = require('./evaluation')
6
+ const TAGS = require('./tags')
7
+
8
+ /**
9
+ * Resolves the AI Guard host for a Datadog site.
10
+ *
11
+ * @param {string} site
12
+ * @returns {string}
13
+ */
14
+ function aiGuardHost (site) {
15
+ return site.split('.').length === 2 ? `app.${site}` : site
16
+ }
17
+
18
+ /**
19
+ * Sends a request to the AI Guard service.
20
+ *
21
+ * @param {object} body
22
+ * @param {{ url: string, headers: Record<string, string>, timeout: number }} opts
23
+ * @returns {Promise<{ status: number, body: unknown }>}
24
+ */
3
25
  async function executeRequest (body, opts) {
4
26
  const postData = JSON.stringify(body)
5
27
  const headers = {
@@ -22,4 +44,81 @@ async function executeRequest (body, opts) {
22
44
  }
23
45
  }
24
46
 
25
- module.exports = executeRequest
47
+ class AIGuardClient {
48
+ #headers
49
+ #evaluateUrl
50
+ #timeout
51
+
52
+ /**
53
+ * @param {import('../config/config-base')} config
54
+ */
55
+ constructor (config) {
56
+ this.#headers = {
57
+ 'DD-API-KEY': config.DD_API_KEY,
58
+ 'DD-APPLICATION-KEY': config.DD_APP_KEY,
59
+ 'DD-AI-GUARD-VERSION': tracerVersion,
60
+ 'DD-AI-GUARD-SOURCE': 'SDK',
61
+ 'DD-AI-GUARD-LANGUAGE': 'nodejs',
62
+ }
63
+ const endpoint = config.experimental.aiguard.endpoint || `https://${aiGuardHost(config.site)}/api/v2/ai-guard`
64
+ this.#evaluateUrl = `${endpoint}/evaluate`
65
+ this.#timeout = config.experimental.aiguard.timeout
66
+ }
67
+
68
+ /**
69
+ * Evaluates messages and converts the service response to the internal evaluation contract.
70
+ *
71
+ * @param {import('../../../../index').aiguard.Message[]} messages
72
+ * @param {{ service: string, env: string }} meta
73
+ * @returns {Promise<NonNullable<ReturnType<typeof parseEvaluationResponse>>>}
74
+ */
75
+ evaluate (messages, meta) {
76
+ const payload = {
77
+ data: {
78
+ attributes: {
79
+ messages,
80
+ meta,
81
+ },
82
+ },
83
+ }
84
+ return executeRequest(
85
+ payload,
86
+ { url: this.#evaluateUrl, headers: this.#headers, timeout: this.#timeout }
87
+ )
88
+ .then(response => this.#parseResponse(response))
89
+ .catch(cause => {
90
+ if (cause instanceof AIGuardClientError) throw cause
91
+
92
+ throw new AIGuardClientError(`Unexpected error calling AI Guard service: ${cause.message}`, {
93
+ cause,
94
+ telemetryType: TAGS.ERROR_TYPE_CLIENT,
95
+ })
96
+ })
97
+ }
98
+
99
+ /**
100
+ * Validates an AI Guard HTTP response.
101
+ *
102
+ * @param {{ status: number, body: unknown }} response
103
+ * @returns {NonNullable<ReturnType<typeof parseEvaluationResponse>>}
104
+ */
105
+ #parseResponse (response) {
106
+ if (response.status !== 200) {
107
+ throw new AIGuardClientError(`AI Guard service call failed, status ${response.status}`, {
108
+ errors: response.body?.errors,
109
+ telemetryType: TAGS.ERROR_TYPE_STATUS,
110
+ })
111
+ }
112
+
113
+ const evaluation = parseEvaluationResponse(response.body)
114
+ if (!evaluation) {
115
+ throw new AIGuardClientError(`AI Guard service returned unexpected response : ${response.body}`, {
116
+ telemetryType: TAGS.ERROR_TYPE_RESPONSE,
117
+ })
118
+ }
119
+
120
+ return evaluation
121
+ }
122
+ }
123
+
124
+ module.exports = AIGuardClient
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ class AIGuardAbortError extends Error {
4
+ /**
5
+ * @param {string|undefined} reason
6
+ * @param {Array<unknown>} tags
7
+ * @param {Record<string, unknown>} tagProbabilities
8
+ * @param {Array<unknown>} sds
9
+ */
10
+ constructor (reason, tags, tagProbabilities, sds) {
11
+ super(reason)
12
+ this.name = 'AIGuardAbortError'
13
+ this.reason = reason
14
+ this.tags = tags
15
+ this.tagProbabilities = tagProbabilities
16
+ this.sds = sds || []
17
+ }
18
+ }
19
+
20
+ class AIGuardClientError extends Error {
21
+ /**
22
+ * @param {string} message
23
+ * @param {{ telemetryType: string, errors?: Array<unknown>, cause?: Error }} opts
24
+ */
25
+ constructor (message, opts) {
26
+ super(message)
27
+ this.name = 'AIGuardClientError'
28
+ this.telemetryType = opts.telemetryType
29
+ if (opts.errors) {
30
+ this.errors = opts.errors
31
+ }
32
+ if (opts.cause) {
33
+ this.cause = opts.cause
34
+ }
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ AIGuardAbortError,
40
+ AIGuardClientError,
41
+ }
@@ -0,0 +1,357 @@
1
+ 'use strict'
2
+
3
+ const { HTTP_CLIENT_IP, HTTP_USERAGENT, NETWORK_CLIENT_IP } = require('../../../../ext/tags')
4
+ const clone = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false })
5
+ const { USER_ID, USER_SESSION_ID } = require('../appsec/addresses')
6
+ const { getActiveRequest } = require('../appsec/store')
7
+ const { keepTrace } = require('../priority_sampler')
8
+ const { extractIp } = require('../plugins/util/ip_extractor')
9
+ const { AI_GUARD } = require('../standalone/product')
10
+ const telemetryMetrics = require('../telemetry/metrics')
11
+ const TAGS = require('./tags')
12
+
13
+ const ALLOW = 'ALLOW'
14
+
15
+ /** @typedef {import('../../../../index').aiguard.Message} Message */
16
+ /** @typedef {import('../opentracing/span')} Span */
17
+
18
+ /**
19
+ * @typedef {object} EvaluationResponse
20
+ * @property {string} action
21
+ * @property {string|undefined} reason
22
+ * @property {Array<unknown>} tags
23
+ * @property {Array<unknown>} sdsFindings
24
+ * @property {Record<string, unknown>} tagProbabilities
25
+ * @property {boolean} hasTagProbabilities
26
+ * @property {boolean} blockingEnabled
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} EvaluationOutcome
31
+ * @property {{ action: string, reason: string|undefined, tags: Array<unknown>,
32
+ * tagProbabilities: Record<string, unknown>, sds: Array<unknown> }} result
33
+ * @property {boolean} shouldBlock
34
+ * @property {boolean} hasTagProbabilities
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} EvaluationMetaStruct
39
+ * @property {Message[]} messages
40
+ * @property {Array<unknown>} [attack_categories]
41
+ * @property {Array<unknown>} [sds]
42
+ * @property {Record<string, unknown>} [tag_probs]
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} EvaluationReport
47
+ * @property {Span} span
48
+ * @property {EvaluationMetaStruct} metaStruct
49
+ * @property {{ source: string, integration: string }} telemetryTags
50
+ */
51
+
52
+ /**
53
+ * Returns whether a value is a non-array object.
54
+ *
55
+ * @param {unknown} value
56
+ * @returns {value is Record<string, unknown>}
57
+ */
58
+ function isRecord (value) {
59
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
60
+ }
61
+
62
+ /**
63
+ * Parses the backend evaluation response into the tracer's internal contract.
64
+ *
65
+ * @param {unknown} body
66
+ * @returns {EvaluationResponse|undefined}
67
+ */
68
+ function parseEvaluationResponse (body) {
69
+ if (!isRecord(body) || !isRecord(body.data) || !isRecord(body.data.attributes)) return
70
+
71
+ const attributes = body.data.attributes
72
+ if (typeof attributes.action !== 'string' || attributes.action.length === 0) return
73
+
74
+ return {
75
+ action: attributes.action,
76
+ reason: typeof attributes.reason === 'string' ? attributes.reason : undefined,
77
+ tags: Array.isArray(attributes.tags) ? attributes.tags : [],
78
+ sdsFindings: Array.isArray(attributes.sds_findings) ? attributes.sds_findings : [],
79
+ tagProbabilities: isRecord(attributes.tag_probs) ? attributes.tag_probs : {},
80
+ hasTagProbabilities: isRecord(attributes.tag_probs),
81
+ blockingEnabled: attributes.is_blocking_enabled === true,
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Determines whether the current evaluation must abort the guarded operation.
87
+ *
88
+ * @param {boolean} block
89
+ * @param {{ action: string, blockingEnabled: boolean }} evaluation
90
+ * @returns {boolean}
91
+ */
92
+ function shouldBlockEvaluation (block, evaluation) {
93
+ return block && evaluation.blockingEnabled && evaluation.action !== ALLOW
94
+ }
95
+
96
+ /**
97
+ * Applies local evaluation policy and creates the result returned to the caller.
98
+ *
99
+ * @param {EvaluationResponse} evaluation
100
+ * @param {boolean} block
101
+ * @returns {EvaluationOutcome}
102
+ */
103
+ function createEvaluationOutcome (evaluation, block) {
104
+ return {
105
+ result: {
106
+ action: evaluation.action,
107
+ reason: evaluation.reason,
108
+ tags: evaluation.tags,
109
+ tagProbabilities: evaluation.tagProbabilities,
110
+ sds: evaluation.sdsFindings,
111
+ },
112
+ shouldBlock: shouldBlockEvaluation(block, evaluation),
113
+ hasTagProbabilities: evaluation.hasTagProbabilities,
114
+ }
115
+ }
116
+
117
+ const aiguardMetrics = telemetryMetrics.manager.namespace('ai_guard')
118
+
119
+ // Tags from the service entry span that must be mirrored onto every AI Guard span
120
+ // so anomaly detection pipelines can process each span independently.
121
+ const SERVICE_ENTRY_TAG_MAPPINGS = [
122
+ [HTTP_USERAGENT, TAGS.HTTP_USERAGENT_TAG_KEY],
123
+ [HTTP_CLIENT_IP, TAGS.HTTP_CLIENT_IP_TAG_KEY],
124
+ [NETWORK_CLIENT_IP, TAGS.NETWORK_CLIENT_IP_TAG_KEY],
125
+ [USER_ID, TAGS.USR_ID_TAG_KEY],
126
+ [USER_SESSION_ID, TAGS.USR_SESSION_ID_TAG_KEY],
127
+ ]
128
+
129
+ class EvaluationReporter {
130
+ #config
131
+ #maxMessagesLength
132
+ #maxContentSize
133
+
134
+ /**
135
+ * @param {import('../config/config-base')} config
136
+ */
137
+ constructor (config) {
138
+ this.#config = config
139
+ this.#maxMessagesLength = config.experimental.aiguard.maxMessagesLength
140
+ this.#maxContentSize = config.experimental.aiguard.maxContentSize
141
+ }
142
+
143
+ /**
144
+ * Initializes span and trace reporting before the AI Guard request starts.
145
+ *
146
+ * @param {Span} span
147
+ * @param {Message[]} messages
148
+ * @param {{ source: string, integration: string }} options
149
+ * @returns {EvaluationReport}
150
+ */
151
+ start (span, messages, options) {
152
+ const telemetryTags = { source: options.source, integration: options.integration }
153
+ const last = messages.at(-1)
154
+ const target = this.#isToolCall(last) ? 'tool' : 'prompt'
155
+ span.setTag(TAGS.TARGET_TAG_KEY, target)
156
+ if (target === 'tool') {
157
+ const name = this.#getToolName(last, messages)
158
+ if (name) {
159
+ span.setTag(TAGS.TOOL_NAME_TAG_KEY, name)
160
+ }
161
+ }
162
+
163
+ const metaStruct = {
164
+ messages: this.#buildMessagesForMetaStruct(messages, telemetryTags),
165
+ }
166
+ span.meta_struct = {
167
+ [TAGS.META_STRUCT_KEY]: metaStruct,
168
+ }
169
+
170
+ const rootSpan = span.context()?._trace?.started?.[0]
171
+ if (rootSpan) {
172
+ this.#setRootSpanClientIpTags(rootSpan)
173
+ this.#copyServiceEntryTagsToGuardSpan(span, rootSpan)
174
+ // This must happen before the request so its sampling decision reaches outgoing HTTP spans.
175
+ keepTrace(rootSpan, AI_GUARD)
176
+ rootSpan.setTag(TAGS.EVENT_TAG_KEY, 'true')
177
+ }
178
+
179
+ return {
180
+ span,
181
+ metaStruct,
182
+ telemetryTags,
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Reports a failed evaluation request.
188
+ *
189
+ * @param {EvaluationReport} report
190
+ * @param {string} errorType
191
+ * @returns {void}
192
+ */
193
+ fail (report, errorType) {
194
+ aiguardMetrics.count(TAGS.TELEMETRY_REQUESTS, { error: true, ...report.telemetryTags }).inc(1)
195
+ aiguardMetrics.count(TAGS.TELEMETRY_ERROR, { type: errorType, ...report.telemetryTags }).inc(1)
196
+ }
197
+
198
+ /**
199
+ * Reports a successful evaluation and its local policy outcome.
200
+ *
201
+ * @param {EvaluationReport} report
202
+ * @param {EvaluationOutcome} outcome
203
+ * @returns {void}
204
+ */
205
+ finish (report, outcome) {
206
+ const { result, shouldBlock } = outcome
207
+
208
+ if (result.tags.length > 0) report.metaStruct.attack_categories = result.tags
209
+ if (result.sds.length > 0) report.metaStruct.sds = result.sds
210
+ if (outcome.hasTagProbabilities) report.metaStruct.tag_probs = result.tagProbabilities
211
+
212
+ const requestTelemetryTags = {
213
+ action: result.action,
214
+ error: false,
215
+ block: shouldBlock,
216
+ ...report.telemetryTags,
217
+ }
218
+ aiguardMetrics.count(TAGS.TELEMETRY_REQUESTS, requestTelemetryTags).inc(1)
219
+
220
+ report.span.setTag(TAGS.ACTION_TAG_KEY, result.action)
221
+ if (result.reason) {
222
+ report.span.setTag(TAGS.REASON_TAG_KEY, result.reason)
223
+ }
224
+ if (shouldBlock) {
225
+ report.span.setTag(TAGS.BLOCKED_TAG_KEY, 'true')
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Returns a safe, size-limited message copy for the span meta-struct.
231
+ *
232
+ * @param {Message[]} messages
233
+ * @param {{ source: string, integration: string }} telemetryTags
234
+ * @returns {Message[]}
235
+ */
236
+ #buildMessagesForMetaStruct (messages, telemetryTags) {
237
+ const size = Math.min(messages.length, this.#maxMessagesLength)
238
+ if (messages.length > size) {
239
+ aiguardMetrics.count(TAGS.TELEMETRY_TRUNCATED, { type: 'messages', ...telemetryTags }).inc(1)
240
+ }
241
+ const result = []
242
+ let contentTruncated = false
243
+ for (let i = messages.length - size; i < messages.length; i++) {
244
+ const message = clone(messages[i])
245
+ if (message.content?.length > this.#maxContentSize) {
246
+ contentTruncated = true
247
+ message.content = message.content.slice(0, this.#maxContentSize)
248
+ }
249
+ result.push(message)
250
+ }
251
+ if (contentTruncated) {
252
+ aiguardMetrics.count(TAGS.TELEMETRY_TRUNCATED, { type: 'content', ...telemetryTags }).inc(1)
253
+ }
254
+ return result
255
+ }
256
+
257
+ /**
258
+ * Returns whether a message represents a tool call or tool output.
259
+ *
260
+ * @param {Message} message
261
+ * @returns {boolean}
262
+ */
263
+ #isToolCall (message) {
264
+ return Boolean(message.tool_calls || message.tool_call_id)
265
+ }
266
+
267
+ /**
268
+ * Resolves the tool name associated with a tool call or output message.
269
+ *
270
+ * @param {Message} message
271
+ * @param {Message[]} history
272
+ * @returns {string|null}
273
+ */
274
+ #getToolName (message, history) {
275
+ if (message.tool_calls) {
276
+ const names = message.tool_calls.map(tool => tool.function.name)
277
+ return names.length === 0 ? null : names.join(',')
278
+ }
279
+
280
+ const id = message.tool_call_id
281
+ for (let i = history.length - 2; i >= 0; i--) {
282
+ const item = history[i]
283
+ if (item.tool_calls) {
284
+ for (const toolCall of item.tool_calls) {
285
+ if (toolCall.id === id) {
286
+ return toolCall.function.name
287
+ }
288
+ }
289
+ }
290
+ }
291
+ return null
292
+ }
293
+
294
+ /**
295
+ * Adds missing client IP tags to the service entry span.
296
+ *
297
+ * @param {Span} rootSpan
298
+ * @returns {void}
299
+ */
300
+ #setRootSpanClientIpTags (rootSpan) {
301
+ const currentTags = rootSpan.context().getTags()
302
+ const needsHttpClientIp = !Object.hasOwn(currentTags, HTTP_CLIENT_IP)
303
+ const needsNetworkClientIp = !Object.hasOwn(currentTags, NETWORK_CLIENT_IP)
304
+
305
+ if (!needsHttpClientIp && !needsNetworkClientIp) return
306
+
307
+ const request = getActiveRequest()
308
+ if (!request) return
309
+
310
+ const newTags = {}
311
+ let hasNewTags = false
312
+
313
+ if (needsHttpClientIp) {
314
+ const clientIp = extractIp(this.#config, request)
315
+ if (clientIp) {
316
+ newTags[HTTP_CLIENT_IP] = clientIp
317
+ hasNewTags = true
318
+ }
319
+ }
320
+
321
+ if (needsNetworkClientIp) {
322
+ const networkClientIp = request.socket?.remoteAddress
323
+ if (networkClientIp) {
324
+ newTags[NETWORK_CLIENT_IP] = networkClientIp
325
+ hasNewTags = true
326
+ }
327
+ }
328
+
329
+ if (hasNewTags) {
330
+ rootSpan.addTags(newTags)
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Copies service entry tags required by anomaly detection to the AI Guard span.
336
+ *
337
+ * @param {Span} guardSpan
338
+ * @param {Span} rootSpan
339
+ * @returns {void}
340
+ */
341
+ #copyServiceEntryTagsToGuardSpan (guardSpan, rootSpan) {
342
+ const rootTags = rootSpan.context().getTags()
343
+ for (const [sourceTag, destinationTag] of SERVICE_ENTRY_TAG_MAPPINGS) {
344
+ const value = rootTags[sourceTag]
345
+ if (value !== undefined && value !== null) {
346
+ guardSpan.setTag(destinationTag, value)
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ module.exports = {
353
+ createEvaluationOutcome,
354
+ EvaluationReporter,
355
+ parseEvaluationResponse,
356
+ shouldBlockEvaluation,
357
+ }