dd-trace 5.117.0 → 5.118.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 (83) hide show
  1. package/ci/diagnose.js +162 -21
  2. package/ci/init.js +0 -1
  3. package/ci/runbook.md +187 -180
  4. package/ci/test-optimization-validation/approval-artifacts.js +3 -1
  5. package/ci/test-optimization-validation/approval.js +52 -15
  6. package/ci/test-optimization-validation/ci-command-candidate.js +26 -32
  7. package/ci/test-optimization-validation/ci-discovery.js +1 -1
  8. package/ci/test-optimization-validation/ci-package-scripts.js +141 -0
  9. package/ci/test-optimization-validation/ci-remediation.js +112 -39
  10. package/ci/test-optimization-validation/cli.js +516 -681
  11. package/ci/test-optimization-validation/command-blocker.js +188 -33
  12. package/ci/test-optimization-validation/command-output-policy.js +3 -26
  13. package/ci/test-optimization-validation/command-runner.js +189 -245
  14. package/ci/test-optimization-validation/environment.js +90 -0
  15. package/ci/test-optimization-validation/executable.js +159 -388
  16. package/ci/test-optimization-validation/framework-adapters/cucumber.js +119 -0
  17. package/ci/test-optimization-validation/framework-adapters/cypress.js +107 -0
  18. package/ci/test-optimization-validation/framework-adapters/playwright.js +181 -0
  19. package/ci/test-optimization-validation/generated-files.js +75 -13
  20. package/ci/test-optimization-validation/generated-test-contract.js +283 -0
  21. package/ci/test-optimization-validation/generated-verifier.js +49 -16
  22. package/ci/test-optimization-validation/literal-environment.js +57 -0
  23. package/ci/test-optimization-validation/manifest-loader.js +2 -22
  24. package/ci/test-optimization-validation/manifest-scaffold.js +816 -515
  25. package/ci/test-optimization-validation/manifest-schema.js +494 -702
  26. package/ci/test-optimization-validation/offline-fixtures.js +4 -1
  27. package/ci/test-optimization-validation/plan-writer.js +243 -984
  28. package/ci/test-optimization-validation/preflight-runner.js +49 -55
  29. package/ci/test-optimization-validation/redaction.js +2 -1
  30. package/ci/test-optimization-validation/report-writer.js +391 -1357
  31. package/ci/test-optimization-validation/result-semantics.js +86 -0
  32. package/ci/test-optimization-validation/runner-command.js +249 -0
  33. package/ci/test-optimization-validation/runner-contract.js +664 -0
  34. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +29 -3
  35. package/ci/test-optimization-validation/scenarios/basic-reporting.js +240 -543
  36. package/ci/test-optimization-validation/scenarios/ci-wiring.js +450 -670
  37. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +4 -3
  38. package/ci/test-optimization-validation/scenarios/helpers.js +67 -9
  39. package/ci/test-optimization-validation/scenarios/test-management.js +4 -3
  40. package/ci/test-optimization-validation/source-text.js +87 -0
  41. package/ci/test-optimization-validation/test-output.js +12 -22
  42. package/ext/tags.d.ts +1 -0
  43. package/index.d.ts +7 -0
  44. package/package.json +2 -2
  45. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/openai-agents.js +31 -0
  48. package/packages/datadog-instrumentations/src/openai-agents.js +159 -0
  49. package/packages/datadog-plugin-openai-agents/src/index.js +74 -0
  50. package/packages/datadog-plugin-openai-agents/src/integration.js +503 -0
  51. package/packages/datadog-plugin-openai-agents/src/processor.js +108 -0
  52. package/packages/datadog-plugin-openai-agents/src/util.js +60 -0
  53. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +48 -49
  54. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +161 -33
  55. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +6 -4
  56. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  57. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  58. package/packages/dd-trace/src/encode/span-stats.js +7 -1
  59. package/packages/dd-trace/src/exporter.js +16 -9
  60. package/packages/dd-trace/src/llmobs/index.js +9 -1
  61. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +104 -22
  62. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +5 -9
  63. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +20 -0
  64. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +321 -0
  65. package/packages/dd-trace/src/llmobs/sdk.js +2 -1
  66. package/packages/dd-trace/src/llmobs/span_processor.js +7 -2
  67. package/packages/dd-trace/src/llmobs/tagger.js +23 -3
  68. package/packages/dd-trace/src/llmobs/util.js +56 -3
  69. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +3 -6
  70. package/packages/dd-trace/src/openfeature/configuration_source.js +5 -8
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +5 -1
  72. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -3
  73. package/packages/dd-trace/src/plugins/index.js +1 -0
  74. package/packages/dd-trace/src/plugins/outbound.js +4 -1
  75. package/packages/dd-trace/src/span_processor.js +1 -1
  76. package/packages/dd-trace/src/span_stats.js +22 -4
  77. package/ci/test-optimization-validation/command-suitability.js +0 -471
  78. package/ci/test-optimization-validation/init-probe-preload.js +0 -163
  79. package/ci/test-optimization-validation/init-probe.js +0 -246
  80. package/ci/test-optimization-validation/late-initialization.js +0 -63
  81. package/ci/test-optimization-validation/local-command.js +0 -163
  82. package/ci/test-optimization-validation/setup-runner.js +0 -97
  83. package/ci/test-optimization-validation-manifest.schema.json +0 -1
@@ -7,6 +7,7 @@ const {
7
7
  failWithDebugRerun,
8
8
  pass,
9
9
  prepareGeneratedScenario,
10
+ reportMissingGeneratedTest,
10
11
  requireGeneratedScenario,
11
12
  runInstrumentedCommand,
12
13
  skip,
@@ -28,14 +29,14 @@ async function runEarlyFlakeDetection ({ framework, out, options }) {
28
29
 
29
30
  const discovery = await discoverScenarioTests({ framework, out, scenarioName, scenario, options })
30
31
  if (discovery.tests.length === 0) {
31
- return failWithDebugRerun({
32
+ return reportMissingGeneratedTest({
32
33
  command: scenario.runCommand,
33
34
  diagnosis: 'The generated new-test candidate was not reported during baseline identity discovery.',
34
- evidence: discoveryEvidence(discovery),
35
+ discovery,
35
36
  framework,
36
37
  options,
37
38
  out,
38
- outDir: discovery.outDir,
39
+ scenario,
39
40
  scenarioName,
40
41
  })
41
42
  }
@@ -4,7 +4,7 @@ const fs = require('node:fs')
4
4
  const path = require('path')
5
5
 
6
6
  const { getArtifactId } = require('../artifact-id')
7
- const { buildCiWiringEnv, buildDatadogEnv, runCommand } = require('../command-runner')
7
+ const { buildDatadogEnv, buildOfflineCaptureEnv, runCommand } = require('../command-runner')
8
8
  const {
9
9
  cleanupGeneratedRuntimeFiles,
10
10
  findGeneratedScenario,
@@ -14,10 +14,10 @@ const {
14
14
  eventsOfType,
15
15
  findTestsByIdentity,
16
16
  } = require('../payload-normalizer')
17
- const { getLocalValidationCommand } = require('../local-command')
18
17
  const { cleanupOfflineFixture, createOfflineFixture } = require('../offline-fixtures')
19
18
  const { readOfflineOutput } = require('../offline-output')
20
19
  const { sanitizeForReport, sanitizeString } = require('../redaction')
20
+ const { getGeneratedCommand } = require('../runner-command')
21
21
  const { ensureSafeDirectory, writeFileSafely } = require('../safe-files')
22
22
 
23
23
  const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}${String.raw`\[[0-?]*[ -/]*[@-~]`}`, 'g')
@@ -34,7 +34,7 @@ async function runInstrumentedCommand ({
34
34
  options,
35
35
  extraEnv,
36
36
  fixtureConfig,
37
- ciWiring = false,
37
+ injectInitialization = true,
38
38
  allowMissingInitialization = false,
39
39
  }) {
40
40
  const outDir = frameworkOutDir(out, framework, scenarioName)
@@ -52,9 +52,9 @@ async function runInstrumentedCommand ({
52
52
  scenarioName,
53
53
  ...fixtureConfig,
54
54
  })
55
- const validationEnv = ciWiring
56
- ? buildCiWiringEnv({ fixture, outputRoot: rawOutputRoot })
57
- : buildDatadogEnv({ fixture, outputRoot: rawOutputRoot, scenario: scenarioName, framework })
55
+ const validationEnv = injectInitialization
56
+ ? buildDatadogEnv({ fixture, outputRoot: rawOutputRoot, scenario: scenarioName, framework })
57
+ : buildOfflineCaptureEnv({ fixture, outputRoot: rawOutputRoot })
58
58
  result = await runCommand(command, {
59
59
  env: {
60
60
  ...validationEnv,
@@ -82,7 +82,7 @@ async function runInstrumentedCommand ({
82
82
  `${JSON.stringify(sanitizeForReport(result), null, 2)}\n`,
83
83
  'scenario result artifact'
84
84
  )
85
- if (!offline.initialized && !ciWiring && !allowMissingInitialization) {
85
+ if (!offline.initialized && injectInitialization && !allowMissingInitialization) {
86
86
  const stderr = sanitizeString(result.stderr).trim().slice(-2000)
87
87
  throw new Error(
88
88
  'Offline Test Optimization exporter did not initialize or write completion evidence. ' +
@@ -180,11 +180,11 @@ async function prepareGeneratedScenario (framework, scenarioId) {
180
180
  const scenario = findGeneratedScenario(framework, scenarioId)
181
181
  if (!scenario) return { scenario: null, written: [] }
182
182
  cleanupGeneratedRuntimeFiles(framework)
183
- const written = await writeGeneratedFiles(framework)
183
+ const written = await writeGeneratedFiles(framework, scenario)
184
184
  return {
185
185
  scenario: {
186
186
  ...scenario,
187
- runCommand: getLocalValidationCommand(framework, scenario.runCommand),
187
+ runCommand: getGeneratedCommand(framework, scenario),
188
188
  },
189
189
  written,
190
190
  }
@@ -314,6 +314,63 @@ async function discoverScenarioTests ({ framework, out, scenarioName, scenario,
314
314
  }
315
315
  }
316
316
 
317
+ /**
318
+ * Reports a missing generated test without blaming dd-trace when clean execution was unproven.
319
+ *
320
+ * @param {object} input missing-test evidence
321
+ * @param {object} input.command generated scenario command
322
+ * @param {string} input.diagnosis diagnosis used when clean execution was proven
323
+ * @param {object} input.discovery instrumented baseline result
324
+ * @param {object} input.framework framework manifest entry
325
+ * @param {object} input.options execution options
326
+ * @param {string} input.out validation output root
327
+ * @param {object} input.scenario generated scenario
328
+ * @param {string} input.scenarioName advanced scenario name
329
+ * @returns {object|Promise<object>} incomplete or confirmed failure result
330
+ */
331
+ function reportMissingGeneratedTest ({
332
+ command,
333
+ diagnosis,
334
+ discovery,
335
+ framework,
336
+ options,
337
+ out,
338
+ scenario,
339
+ scenarioName,
340
+ }) {
341
+ const verification = framework.generatedTestStrategy?.verification?.observedScenarios
342
+ ?.find(observed => observed.id === scenario.id)
343
+ const evidence = {
344
+ ...discoveryEvidence(discovery),
345
+ generatedVerificationObservedTestCount: verification?.observedTestCount,
346
+ }
347
+ if (verification?.observedTestCount === null) {
348
+ return inconclusive(
349
+ framework,
350
+ scenarioName,
351
+ 'The clean temporary validation command exited as expected without a parseable test count, and the ' +
352
+ 'instrumented baseline emitted no matching test event. The validator cannot prove that the generated test ' +
353
+ 'executed, so no advanced-feature conclusion was reached.',
354
+ {
355
+ ...evidence,
356
+ reasonCode: 'generated-test-execution-unproven',
357
+ },
358
+ discovery.outDir
359
+ )
360
+ }
361
+
362
+ return failWithDebugRerun({
363
+ command,
364
+ diagnosis,
365
+ evidence,
366
+ framework,
367
+ options,
368
+ out,
369
+ outDir: discovery.outDir,
370
+ scenarioName,
371
+ })
372
+ }
373
+
317
374
  function testsForDiscoveredScenario (events, scenario, discovery) {
318
375
  if (discovery?.testIdentities?.length > 0) {
319
376
  return findTestsByIdentity(events, discovery.testIdentities)
@@ -528,6 +585,7 @@ module.exports = {
528
585
  inconclusive,
529
586
  pass,
530
587
  prepareGeneratedScenario,
588
+ reportMissingGeneratedTest,
531
589
  requireGeneratedScenario,
532
590
  runDebugInstrumentedCommand,
533
591
  runInstrumentedCommand,
@@ -9,6 +9,7 @@ const {
9
9
  failWithDebugRerun,
10
10
  pass,
11
11
  prepareGeneratedScenario,
12
+ reportMissingGeneratedTest,
12
13
  requireGeneratedScenario,
13
14
  runInstrumentedCommand,
14
15
  testEventSamples,
@@ -25,14 +26,14 @@ async function runTestManagement ({ framework, out, options }) {
25
26
  const { scenario } = await prepareGeneratedScenario(framework, 'test-management-target')
26
27
  const discovery = await discoverScenarioTests({ framework, out, scenarioName, scenario, options })
27
28
  if (discovery.tests.length === 0) {
28
- return failWithDebugRerun({
29
+ return reportMissingGeneratedTest({
29
30
  command: scenario.runCommand,
30
31
  diagnosis: 'The test-management target was not reported during baseline identity discovery.',
31
- evidence: discoveryEvidence(discovery),
32
+ discovery,
32
33
  framework,
33
34
  options,
34
35
  out,
35
- outDir: discovery.outDir,
36
+ scenario,
36
37
  scenarioName,
37
38
  })
38
39
  }
@@ -0,0 +1,87 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Replaces JavaScript comments with spaces while preserving line structure and string literals.
5
+ *
6
+ * @param {string} source JavaScript or TypeScript source
7
+ * @returns {string} source with comments masked
8
+ */
9
+ function maskJavaScriptComments (source) {
10
+ return maskJavaScriptSource(source, false)
11
+ }
12
+
13
+ /**
14
+ * Replaces JavaScript comments and string literals with spaces while preserving line structure.
15
+ *
16
+ * @param {string} source JavaScript or TypeScript source
17
+ * @returns {string} source with non-code text masked
18
+ */
19
+ function maskJavaScriptNonCode (source) {
20
+ return maskJavaScriptSource(source, true)
21
+ }
22
+
23
+ /**
24
+ * Masks comments and optionally string literals in JavaScript-like source.
25
+ *
26
+ * @param {string} source JavaScript or TypeScript source
27
+ * @param {boolean} maskStrings whether string and template literal contents should be masked
28
+ * @returns {string} masked source
29
+ */
30
+ function maskJavaScriptSource (source, maskStrings) {
31
+ const characters = [...source]
32
+ let blockComment = false
33
+ let lineComment = false
34
+ let quote = ''
35
+
36
+ for (let index = 0; index < characters.length; index++) {
37
+ const character = characters[index]
38
+ const next = characters[index + 1]
39
+ if (lineComment) {
40
+ if (character === '\n') {
41
+ lineComment = false
42
+ } else {
43
+ characters[index] = ' '
44
+ }
45
+ continue
46
+ }
47
+ if (blockComment) {
48
+ if (character === '*' && next === '/') {
49
+ characters[index] = ' '
50
+ characters[++index] = ' '
51
+ blockComment = false
52
+ } else if (character !== '\r' && character !== '\n') {
53
+ characters[index] = ' '
54
+ }
55
+ continue
56
+ }
57
+ if (quote) {
58
+ if (maskStrings && character !== '\r' && character !== '\n') characters[index] = ' '
59
+ if (character.charCodeAt(0) === 92) {
60
+ if (maskStrings && index + 1 < characters.length &&
61
+ characters[index + 1] !== '\r' && characters[index + 1] !== '\n') {
62
+ characters[index + 1] = ' '
63
+ }
64
+ index++
65
+ } else if (character === quote) {
66
+ quote = ''
67
+ }
68
+ continue
69
+ }
70
+ if (character === '"' || character === '\'' || character === '`') {
71
+ quote = character
72
+ if (maskStrings) characters[index] = ' '
73
+ } else if (character === '/' && next === '/') {
74
+ characters[index] = ' '
75
+ characters[++index] = ' '
76
+ lineComment = true
77
+ } else if (character === '/' && next === '*') {
78
+ characters[index] = ' '
79
+ characters[++index] = ' '
80
+ blockComment = true
81
+ }
82
+ }
83
+
84
+ return characters.join('')
85
+ }
86
+
87
+ module.exports = { maskJavaScriptComments, maskJavaScriptNonCode }
@@ -1,5 +1,9 @@
1
1
  'use strict'
2
2
 
3
+ const cypressAdapter = require('./framework-adapters/cypress')
4
+ const cucumberAdapter = require('./framework-adapters/cucumber')
5
+ const playwrightAdapter = require('./framework-adapters/playwright')
6
+
3
7
  const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}${String.raw`\[[0-?]*[ -/]*[@-~]`}`, 'g')
4
8
 
5
9
  /**
@@ -11,16 +15,14 @@ const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}${String.raw`\[[0-?]*
11
15
  * @returns {number|null} observed count when a supported summary was found
12
16
  */
13
17
  function getObservedTestCount (framework, stdout = '', stderr = '') {
14
- const output = `${stdout}\n${stderr}`.replaceAll(ANSI_PATTERN, '')
18
+ const output = stripAnsi(`${stdout}\n${stderr}`)
19
+ if (framework === 'cucumber') return cucumberAdapter.getObservedTestCount(output)
20
+ if (framework === 'cypress') return cypressAdapter.getObservedTestCount(output)
21
+ if (framework === 'playwright') return playwrightAdapter.getObservedTestCount(output)
15
22
  if (framework === 'jest') return getJestObservedTestCount(output)
16
23
  if (framework === 'vitest') return getVitestObservedTestCount(output)
17
- if (framework === 'playwright') return getPlaywrightObservedTestCount(output)
18
24
 
19
- const totalPatterns = framework === 'node:test'
20
- ? [/^# tests\s+(\d+)\s*$/gim]
21
- : framework === 'cucumber'
22
- ? [/\b(\d+)\s+scenarios?\b/gi]
23
- : []
25
+ const totalPatterns = framework === 'node:test' ? [/^# tests\s+(\d+)\s*$/gim] : []
24
26
 
25
27
  for (const pattern of totalPatterns) {
26
28
  const count = getLastMatchCount(output, pattern)
@@ -38,20 +40,8 @@ function getObservedTestCount (framework, stdout = '', stderr = '') {
38
40
  return getLastMatchCount(output, /\b(\d+)\s+tests?\s+(?:passed|failed)\b/gi)
39
41
  }
40
42
 
41
- /**
42
- * Counts Playwright tests that completed instead of treating skipped tests as executions.
43
- *
44
- * @param {string} output test output without ANSI codes
45
- * @returns {number|null} executed test count
46
- */
47
- function getPlaywrightObservedTestCount (output) {
48
- const observed = sumLastMatchCounts(output, [
49
- /^\s*(\d+)\s+passed\b/gim,
50
- /^\s*(\d+)\s+failed\b/gim,
51
- /^\s*(\d+)\s+flaky\b/gim,
52
- ])
53
- if (observed !== null) return observed
54
- return /^\s*\d+\s+skipped\b/im.test(output) ? 0 : null
43
+ function stripAnsi (value) {
44
+ return String(value || '').replaceAll(ANSI_PATTERN, '')
55
45
  }
56
46
 
57
47
  /**
@@ -126,4 +116,4 @@ function sumLastMatchCounts (output, patterns) {
126
116
  return found ? count : null
127
117
  }
128
118
 
129
- module.exports = { getObservedTestCount }
119
+ module.exports = { getObservedTestCount, stripAnsi }
package/ext/tags.d.ts CHANGED
@@ -20,6 +20,7 @@ declare const tags: {
20
20
  HTTP_RESPONSE_HEADERS: 'http.response.headers'
21
21
  HTTP_USERAGENT: 'http.useragent',
22
22
  HTTP_CLIENT_IP: 'http.client_ip',
23
+ GRPC_STATUS_CODE: 'grpc.status.code'
23
24
  PATHWAY_HASH: 'pathway.hash'
24
25
  }
25
26
 
package/index.d.ts CHANGED
@@ -293,6 +293,7 @@ interface Plugins {
293
293
  "next": tracer.plugins.next;
294
294
  "nyc": tracer.plugins.nyc;
295
295
  "openai": tracer.plugins.openai;
296
+ "openai-agents": tracer.plugins.openai_agents;
296
297
  "opensearch": tracer.plugins.opensearch;
297
298
  "oracledb": tracer.plugins.oracledb;
298
299
  "playwright": tracer.plugins.playwright;
@@ -3138,6 +3139,12 @@ declare namespace tracer {
3138
3139
  */
3139
3140
  interface openai extends Instrumentation {}
3140
3141
 
3142
+ /**
3143
+ * This plugin automatically instruments the
3144
+ * [@openai/agents](https://www.npmjs.com/package/@openai/agents) library.
3145
+ */
3146
+ interface openai_agents extends Instrumentation {}
3147
+
3141
3148
  /**
3142
3149
  * This plugin automatically instruments the
3143
3150
  * [opensearch](https://github.com/opensearch-project/opensearch-js) module.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.117.0",
3
+ "version": "5.118.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -198,7 +198,7 @@
198
198
  "@types/mocha": "^10.0.10",
199
199
  "@types/node": "^18.19.106",
200
200
  "@types/sinon": "^22.0.0",
201
- "@vercel/nft": "^0.29.4",
201
+ "@vercel/nft": "^1.10.2",
202
202
  "axios": "^1.18.1",
203
203
  "benchmark": "^2.1.4",
204
204
  "body-parser": "^2.3.0",
@@ -21,6 +21,8 @@ module.exports = {
21
21
  '@apollo/gateway': () => require('../apollo'),
22
22
  '@langchain/langgraph': { esmFirst: true, fn: () => require('../langgraph') },
23
23
  '@modelcontextprotocol/sdk': { esmFirst: true, fn: () => require('../modelcontextprotocol-sdk') },
24
+ '@openai/agents': () => require('../openai-agents'),
25
+ '@openai/agents-openai': () => require('../openai-agents'),
24
26
  'apollo-server-core': () => require('../apollo-server-core'),
25
27
  '@aws-sdk/smithy-client': () => require('../aws-sdk'),
26
28
  '@aws/durable-execution-sdk-js': () => require('../aws-durable-execution-sdk-js'),
@@ -10,6 +10,7 @@ module.exports = [
10
10
  ...require('./langgraph'),
11
11
  ...require('./mercurius'),
12
12
  ...require('./modelcontextprotocol-sdk'),
13
+ ...require('./openai-agents'),
13
14
  ...require('./playwright'),
14
15
  ...require('./aws-durable-execution-sdk-js'),
15
16
  ]
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ const moduleName = '@openai/agents-openai'
4
+ const versionRange = '>=0.7.0'
5
+ const functionQuery = {
6
+ methodName: 'constructor',
7
+ className: 'OpenAIChatCompletionsModel',
8
+ kind: 'Sync',
9
+ }
10
+ const channelName = 'OpenAIChatCompletionsModel_constructor'
11
+
12
+ module.exports = [
13
+ {
14
+ module: {
15
+ name: moduleName,
16
+ versionRange,
17
+ filePath: 'dist/openaiChatCompletionsModel.js',
18
+ },
19
+ functionQuery,
20
+ channelName,
21
+ },
22
+ {
23
+ module: {
24
+ name: moduleName,
25
+ versionRange,
26
+ filePath: 'dist/openaiChatCompletionsModel.mjs',
27
+ },
28
+ functionQuery,
29
+ channelName,
30
+ },
31
+ ]
@@ -0,0 +1,159 @@
1
+ 'use strict'
2
+
3
+ const { channel, tracingChannel } = require('dc-polyfill')
4
+ const shimmer = require('../../datadog-shimmer')
5
+ const { addHook, getHooks } = require('./helpers/instrument')
6
+
7
+ // `WeakSet` keyed by module exports — replaces the underscored
8
+ // `mod._datadogPatched` flag while keeping dedupe semantics. Mods are kept
9
+ // alive by `require.cache` anyway, so this doesn't add lifetime to anything.
10
+ const patchedMods = new WeakSet()
11
+ const modelBaseURLs = new WeakMap()
12
+ const chatCompletionsModelConstructorCh =
13
+ tracingChannel('orchestrion:@openai/agents-openai:OpenAIChatCompletionsModel_constructor')
14
+
15
+ /**
16
+ * Capture the client base URL before the SDK stores the client in a #private field.
17
+ *
18
+ * @param {{ arguments?: [{ baseURL?: string }], self?: object }} ctx
19
+ */
20
+ function captureChatCompletionsModelBaseURL (ctx) {
21
+ const baseURL = ctx.arguments?.[0]?.baseURL
22
+ if (ctx.self && typeof baseURL === 'string') modelBaseURLs.set(ctx.self, baseURL)
23
+ }
24
+
25
+ chatCompletionsModelConstructorCh.end.subscribe(captureChatCompletionsModelBaseURL)
26
+
27
+ for (const hook of getHooks('@openai/agents-openai')) {
28
+ addHook(hook, moduleExports => moduleExports)
29
+ }
30
+
31
+ // Plugin subscribes to this and registers its TracingProcessor when
32
+ // `@openai/agents` loads. Publishing from here keeps this file free of
33
+ // any cross-package import from the plugin.
34
+ const agentsCoreLoadedCh = channel('apm:openai-agents:agents-core:loaded')
35
+
36
+ // Plugin uses addBind on this channel so that legacyStorage.run(store, fn) wraps
37
+ // the model call — including async iterator advancement for streaming responses.
38
+ // This ensures the active dd-trace span is visible to the openai plugin when it
39
+ // creates its openai.request span, correctly parenting it under the agent span.
40
+ const modelStartCh = channel('apm:openai-agents:model:start')
41
+
42
+ // Tool.invoke runs inside agents-core's public function-span context. Bind the
43
+ // corresponding dd-trace tool span around that public invocation boundary so
44
+ // spans created by user tool code inherit it.
45
+ const toolStartCh = channel('apm:openai-agents:tool:start')
46
+
47
+ // Reference to the loaded @openai/agents module, captured in the first hook
48
+ // so that wrapResponseMethod can call getCurrentSpan() without an additional
49
+ // require (and without triggering n/no-missing-require on agents-core internals).
50
+ let agentsMod
51
+
52
+ // @openai/agents >=0.8.0 moved addTraceProcessor / getCurrentSpan out of the
53
+ // top-level re-exports. The new public surface uses getGlobalTraceProvider():
54
+ // provider.registerProcessor(processor) (replaces addTraceProcessor)
55
+ // provider.getCurrentSpan() (replaces getCurrentSpan)
56
+ // Both APIs are tried so this file works across the full supported version range.
57
+ // The plugin subscriber (index.js) handles processor registration via the channel.
58
+ function getCurrentSpan () {
59
+ if (typeof agentsMod?.getCurrentSpan === 'function') {
60
+ return agentsMod.getCurrentSpan()
61
+ }
62
+ if (typeof agentsMod?.getGlobalTraceProvider === 'function') {
63
+ return agentsMod.getGlobalTraceProvider().getCurrentSpan()
64
+ }
65
+ }
66
+
67
+ function getCurrentSpanId () {
68
+ return getCurrentSpan()?.spanId
69
+ }
70
+
71
+ addHook({ name: '@openai/agents', versions: ['>=0.7.0'] }, (mod) => {
72
+ if (patchedMods.has(mod)) return mod
73
+ if (typeof mod?.addTraceProcessor !== 'function' && typeof mod?.getGlobalTraceProvider !== 'function') return mod
74
+ patchedMods.add(mod)
75
+ agentsMod = mod
76
+ if (typeof mod.tool === 'function') {
77
+ shimmer.wrap(mod, 'tool', wrapToolFactory, { replaceGetter: true })
78
+ }
79
+ agentsCoreLoadedCh.publish({ mod })
80
+ return mod
81
+ })
82
+
83
+ function wrapToolFactory (original) {
84
+ return function (...args) {
85
+ const tool = original.apply(this, args)
86
+ if (typeof tool?.invoke === 'function') {
87
+ shimmer.wrap(tool, 'invoke', wrapToolInvoke)
88
+ }
89
+ return tool
90
+ }
91
+ }
92
+
93
+ function wrapToolInvoke (original) {
94
+ return function (...args) {
95
+ const agentsCoreSpan = getCurrentSpan()
96
+ return toolStartCh.runStores({ agentsCoreSpan }, () => original.apply(this, args))
97
+ }
98
+ }
99
+
100
+ function wrapResponseMethod (original) {
101
+ return function (...args) {
102
+ const agentsCoreSpanId = getCurrentSpanId()
103
+ const baseURL = getClientBaseURL(this)
104
+ return modelStartCh.runStores({ agentsCoreSpanId, baseURL }, () => original.apply(this, args))
105
+ }
106
+ }
107
+
108
+ function wrapStreamedResponseMethod (original) {
109
+ return function (...args) {
110
+ const agentsCoreSpanId = getCurrentSpanId()
111
+ const baseURL = getClientBaseURL(this)
112
+ const context = { agentsCoreSpanId, baseURL }
113
+ const iterator = modelStartCh.runStores(context, () => original.apply(this, args))
114
+ return wrapAsyncIterator(iterator, context)
115
+ }
116
+ }
117
+
118
+ function getClientBaseURL (model) {
119
+ return model?.client?.baseURL ?? model?._client?.baseURL ?? modelBaseURLs.get(model)
120
+ }
121
+
122
+ function wrapAsyncIterator (iterator, context) {
123
+ if (!iterator || typeof iterator !== 'object') return iterator
124
+
125
+ return {
126
+ next () {
127
+ return modelStartCh.runStores(context, () => iterator.next.apply(iterator, arguments))
128
+ },
129
+ throw () {
130
+ if (typeof iterator.throw !== 'function') return Promise.reject(arguments[0])
131
+ return modelStartCh.runStores(context, () => iterator.throw.apply(iterator, arguments))
132
+ },
133
+ return () {
134
+ if (typeof iterator.return !== 'function') return Promise.resolve({ done: true, value: arguments[0] })
135
+ return modelStartCh.runStores(context, () => iterator.return.apply(iterator, arguments))
136
+ },
137
+ [Symbol.asyncIterator] () {
138
+ return this
139
+ },
140
+ }
141
+ }
142
+
143
+ addHook({ name: '@openai/agents-openai', versions: ['>=0.7.0'] }, (mod) => {
144
+ if (patchedMods.has(mod)) return mod
145
+ const responseProto = mod?.OpenAIResponsesModel?.prototype
146
+ const chatCompletionsProto = mod?.OpenAIChatCompletionsModel?.prototype
147
+ if (!responseProto && !chatCompletionsProto) return mod
148
+
149
+ patchedMods.add(mod)
150
+ for (const proto of [responseProto, chatCompletionsProto]) {
151
+ if (typeof proto?.getResponse === 'function') {
152
+ shimmer.wrap(proto, 'getResponse', wrapResponseMethod)
153
+ }
154
+ if (typeof proto?.getStreamedResponse === 'function') {
155
+ shimmer.wrap(proto, 'getStreamedResponse', wrapStreamedResponseMethod)
156
+ }
157
+ }
158
+ return mod
159
+ })
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../datadog-core')
4
+ const Plugin = require('../../dd-trace/src/plugins/plugin')
5
+ const { MODEL_BASE_URL_STORE_KEY, OpenAIAgentsIntegration } = require('./integration')
6
+ const { DDOpenAIAgentsProcessor } = require('./processor')
7
+
8
+ const legacyStorage = storage('legacy')
9
+
10
+ /**
11
+ * Drives the openai-agents integration through agents-core's
12
+ * `TracingProcessor` interface. The instrumentation hook publishes the
13
+ * loaded `@openai/agents` module on a channel; this plugin subscribes
14
+ * during its constructor (which runs synchronously between `loadChannel`'s
15
+ * publish and the addHook callback) and registers the processor.
16
+ *
17
+ * The integration's `enabled` flag follows this plugin's configure()
18
+ * lifecycle. Each loaded version of the agents package replaces all processors
19
+ * via setTraceProcessors() on module load, so the plugin re-registers a
20
+ * fresh DDOpenAIAgentsProcessor for each module version that fires the channel.
21
+ */
22
+ class OpenaiAgentsPlugin extends Plugin {
23
+ static id = 'openai-agents'
24
+
25
+ #integration
26
+
27
+ constructor (tracer, tracerConfig) {
28
+ super(tracer, tracerConfig)
29
+ this.#integration = new OpenAIAgentsIntegration({
30
+ tracer: this.tracer,
31
+ config: tracerConfig,
32
+ })
33
+
34
+ // Register a new processor each time @openai/agents fires the channel.
35
+ // Each module version calls setTraceProcessors() on load (which replaces
36
+ // all processors), so we must re-register after every new version loads.
37
+ // The instrumentation's patchedMods WeakSet ensures each module instance
38
+ // fires the channel exactly once, so no duplicates accumulate.
39
+ this.addSub('apm:openai-agents:agents-core:loaded', ({ mod }) => {
40
+ const processor = new DDOpenAIAgentsProcessor(() => this.#integration)
41
+ if (typeof mod?.addTraceProcessor === 'function') {
42
+ mod.addTraceProcessor(processor)
43
+ } else {
44
+ mod.getGlobalTraceProvider().registerProcessor(processor)
45
+ }
46
+ })
47
+
48
+ // Activate the current agent's dd-trace span in legacyStorage for the
49
+ // duration of model response calls and stream iterator advancement. This
50
+ // makes the openai plugin's shimmer see the correct parent when it creates its
51
+ // openai.request span, so all spans land in the same trace.
52
+ this.addBind('apm:openai-agents:model:start', ({ agentsCoreSpanId, baseURL }) => {
53
+ const store = legacyStorage.getStore()
54
+ if (!this.#integration.enabled || !agentsCoreSpanId) return store
55
+ const ddSpan = this.#integration.getDDSpan(agentsCoreSpanId)
56
+ if (!ddSpan) return store
57
+ return { ...store, [MODEL_BASE_URL_STORE_KEY]: baseURL, span: ddSpan }
58
+ })
59
+
60
+ this.addBind('apm:openai-agents:tool:start', ({ agentsCoreSpan }) => {
61
+ const store = legacyStorage.getStore()
62
+ if (!this.#integration.enabled || !agentsCoreSpan) return store
63
+ const ddSpan = this.#integration.getOrStartToolSpan(agentsCoreSpan)
64
+ return ddSpan ? { ...store, span: ddSpan } : store
65
+ })
66
+ }
67
+
68
+ configure (config) {
69
+ super.configure(config)
70
+ this.#integration.configure(config)
71
+ }
72
+ }
73
+
74
+ module.exports = OpenaiAgentsPlugin