From 06ff80448ea9fb0b8050a6e5c08b86db1a612303 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Thu, 23 Jul 2026 17:29:11 +0200 Subject: [PATCH 1/9] fix(tracing): make peer service finalization idempotent (#9364) * test: stop peer service checks racing operation spans The shared helper started the operation before registering its trace expectation and only inspected the first span of each payload. MongoDB connection traffic could therefore consume the payload containing the expected operation span, leaving the test to time out. * test: bind peer service checks to their operation spans Delayed setup traces can carry the same peer-service tags as the operation under test, allowing the shared matcher to pass without observing that operation. Run each generator under a unique parent and match its child span instead. Generator throws and invalid return values can bypass assertion cleanup. Keep the whole operation inside the cleanup boundary so the original failure is not replaced by a leaked-expectation teardown error. * test: match peer service spans by trace Peer-service spans can be nested below integration spans, so requiring a direct parent excluded valid operations. Match the generated trace to keep setup traffic isolated without constraining span depth. * test: stop correlation spans delaying trace export Keeping the correlation parent open until the operation settled delayed trace export. Integrations that publish duplicate finish notifications could then recompute `_dd.peer.service.source` before the trace was sent. * fix(tracing): make peer service finalization idempotent Repeated finish notifications can reach an outbound span before an ancestor lets the trace flush. The second pass treats the computed peer service as preconfigured and rewrites its source to `peer.service`; an existing source now marks finalization. The peer-service assertion keeps its correlation parent open through completion and matches the integration component, so delayed setup traces and spans from another integration cannot satisfy it. * test(mongodb): target bulkWrite peer service parent --- .../test/mongodb.spec.js | 9 +- .../test/index.spec.js | 3 +- packages/dd-trace/src/plugins/outbound.js | 5 +- .../dd-trace/test/plugins/outbound.spec.js | 38 ++++++++ packages/dd-trace/test/setup/mocha.js | 94 ++++++++++++++----- 5 files changed, 122 insertions(+), 27 deletions(-) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index bbaf4eed2e..e570dbe90b 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -109,7 +109,8 @@ describe('Plugin', () => { 'mongodb-core', (done) => collection.insertOne({ a: 1 }, {}, done), 'test', - 'peer.service' + 'peer.service', + { component: 'mongodb' } ) // The bulkWrite span is opened with `db.name` set, so it flows through the same @@ -120,7 +121,11 @@ describe('Plugin', () => { () => collection.bulkWrite([{ insertOne: { document: { a: 1 } } }]), 'test', 'peer.service', - { desc: 'with bulkWrite' } + { + component: 'mongodb', + desc: 'with bulkWrite', + resource: () => `bulkWrite test.${collectionName}`, + } ) it('should do automatic instrumentation', done => { diff --git a/packages/datadog-plugin-mongoose/test/index.spec.js b/packages/datadog-plugin-mongoose/test/index.spec.js index 2d63853655..0753039d68 100644 --- a/packages/datadog-plugin-mongoose/test/index.spec.js +++ b/packages/datadog-plugin-mongoose/test/index.spec.js @@ -73,7 +73,8 @@ describe('Plugin', () => { return new PeerCat({ name: 'PeerCat' }).save() }, () => dbName, - 'peer.service') + 'peer.service', + { component: 'mongodb' }) it('should propagate context with write operations', () => { const Cat = mongoose.model('Cat1', { name: String }) diff --git a/packages/dd-trace/src/plugins/outbound.js b/packages/dd-trace/src/plugins/outbound.js index 05ae4bcae3..635a07cd51 100644 --- a/packages/dd-trace/src/plugins/outbound.js +++ b/packages/dd-trace/src/plugins/outbound.js @@ -125,7 +125,10 @@ class OutboundPlugin extends TracingPlugin { */ tagPeerService (span) { if (this._tracerConfig.spanComputePeerService) { - const peerData = this.getPeerService(span.context().getTags()) + const tags = span.context().getTags() + if (tags[PEER_SERVICE_SOURCE_KEY] !== undefined) return + + const peerData = this.getPeerService(tags) if (peerData !== undefined) { span.addTags(this.getPeerServiceRemap(peerData)) } diff --git a/packages/dd-trace/test/plugins/outbound.spec.js b/packages/dd-trace/test/plugins/outbound.spec.js index 2edd27f354..1ef24c107b 100644 --- a/packages/dd-trace/test/plugins/outbound.spec.js +++ b/packages/dd-trace/test/plugins/outbound.spec.js @@ -50,6 +50,44 @@ describe('OuboundPlugin', () => { sinon.assert.notCalled(getRemapStub) }) + it('should not recompute peer service after its source was set', () => { + computePeerServiceStub.value({ spanComputePeerService: true }) + const tags = { + 'peer.service': 'mypeerservice', + '_dd.peer.service.source': 'out.host', + } + instance.tagPeerService({ context: () => ({ getTags: () => tags }) }) + + sinon.assert.notCalled(getPeerServiceStub) + sinon.assert.notCalled(getRemapStub) + }) + + it('should not recompute peer service when only its source was set', () => { + computePeerServiceStub.value({ spanComputePeerService: true }) + const tags = { + '_dd.peer.service.source': 'out.host', + } + instance.tagPeerService({ context: () => ({ getTags: () => tags }) }) + + sinon.assert.notCalled(getPeerServiceStub) + sinon.assert.notCalled(getRemapStub) + }) + + it('should compute peer service when only its value was set', () => { + computePeerServiceStub.value({ spanComputePeerService: true }) + getPeerServiceStub.returns({ + 'peer.service': 'mypeerservice', + '_dd.peer.service.source': 'peer.service', + }) + const tags = { + 'peer.service': 'mypeerservice', + } + instance.tagPeerService({ context: () => ({ getTags: () => tags }), addTags: () => {} }) + + sinon.assert.called(getPeerServiceStub) + sinon.assert.called(getRemapStub) + }) + it('should do nothing when disabled', () => { computePeerServiceStub.value({ spanComputePeerService: false }) instance.tagPeerService({ context: () => ({ _tags: {}, getTags () { return this._tags } }), addTags: () => {} }) diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 2b7af5263f..f7888d5f95 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -182,6 +182,14 @@ function withNamingSchema ( }) } +/** + * @param {() => import('../../src/proxy')} tracer + * @param {string} pluginName + * @param {((callback: (error?: Error) => void) => unknown) | (() => Promise)} spanGenerationFn + * @param {string | (() => string)} service + * @param {string} serviceSource + * @param {{ component?: string, desc?: string, resource?: string | (() => string) }} [opts] + */ function withPeerService (tracer, pluginName, spanGenerationFn, service, serviceSource, opts = {}) { describe('peer service computation' + (opts.desc ? ` ${opts.desc}` : ''), function () { this.timeout(10000) @@ -199,32 +207,72 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service }) it('should compute peer service', async () => { - const useCallback = spanGenerationFn.length === 1 - const spanGenerationPromise = useCallback - ? new Promise(/** @type {() => void} */ (resolve, reject) => { - const result = spanGenerationFn((err) => err ? reject(err) : resolve()) - // Some callback based methods are a mixture of callback and promise, - // depending on the module version. Await the promises as well. - if (util.types.isPromise(result)) { - result.then?.(resolve, reject) + const currentTracer = global._ddtrace + const parentSpan = currentTracer.startSpan('peer-service.test') + const traceId = BigInt(parentSpan.context().toTraceId()) + const component = opts.component ?? pluginName + let traceAssertion + + /** + * @param {Array }>>} traces + */ + function assertPeerServiceSpan (traces) { + const expectedService = typeof service === 'function' ? service() : service + const expectedResource = typeof opts.resource === 'function' ? opts.resource() : opts.resource + + for (const trace of traces) { + for (const span of trace) { + if ( + span.trace_id === traceId && + span.meta.component === component && + (expectedResource === undefined || span.resource === expectedResource) && + span.meta['peer.service'] === expectedService && + span.meta['_dd.peer.service.source'] === serviceSource + ) { + return + } } - }) - : spanGenerationFn() + } - assert.strictEqual( - typeof spanGenerationPromise?.then, 'function', - 'spanGenerationFn should return a promise in case no callback is defined. Received: ' + - util.inspect(spanGenerationPromise, { depth: 1 }), - ) + assert.fail( + `No ${component} span in trace ${traceId} had peer.service=${expectedService} and ` + + `_dd.peer.service.source=${serviceSource}.\n\nCandidate Traces:\n${util.inspect(traces, { depth: null })}` + ) + } - await Promise.all([ - getAgent().assertSomeTraces(traces => { - const span = traces[0][0] - assert.strictEqual(span.meta['peer.service'], typeof service === 'function' ? service() : service) - assert.strictEqual(span.meta['_dd.peer.service.source'], serviceSource) - }), - spanGenerationPromise, - ]) + try { + traceAssertion = getAgent().assertSomeTraces(assertPeerServiceSpan, { timeoutMs: 9000 }) + const useCallback = spanGenerationFn.length === 1 + const spanGenerationPromise = currentTracer.scope().activate(parentSpan, () => { + return useCallback + ? new Promise(/** @type {() => void} */ (resolve, reject) => { + const result = spanGenerationFn((error) => error ? reject(error) : resolve()) + // Some callback based methods are a mixture of callback and promise, + // depending on the module version. Await the promises as well. + if (util.types.isPromise(result)) { + result.then?.(resolve, reject) + } + }) + : spanGenerationFn() + }) + + assert.strictEqual( + typeof spanGenerationPromise?.then, 'function', + 'spanGenerationFn should return a promise in case no callback is defined. Received: ' + + util.inspect(spanGenerationPromise, { depth: 1 }), + ) + + await Promise.all([ + traceAssertion, + (async () => { + await spanGenerationPromise + parentSpan.finish() + })(), + ]) + } finally { + parentSpan.finish() + traceAssertion?.cancel() + } }) }) } From 287316b03b260ff51c5d513a0dbfebf210921137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:12:12 +0200 Subject: [PATCH 2/9] chore(deps): bump the gh-actions-packages group across 6 directories with 5 updates (#9478) * chore(deps): bump the gh-actions-packages group across 6 directories with 5 updates Bumps the gh-actions-packages group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action). Bumps the gh-actions-packages group with 1 update in the /.github/actions/datadog-ci directory: [actions/setup-node](https://github.com/actions/setup-node). Bumps the gh-actions-packages group with 1 update in the /.github/actions/node/setup directory: [actions/setup-node](https://github.com/actions/setup-node). Bumps the gh-actions-packages group with 1 update in the /.github/actions/testagent/logs directory: [actions/checkout](https://github.com/actions/checkout). Bumps the gh-actions-packages group with 1 update in the /.github/actions/testagent/start directory: [actions/checkout](https://github.com/actions/checkout). Bumps the gh-actions-packages group with 4 updates in the /.github/workflows directory: [actions/checkout](https://github.com/actions/checkout), [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action). Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `github/codeql-action/init` from 4.37.0 to 4.37.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a) Updates `github/codeql-action/analyze` from 4.37.0 to 4.37.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a) Updates `slackapi/slack-github-action` from 3.0.4 to 4.0.0 - [Release notes](https://github.com/slackapi/slack-github-action/releases) - [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/slackapi/slack-github-action/compare/fc46ded2fc4d7f11bfa62864526f920cf1a1167d...dcb1066f776dd043e64d0e8ba94ca15cc7e1875d) Updates `actions/setup-node` from 6.4.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020) Updates `actions/setup-node` from 6.4.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020) Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `github/codeql-action/init` from 4.37.0 to 4.37.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a) Updates `github/codeql-action/analyze` from 4.37.0 to 4.37.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/99df26d4f13ea111d4ec1a7dddef6063f76b97e9...7188fc363630916deb702c7fdcf4e481b751f97a) Updates `slackapi/slack-github-action` from 3.0.4 to 4.0.0 - [Release notes](https://github.com/slackapi/slack-github-action/releases) - [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/slackapi/slack-github-action/compare/fc46ded2fc4d7f11bfa62864526f920cf1a1167d...dcb1066f776dd043e64d0e8ba94ca15cc7e1875d) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: github/codeql-action/init dependency-version: 4.37.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: github/codeql-action/analyze dependency-version: 4.37.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: slackapi/slack-github-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: gh-actions-packages - dependency-name: actions/setup-node dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: gh-actions-packages - dependency-name: actions/setup-node dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: gh-actions-packages - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: github/codeql-action/init dependency-version: 4.37.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: github/codeql-action/analyze dependency-version: 4.37.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gh-actions-packages - dependency-name: slackapi/slack-github-action dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: gh-actions-packages ... Signed-off-by: dependabot[bot] * attempt to fix jobs --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Thomas Hunter II --- .github/actions/datadog-ci/action.yml | 2 +- .github/actions/node/action.yml | 6 + .github/actions/node/setup/action.yml | 12 +- .github/actions/testagent/logs/action.yml | 2 +- .github/actions/testagent/start/action.yml | 2 +- .github/workflows/aiguard.yml | 8 +- .github/workflows/all-green.yml | 2 +- .github/workflows/apm-capabilities.yml | 6 +- .github/workflows/apm-integrations.yml | 158 +++++++++--------- .github/workflows/appsec.yml | 40 ++--- .github/workflows/audit.yml | 2 +- .github/workflows/codeql-analysis.yml | 6 +- .github/workflows/debugger.yml | 2 +- .github/workflows/electron.yml | 4 +- .github/workflows/eslint-rules.yml | 2 +- .github/workflows/flakiness.yml | 4 +- .github/workflows/instrumentation.yml | 96 +++++------ .github/workflows/llmobs.yml | 22 +-- .github/workflows/openfeature.yml | 8 +- .github/workflows/platform.yml | 22 +-- .github/workflows/profiling.yml | 6 +- .github/workflows/project.yml | 22 +-- .github/workflows/release-proposal.yml | 6 +- .github/workflows/release-validate.yml | 2 +- .github/workflows/release.yml | 22 ++- .github/workflows/serverless.yml | 18 +- .github/workflows/system-tests.yml | 2 +- .github/workflows/test-optimization.yml | 24 +-- .../workflows/update-3rdparty-licenses.yml | 4 +- 29 files changed, 267 insertions(+), 245 deletions(-) diff --git a/.github/actions/datadog-ci/action.yml b/.github/actions/datadog-ci/action.yml index 13924ed071..f5a5b01c11 100644 --- a/.github/actions/datadog-ci/action.yml +++ b/.github/actions/datadog-ci/action.yml @@ -4,7 +4,7 @@ description: Install @datadog/datadog-ci from npm and add it to PATH. runs: using: composite steps: - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '20' diff --git a/.github/actions/node/action.yml b/.github/actions/node/action.yml index c1513f2c5f..355690ba13 100644 --- a/.github/actions/node/action.yml +++ b/.github/actions/node/action.yml @@ -5,6 +5,10 @@ inputs: description: "Version identifier of the version to use." required: false default: 'latest' + registry-url: + description: "npm registry URL to configure for publishing. Leave unset for jobs that don't publish." + required: false + default: '' runs: using: composite steps: @@ -15,6 +19,7 @@ runs: continue-on-error: true with: version: ${{ inputs.version }} + registry-url: ${{ inputs.registry-url }} - if: steps.attempt.outcome == 'failure' shell: bash run: sleep 60 @@ -23,3 +28,4 @@ runs: uses: ./.github/actions/node/setup with: version: ${{ inputs.version }} + registry-url: ${{ inputs.registry-url }} diff --git a/.github/actions/node/setup/action.yml b/.github/actions/node/setup/action.yml index ef54965603..490dd98173 100644 --- a/.github/actions/node/setup/action.yml +++ b/.github/actions/node/setup/action.yml @@ -5,6 +5,10 @@ inputs: description: "Version identifier of the version to use." required: false default: 'latest' + registry-url: + description: "npm registry URL to configure for publishing. Leave unset for jobs that don't publish." + required: false + default: '' runs: using: composite steps: @@ -41,10 +45,14 @@ runs: esac echo "version=$version" >> "$GITHUB_OUTPUT" - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + # registry-url is left empty for jobs that don't publish: setup-node only writes an + # .npmrc auth placeholder when it's set, and since v7.0.0 no longer backstops that + # placeholder with a dummy NODE_AUTH_TOKEN, so an always-on registry-url broke Yarn + # Classic (it hard-errors on an unresolved ${NODE_AUTH_TOKEN} env reference in .npmrc). + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ steps.node-version.outputs.version }} - registry-url: ${{ inputs.registry-url || 'https://registry.npmjs.org' }} + registry-url: ${{ inputs.registry-url }} # The shipped package.json pins engines.node to the supported runtime range, but CI keeps # running the full suite on Node 18/20. Widen the field to >=18 for this checkout so the diff --git a/.github/actions/testagent/logs/action.yml b/.github/actions/testagent/logs/action.yml index 91a24f4aae..6434afb021 100644 --- a/.github/actions/testagent/logs/action.yml +++ b/.github/actions/testagent/logs/action.yml @@ -10,7 +10,7 @@ inputs: runs: using: composite steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Preserve untracked artifacts (coverage/, .nyc_output/, node_modules/) produced by earlier # test steps. Without this, the default `git clean -ffdx` wipes them before subsequent diff --git a/.github/actions/testagent/start/action.yml b/.github/actions/testagent/start/action.yml index 3d42d42d76..b53bb2de5b 100644 --- a/.github/actions/testagent/start/action.yml +++ b/.github/actions/testagent/start/action.yml @@ -3,7 +3,7 @@ description: "Starts the APM Test Agent image with environment." runs: using: composite steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: docker compose up -d testagent || docker compose up -d testagent shell: bash - name: Wait for test agent to be ready diff --git a/.github/workflows/aiguard.yml b/.github/workflows/aiguard.yml index 8e7eadfa86..3232e1331f 100644 --- a/.github/workflows/aiguard.yml +++ b/.github/workflows/aiguard.yml @@ -18,7 +18,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:aiguard:ci @@ -36,7 +36,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:aiguard:ci @@ -60,7 +60,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: 24.14.1 # TODO: remove pin when https://github.com/nodejs/node/issues/62991 is fixed @@ -86,7 +86,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} diff --git a/.github/workflows/all-green.yml b/.github/workflows/all-green.yml index 303e6bd174..83b8e3a575 100644 --- a/.github/workflows/all-green.yml +++ b/.github/workflows/all-green.yml @@ -22,7 +22,7 @@ jobs: with: scope: DataDog/dd-trace-js policy: all-green - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout-cone-mode: false sparse-checkout: | diff --git a/.github/workflows/apm-capabilities.yml b/.github/workflows/apm-capabilities.yml index b1f3016d62..d30a334d27 100644 --- a/.github/workflows/apm-capabilities.yml +++ b/.github/workflows/apm-capabilities.yml @@ -26,7 +26,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:trace:core:ci @@ -47,7 +47,7 @@ jobs: matrix: node-version: [oldest, maintenance, active, latest] steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -66,7 +66,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: 24.14.1 # TODO: remove pin when https://github.com/nodejs/node/issues/62991 is fixed diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index d27e767385..4ad0daa44d 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -72,7 +72,7 @@ jobs: SERVICES: aerospike PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -106,7 +106,7 @@ jobs: SERVICES: qpid DD_DATA_STREAMS_ENABLED: true steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream amqplib: @@ -122,7 +122,7 @@ jobs: PLUGINS: amqplib SERVICES: rabbitmq steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream apollo: @@ -132,7 +132,7 @@ jobs: env: PLUGINS: apollo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream avsc: @@ -143,7 +143,7 @@ jobs: PLUGINS: avsc DD_DATA_STREAMS_ENABLED: true steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream axios: @@ -153,7 +153,7 @@ jobs: env: PLUGINS: axios steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/upstream # `plugins/upstream` only runs `test:plugins:upstream`, which is a non-glob suite runner; # the in-tree `test/integration-test/*.spec.js` files would otherwise have no CI invocation @@ -167,7 +167,7 @@ jobs: env: PLUGINS: body-parser steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/upstream - uses: ./.github/actions/plugins/integration-test @@ -184,7 +184,7 @@ jobs: PLUGINS: bullmq SERVICES: redis steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test bunyan: @@ -194,7 +194,7 @@ jobs: env: PLUGINS: bunyan steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream cassandra: @@ -210,7 +210,7 @@ jobs: PLUGINS: cassandra-driver SERVICES: cassandra steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test child_process: @@ -220,7 +220,7 @@ jobs: env: PLUGINS: child_process steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -245,7 +245,7 @@ jobs: env: PLUGINS: crypto steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test confluentinc-kafka-javascript: @@ -290,7 +290,7 @@ jobs: SERVICES: kafka PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -311,7 +311,7 @@ jobs: env: PLUGINS: cookie-parser steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test cookie: @@ -321,7 +321,7 @@ jobs: env: PLUGINS: cookie steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test couchbase: @@ -350,7 +350,7 @@ jobs: PACKAGE_VERSION_RANGE: ${{ matrix.range }} DD_INJECT_FORCE: "true" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -372,7 +372,7 @@ jobs: env: PLUGINS: connect steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream dns: @@ -382,7 +382,7 @@ jobs: env: PLUGINS: dns steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -415,7 +415,7 @@ jobs: PLUGINS: elasticsearch SERVICES: elasticsearch steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -434,7 +434,7 @@ jobs: env: PLUGINS: electron steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node with: @@ -479,7 +479,7 @@ jobs: env: PLUGINS: express steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -494,7 +494,7 @@ jobs: env: PLUGINS: express-mongo-sanitize steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test express-session: @@ -504,7 +504,7 @@ jobs: env: PLUGINS: express-session steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test fastify: @@ -514,7 +514,7 @@ jobs: env: PLUGINS: fastify steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test fetch: @@ -524,7 +524,7 @@ jobs: env: PLUGINS: fetch steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test fs: @@ -534,7 +534,7 @@ jobs: env: PLUGINS: fs steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test generic-pool: @@ -544,7 +544,7 @@ jobs: env: PLUGINS: generic-pool steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test graphql: @@ -554,7 +554,7 @@ jobs: env: PLUGINS: graphql steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream grpc: @@ -564,7 +564,7 @@ jobs: env: PLUGINS: grpc steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test handlebars: @@ -574,7 +574,7 @@ jobs: env: PLUGINS: handlebars steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test hapi: @@ -584,7 +584,7 @@ jobs: env: PLUGINS: hapi steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test hono: @@ -594,7 +594,7 @@ jobs: env: PLUGINS: hono steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test http: @@ -608,7 +608,7 @@ jobs: env: PLUGINS: http steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -629,7 +629,7 @@ jobs: env: PLUGINS: http2 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -682,7 +682,7 @@ jobs: PLUGINS: kafkajs SERVICES: kafka steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -703,7 +703,7 @@ jobs: env: PLUGINS: knex steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test koa: @@ -713,7 +713,7 @@ jobs: env: PLUGINS: koa steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream ldapjs: @@ -723,7 +723,7 @@ jobs: env: PLUGINS: ldapjs steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test light-my-request: @@ -733,7 +733,7 @@ jobs: env: PLUGINS: "light-my-request" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test limitd-client: @@ -753,7 +753,7 @@ jobs: PLUGINS: limitd-client SERVICES: limitd steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test lodash: @@ -763,7 +763,7 @@ jobs: env: PLUGINS: lodash steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test mariadb: @@ -782,7 +782,7 @@ jobs: PLUGINS: mariadb SERVICES: mariadb steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test memcached: @@ -798,7 +798,7 @@ jobs: PLUGINS: memcached SERVICES: memcached steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test mercurius: @@ -808,7 +808,7 @@ jobs: env: PLUGINS: mercurius steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test microgateway-core: @@ -818,7 +818,7 @@ jobs: env: PLUGINS: microgateway-core steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test moleculer: @@ -828,7 +828,7 @@ jobs: env: PLUGINS: moleculer steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test mongodb: @@ -845,7 +845,7 @@ jobs: PACKAGE_NAMES: mongodb SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test with: node-floor: newest-maintenance-lts @@ -864,7 +864,7 @@ jobs: PACKAGE_NAMES: mongodb-core,express-mongo-sanitize SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test with: node-floor: newest-maintenance-lts @@ -882,7 +882,7 @@ jobs: PLUGINS: mongoose SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test with: node-floor: newest-maintenance-lts @@ -894,7 +894,7 @@ jobs: env: PLUGINS: multer steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test mysql: @@ -913,7 +913,7 @@ jobs: PLUGINS: mysql SERVICES: mysql steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test mysql2: @@ -932,7 +932,7 @@ jobs: PLUGINS: mysql2 SERVICES: mysql2 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test nats: @@ -948,7 +948,7 @@ jobs: PLUGINS: nats SERVICES: nats steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test net: @@ -958,7 +958,7 @@ jobs: env: PLUGINS: net steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -1008,7 +1008,7 @@ jobs: PLUGINS: next PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -1027,7 +1027,7 @@ jobs: env: PLUGINS: node-serialize steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test opensearch: @@ -1046,7 +1046,7 @@ jobs: PLUGINS: opensearch SERVICES: opensearch steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test oracledb: @@ -1066,7 +1066,7 @@ jobs: SERVICES: oracledb DD_INJECT_FORCE: "true" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node with: @@ -1104,7 +1104,7 @@ jobs: env: PLUGINS: pino steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test with: node-floor: newest-maintenance-lts @@ -1116,7 +1116,7 @@ jobs: env: PLUGINS: passport-http steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test postgres: @@ -1135,7 +1135,7 @@ jobs: PLUGINS: pg SERVICES: postgres steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test prisma: @@ -1184,7 +1184,7 @@ jobs: SERVICES: prisma PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -1206,7 +1206,7 @@ jobs: env: PLUGINS: process steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test protobufjs: @@ -1217,7 +1217,7 @@ jobs: PLUGINS: protobufjs DD_DATA_STREAMS_ENABLED: true steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream pug: @@ -1227,7 +1227,7 @@ jobs: env: PLUGINS: pug steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test redis: @@ -1243,7 +1243,7 @@ jobs: PLUGINS: redis SERVICES: redis steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test ioredis: @@ -1271,7 +1271,7 @@ jobs: PLUGINS: ioredis SERVICES: redis-cluster steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test valkey: @@ -1287,7 +1287,7 @@ jobs: PLUGINS: iovalkey SERVICES: valkey steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test # Restify isn't compatible with Node.js v24 so we don't run against latest Node.js @@ -1299,7 +1299,7 @@ jobs: env: PLUGINS: restify steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install @@ -1329,7 +1329,7 @@ jobs: SERVICES: qpid DD_DATA_STREAMS_ENABLED: true steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test-and-upstream router: @@ -1339,7 +1339,7 @@ jobs: env: PLUGINS: router steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test sequelize: @@ -1349,7 +1349,7 @@ jobs: env: PLUGINS: sequelize steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test sharedb: @@ -1359,7 +1359,7 @@ jobs: env: PLUGINS: sharedb steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -1388,7 +1388,7 @@ jobs: PLUGINS: tedious SERVICES: mssql steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:plugins:ci @@ -1408,7 +1408,7 @@ jobs: env: PLUGINS: undici steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test url: @@ -1418,7 +1418,7 @@ jobs: env: PLUGINS: url steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test vm: @@ -1428,7 +1428,7 @@ jobs: env: PLUGINS: vm steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: ./.github/actions/plugins/test winston: @@ -1438,7 +1438,7 @@ jobs: env: PLUGINS: winston steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test ws: @@ -1448,5 +1448,5 @@ jobs: env: PLUGINS: ws steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index 3908a64aed..a887588ae2 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -27,7 +27,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:appsec:ci @@ -45,7 +45,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:ci @@ -69,7 +69,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: 24.14.1 # TODO: remove pin when https://github.com/nodejs/node/issues/62991 is fixed @@ -104,7 +104,7 @@ jobs: LDAP_USERS: "user01,user02" LDAP_PASSWORDS: "password1,password2" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -135,7 +135,7 @@ jobs: PLUGINS: pg|knex SERVICES: postgres steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -166,7 +166,7 @@ jobs: PLUGINS: mysql|mysql2|sequelize SERVICES: mysql steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -188,7 +188,7 @@ jobs: env: PLUGINS: express|body-parser|cookie-parser|multer steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -210,7 +210,7 @@ jobs: env: PLUGINS: fastify steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -232,7 +232,7 @@ jobs: env: PLUGINS: apollo-server|apollo-server-express|apollo-server-fastify|apollo-server-core steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -260,7 +260,7 @@ jobs: PLUGINS: express-mongo-sanitize|mquery SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -288,7 +288,7 @@ jobs: PLUGINS: mongoose SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/newest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -309,7 +309,7 @@ jobs: env: PLUGINS: cookie steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -357,7 +357,7 @@ jobs: PLUGINS: next PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -379,7 +379,7 @@ jobs: env: PLUGINS: lodash steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -403,7 +403,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -424,7 +424,7 @@ jobs: env: PLUGINS: passport-local|passport-http steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -445,7 +445,7 @@ jobs: env: PLUGINS: handlebars|pug steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -467,7 +467,7 @@ jobs: env: PLUGINS: node-serialize steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -507,7 +507,7 @@ jobs: PLUGINS: kafkajs SERVICES: kafka steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci @@ -528,7 +528,7 @@ jobs: env: PLUGINS: stripe steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:appsec:plugins:ci diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 191fc4190a..f6bc78b82b 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -13,7 +13,7 @@ jobs: dependencies: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - run: yarn audit - run: yarn audit diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 163bd884af..3c71b45f8d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,13 +39,13 @@ jobs: policy: codeql-analysis - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ steps.octo-sts.outputs.token }} - name: Initialize CodeQL id: init-codeql - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: languages: ${{ matrix.language }} config-file: .github/codeql_config.yml @@ -57,7 +57,7 @@ jobs: - name: Perform CodeQL Analysis id: analyze - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 with: token: ${{ github.token }} wait-for-processing: false diff --git a/.github/workflows/debugger.yml b/.github/workflows/debugger.yml index 2d051b3574..b4e1f2dc7f 100644 --- a/.github/workflows/debugger.yml +++ b/.github/workflows/debugger.yml @@ -22,7 +22,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index ae2af432e7..f6fe3da0fe 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -18,7 +18,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: '24.15' @@ -34,7 +34,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: '24.15' diff --git a/.github/workflows/eslint-rules.yml b/.github/workflows/eslint-rules.yml index 8296c1f6c1..a6a03beabb 100644 --- a/.github/workflows/eslint-rules.yml +++ b/.github/workflows/eslint-rules.yml @@ -17,7 +17,7 @@ jobs: eslint-rules: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:eslint-rules diff --git a/.github/workflows/flakiness.yml b/.github/workflows/flakiness.yml index 95738bf77a..20b691d794 100644 --- a/.github/workflows/flakiness.yml +++ b/.github/workflows/flakiness.yml @@ -34,7 +34,7 @@ jobs: with: scope: DataDog/dd-trace-js policy: self.flakiness - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout-cone-mode: false sparse-checkout: | @@ -52,7 +52,7 @@ jobs: - run: cat flakiness.md >> $GITHUB_STEP_SUMMARY - id: slack run: echo "report=$(cat flakiness.txt)" >> $GITHUB_OUTPUT - - uses: slackapi/slack-github-action@fc46ded2fc4d7f11bfa62864526f920cf1a1167d # v2.1.0 + - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v2.1.0 if: github.event_name == 'schedule' with: method: chat.postMessage diff --git a/.github/workflows/instrumentation.yml b/.github/workflows/instrumentation.yml index f2cc8d4d73..0769277743 100644 --- a/.github/workflows/instrumentation.yml +++ b/.github/workflows/instrumentation.yml @@ -26,7 +26,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:esbuild:ci @@ -41,7 +41,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:webpack:ci @@ -58,7 +58,7 @@ jobs: env: PLUGINS: ai steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-aws-sdk: @@ -68,7 +68,7 @@ jobs: env: PLUGINS: aws-sdk steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-bluebird: @@ -78,7 +78,7 @@ jobs: env: PLUGINS: bluebird steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-body-parser: @@ -88,7 +88,7 @@ jobs: env: PLUGINS: body-parser steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-child_process: @@ -98,7 +98,7 @@ jobs: env: PLUGINS: child_process steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-connect: @@ -108,7 +108,7 @@ jobs: env: PLUGINS: connect steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-cookie-parser: @@ -118,7 +118,7 @@ jobs: env: PLUGINS: cookie-parser steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test # The couchbase native binding ships libcouchbase: the 3.x line has no @@ -142,7 +142,7 @@ jobs: PLUGINS: couchbase PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.node-version }} @@ -164,7 +164,7 @@ jobs: env: PLUGINS: crypto steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-express-mongo-sanitize: @@ -180,7 +180,7 @@ jobs: PLUGINS: express-mongo-sanitize SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-express-session: @@ -190,7 +190,7 @@ jobs: env: PLUGINS: express-session steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-express: @@ -200,7 +200,7 @@ jobs: env: PLUGINS: express steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-express-multi-version: @@ -210,7 +210,7 @@ jobs: env: PLUGINS: express-multi-version steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-fastify: @@ -220,7 +220,7 @@ jobs: env: PLUGINS: fastify steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-fetch: @@ -230,7 +230,7 @@ jobs: env: PLUGINS: fetch steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-fs: @@ -240,7 +240,7 @@ jobs: env: PLUGINS: fs steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-generic-pool: @@ -250,7 +250,7 @@ jobs: env: PLUGINS: generic-pool steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-hono: @@ -260,7 +260,7 @@ jobs: env: PLUGINS: hono steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test # TODO: Retries below work around a flaky bug in Node.js http code. Revert to using @@ -272,7 +272,7 @@ jobs: env: PLUGINS: http steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - name: Run instrumentation tests (oldest-maintenance, with retries) @@ -305,7 +305,7 @@ jobs: env: PLUGINS: http-client-options steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-kafkajs: @@ -315,7 +315,7 @@ jobs: env: PLUGINS: kafkajs steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-knex: @@ -325,7 +325,7 @@ jobs: env: PLUGINS: knex steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-koa: @@ -335,7 +335,7 @@ jobs: env: PLUGINS: koa steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-light-my-request: @@ -345,7 +345,7 @@ jobs: env: PLUGINS: light-my-request steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-mongoose: @@ -361,7 +361,7 @@ jobs: PLUGINS: mongoose SERVICES: mongo steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test with: node-floor: newest-maintenance-lts @@ -373,7 +373,7 @@ jobs: env: PLUGINS: multer steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-mysql2: @@ -392,7 +392,7 @@ jobs: PLUGINS: mysql2 SERVICES: mysql2 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-nyc: @@ -402,7 +402,7 @@ jobs: env: PLUGINS: nyc steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-openai-lifecycle: @@ -412,7 +412,7 @@ jobs: env: PLUGINS: openai-lifecycle steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-otel-sdk-trace: @@ -422,7 +422,7 @@ jobs: env: PLUGINS: otel-sdk-trace steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-passport: @@ -432,7 +432,7 @@ jobs: env: PLUGINS: passport steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-passport-http: @@ -442,7 +442,7 @@ jobs: env: PLUGINS: passport-http steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-passport-local: @@ -452,7 +452,7 @@ jobs: env: PLUGINS: passport-local steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-pg: @@ -471,7 +471,7 @@ jobs: PLUGINS: pg SERVICES: postgres steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-promise-js: @@ -481,7 +481,7 @@ jobs: env: PLUGINS: promise-js steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-promise: @@ -491,7 +491,7 @@ jobs: env: PLUGINS: promise steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-q: @@ -501,7 +501,7 @@ jobs: env: PLUGINS: q steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-stripe: @@ -511,7 +511,7 @@ jobs: env: PLUGINS: stripe steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-restify: @@ -521,7 +521,7 @@ jobs: env: PLUGINS: restify steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-router: @@ -531,7 +531,7 @@ jobs: env: PLUGINS: router steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-url: @@ -541,7 +541,7 @@ jobs: env: PLUGINS: url steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-when: @@ -551,7 +551,7 @@ jobs: env: PLUGINS: when steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentation-zlib: @@ -561,7 +561,7 @@ jobs: env: PLUGINS: zlib steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/instrumentations/test instrumentations-misc: @@ -569,7 +569,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:instrumentations:misc:ci @@ -602,7 +602,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -630,7 +630,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} diff --git a/.github/workflows/llmobs.yml b/.github/workflows/llmobs.yml index 11c9f032ca..bdeba8bf96 100644 --- a/.github/workflows/llmobs.yml +++ b/.github/workflows/llmobs.yml @@ -30,7 +30,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node with: @@ -60,7 +60,7 @@ jobs: env: PLUGINS: openai steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node with: @@ -88,7 +88,7 @@ jobs: env: PLUGINS: langchain steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/newest-maintenance-lts - uses: ./.github/actions/install @@ -118,7 +118,7 @@ jobs: env: PLUGINS: aws-sdk steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install @@ -146,7 +146,7 @@ jobs: env: PLUGINS: google-cloud-vertexai steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install @@ -176,7 +176,7 @@ jobs: env: PLUGINS: ai steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/latest - uses: ./.github/actions/install @@ -202,7 +202,7 @@ jobs: env: PLUGINS: anthropic steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install @@ -232,7 +232,7 @@ jobs: env: PLUGINS: claude-agent-sdk steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start # The SDK is ESM-only. Tests use dynamic import() to load it. - uses: ./.github/actions/node/active-lts @@ -259,7 +259,7 @@ jobs: env: PLUGINS: google-genai steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install @@ -289,7 +289,7 @@ jobs: env: PLUGINS: langgraph steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test with: node-floor: newest-maintenance-lts @@ -301,5 +301,5 @@ jobs: env: PLUGINS: modelcontextprotocol-sdk steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test diff --git a/.github/workflows/openfeature.yml b/.github/workflows/openfeature.yml index 2241f5b16f..22ec5dd03b 100644 --- a/.github/workflows/openfeature.yml +++ b/.github/workflows/openfeature.yml @@ -22,7 +22,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -42,7 +42,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:integration:openfeature:coverage @@ -60,7 +60,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:integration:openfeature:coverage @@ -84,7 +84,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: 24.14.1 # TODO: remove pin when https://github.com/nodejs/node/issues/62991 is fixed diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index fe30ae4c60..c0e97a897f 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -53,7 +53,7 @@ jobs: install: bun add --linker=hoisted runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/active-lts - uses: ./.github/actions/install - run: FILENAME=$(npm pack --silent --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz @@ -67,7 +67,7 @@ jobs: bun-pack: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/active-lts - uses: ./.github/actions/install - run: mkdir npm bun @@ -80,7 +80,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/active-lts - uses: ./.github/actions/install - run: npm run test:integration:bun @@ -94,7 +94,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:core:ci @@ -115,7 +115,7 @@ jobs: env: PLUGINS: dd-trace-api steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test # TODO: Split this up as it runs tests for multiple different teams. @@ -131,7 +131,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -157,7 +157,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -175,7 +175,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:trace:guardrails:ci @@ -197,7 +197,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/active-lts - uses: ./.github/actions/install - run: bun add --linker=hoisted --ignore-scripts mocha@10 # Use older mocha to support old Node.js versions @@ -221,7 +221,7 @@ jobs: env: DD_TRACE_DEBUG: "true" # This exercises more of the guardrails code steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -235,7 +235,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:shimmer:ci diff --git a/.github/workflows/profiling.yml b/.github/workflows/profiling.yml index 5d66cfed39..6be7c32baa 100644 --- a/.github/workflows/profiling.yml +++ b/.github/workflows/profiling.yml @@ -27,7 +27,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:profiler:ci @@ -46,7 +46,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:profiler:ci @@ -74,7 +74,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: 24.14.1 # TODO: remove pin when https://github.com/nodejs/node/issues/62991 is fixed diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 8833b3db53..ed0b2e630d 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -13,7 +13,7 @@ jobs: actionlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout: .github - uses: ./.github/actions/node/latest @@ -39,7 +39,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run lint @@ -47,13 +47,13 @@ jobs: lint-editorconfig: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: docker build -q -t ec .github/editorconfig-checker && docker run --rm --volume=$PWD:/check ec release-scripts: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run test:release @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-latest name: Generated config types steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - run: npm run verify:config:types @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest name: Workflow job names (unique) steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: sparse-checkout: | .github @@ -94,7 +94,7 @@ jobs: with: scope: DataDog/dd-trace-js policy: package-size-report - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - run: FILENAME=$(npm pack --silent --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz - run: rm -rf * @@ -114,7 +114,7 @@ jobs: if: github.actor != 'dependabot[bot]' && github.event_name != 'pull_request' steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts-api-key - uses: ./.github/actions/dd-sts-app-key @@ -131,7 +131,7 @@ jobs: typescript: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/latest - uses: ./.github/actions/install - run: npm run type:doc:test @@ -141,7 +141,7 @@ jobs: # verify-yaml: # runs-on: ubuntu-latest # steps: - # - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + # - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - uses: ./.github/actions/node/latest # - uses: ./.github/actions/install # - run: node scripts/verify-ci-config.js @@ -154,7 +154,7 @@ jobs: has_changes: ${{ steps.diff.outputs.has_changes }} steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml index 02618239cb..615e706fd3 100644 --- a/.github/workflows/release-proposal.yml +++ b/.github/workflows/release-proposal.yml @@ -52,7 +52,7 @@ jobs: with: scope: DataDog/dd-trace-js policy: release-proposal - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 token: ${{ steps.octo-sts.outputs.token }} @@ -66,7 +66,7 @@ jobs: run: node scripts/release/proposal ${{ matrix.release-line }} -y ${{ github.event_name == 'workflow_dispatch' && '-f' || '' }} env: GITHUB_TOKEN: ${{ steps.octo-sts.outputs.token }} - - uses: slackapi/slack-github-action@fc46ded2fc4d7f11bfa62864526f920cf1a1167d # v2.1.0 + - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v2.1.0 if: > github.event_name == 'schedule' && steps.proposal.outputs.commit_count != '' && @@ -89,7 +89,7 @@ jobs: GitHub's rebase limit is 100 — once reached, commits that don't fit will be deferred to a subsequent release. Consider cutting this release soon. - - uses: slackapi/slack-github-action@fc46ded2fc4d7f11bfa62864526f920cf1a1167d # v2.1.0 + - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v2.1.0 if: > github.event_name == 'schedule' && steps.proposal.outputs.commit_count != '' && diff --git a/.github/workflows/release-validate.yml b/.github/workflows/release-validate.yml index c08a5f886a..75a7f34355 100644 --- a/.github/workflows/release-validate.yml +++ b/.github/workflows/release-validate.yml @@ -17,7 +17,7 @@ jobs: pull-requests: read id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Get GitHub Token via dd-octo-sts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d978cb3045..bcbd2a70b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,10 +79,12 @@ jobs: with: scope: DataDog/dd-trace-js policy: self.github.release.push-tags - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: ./.github/actions/node + with: + registry-url: https://registry.npmjs.org - id: pkg run: | content=`cat ./package.json | tr '\n' ' '` @@ -137,10 +139,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: ./.github/actions/node + with: + registry-url: https://registry.npmjs.org - run: node scripts/release/publish-electron.js --tag ${{ needs.setup.outputs.npm_tag }} docs: @@ -151,7 +155,7 @@ jobs: id-token: write contents: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node - id: pkg run: | @@ -164,7 +168,7 @@ jobs: yarn yarn build mv out /tmp/out - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: gh-pages - name: Deploy @@ -176,7 +180,7 @@ jobs: git add -A git commit -m ${{ fromJson(steps.pkg.outputs.json).version }} git push - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 publish-dev: if: github.event_name == 'workflow_dispatch' @@ -192,10 +196,12 @@ jobs: with: scope: DataDog/dd-trace-js policy: self.github.release.push-tags - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: ./.github/actions/node + with: + registry-url: https://registry.npmjs.org - uses: ./.github/actions/install - id: pkg run: | @@ -218,10 +224,12 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: ./.github/actions/node + with: + registry-url: https://registry.npmjs.org - uses: ./.github/actions/install - id: pkg run: | diff --git a/.github/workflows/serverless.yml b/.github/workflows/serverless.yml index 1a861649aa..63831a2223 100644 --- a/.github/workflows/serverless.yml +++ b/.github/workflows/serverless.yml @@ -26,7 +26,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node/oldest-maintenance-lts - uses: ./.github/actions/install - run: npm run test:lambda:ci @@ -103,7 +103,7 @@ jobs: SERVICES: localstack localstack-legacy DD_DATA_STREAMS_ENABLED: true steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node with: @@ -129,7 +129,7 @@ jobs: env: PLUGINS: aws-durable-execution-sdk-js steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test azure-event-hubs: @@ -156,7 +156,7 @@ jobs: PLUGINS: azure-event-hubs SERVICES: azurite,azureeventhubsemulator steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/integration-test azure-functions: @@ -218,7 +218,7 @@ jobs: SERVICES: azuresqledge,azureservicebusemulator,azurite,azureeventhubsemulator,azurecosmosemulator SPEC: ${{ matrix.spec }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Wait for Cosmos emulator to be ready run: timeout 120 bash -c 'until curl -sf -o /dev/null --max-time 5 http://127.0.0.1:8080/ready; do sleep 3; done' - name: Copy emulator config files @@ -270,7 +270,7 @@ jobs: PLUGINS: azure-service-bus SERVICES: azureservicebusemulator,azuresqledge steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Wait for Service Bus emulator to be ready run: timeout 120 bash -c 'until nc -z localhost 5672; do sleep 3; done' - uses: ./.github/actions/plugins/integration-test @@ -291,7 +291,7 @@ jobs: SERVICES: azurite steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/integration-test-newest-lts with: flags: serverless-azure-durable-functions @@ -323,7 +323,7 @@ jobs: NODE_OPTIONS: '--experimental-global-webcrypto' NODE_TLS_REJECT_UNAUTHORIZED: 0 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Wait for Cosmos emulator to be ready run: timeout 120 bash -c 'until curl -sf -o /dev/null --max-time 5 http://127.0.0.1:8080/ready; do sleep 3; done' - uses: ./.github/actions/plugins/test @@ -342,5 +342,5 @@ jobs: PLUGINS: google-cloud-pubsub SERVICES: gpubsub steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index a28470ebfa..710db17450 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout dd-trace-js - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: dd-trace-js - name: Pack dd-trace-js diff --git a/.github/workflows/test-optimization.yml b/.github/workflows/test-optimization.yml index 6a182b5694..3ce3678890 100644 --- a/.github/workflows/test-optimization.yml +++ b/.github/workflows/test-optimization.yml @@ -27,7 +27,7 @@ jobs: with: scope: DataDog/test-environment policy: dd-trace-js - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: token: ${{ steps.octo-sts.outputs.token }} - uses: ./.github/actions/node/oldest-maintenance-lts @@ -48,7 +48,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/node with: version: ${{ matrix.version }} @@ -84,7 +84,7 @@ jobs: # behaviour of matrix outputs is safe here. images: ${{ steps.versions.outputs.images }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Determine versions id: versions run: | @@ -150,7 +150,7 @@ jobs: OPTIONS_OVERRIDE: 1 PLAYWRIGHT_VERSION: ${{ matrix.playwright-version }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true @@ -185,7 +185,7 @@ jobs: DD_SERVICE: dd-trace-js-integration-tests DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true @@ -221,7 +221,7 @@ jobs: DD_SERVICE: dd-trace-js-integration-tests DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true @@ -249,7 +249,7 @@ jobs: env: PLUGINS: jest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test integration-cucumber: @@ -265,7 +265,7 @@ jobs: DD_SERVICE: dd-trace-js-integration-tests DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true @@ -291,7 +291,7 @@ jobs: outputs: image: ${{ steps.image.outputs.image }} steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Determine image tag id: image run: | @@ -334,7 +334,7 @@ jobs: DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 OPTIONS_OVERRIDE: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true @@ -401,7 +401,7 @@ jobs: DD_SERVICE: dd-trace-js-integration-tests DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true @@ -456,7 +456,7 @@ jobs: DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 OPTIONS_OVERRIDE: 1 steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/dd-sts-api-key id: dd-sts continue-on-error: true diff --git a/.github/workflows/update-3rdparty-licenses.yml b/.github/workflows/update-3rdparty-licenses.yml index 42dc05e40d..5991d859ba 100644 --- a/.github/workflows/update-3rdparty-licenses.yml +++ b/.github/workflows/update-3rdparty-licenses.yml @@ -28,7 +28,7 @@ jobs: policy: self.check-licenses - name: Check out PR branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 @@ -36,7 +36,7 @@ jobs: python-version: "3.14" - name: Check out dd-license-attribution - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: watson/dd-license-attribution ref: 8ea483b9f735bf8da632c89796789cc2a050a9a6 From 5705487cc95f57e5a92ea6f7837c45ba313a291e Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Thu, 23 Jul 2026 15:14:29 -0400 Subject: [PATCH 3/9] feat(llmobs): add support for OpenAI Agents (trace-processor) (#8044) * feat(llmobs): add OpenAI Agents trace-processor integration * ci(llmobs): test OpenAI Agents integration --- .github/workflows/llmobs.yml | 30 ++ docs/API.md | 2 + docs/test.ts | 1 + index.d.ts | 7 + index.d.v5.ts | 7 + .../src/helpers/hooks.js | 2 + .../src/openai-agents.js | 109 +++++ .../datadog-plugin-openai-agents/src/index.js | 74 ++++ .../src/integration.js | 405 ++++++++++++++++++ .../src/processor.js | 107 +++++ .../datadog-plugin-openai-agents/src/util.js | 60 +++ .../test/index.spec.js | 157 +++++++ .../test/integration.spec.js | 323 ++++++++++++++ .../test/processor.spec.js | 166 +++++++ .../test/test-setup.js | 231 ++++++++++ .../src/config/generated-config-types.d.ts | 2 + .../src/config/supported-configurations.json | 7 + .../src/llmobs/plugins/openai-agents/utils.js | 211 +++++++++ .../src/llmobs/plugins/openai/index.js | 14 +- .../src/llmobs/plugins/openai/utils.js | 20 + packages/dd-trace/src/llmobs/tagger.js | 4 + packages/dd-trace/src/plugins/index.js | 1 + .../openai_responses_post_1e503f45.yaml | 141 ++++++ .../openai_responses_post_25ba1894.yaml | 186 ++++++++ .../openai_responses_post_5004ba66.yaml | 139 ++++++ .../openai_responses_post_56b13001.yaml | 130 ++++++ .../openai_responses_post_7bac988c.yaml | 127 ++++++ .../openai_responses_post_9ff3470b.yaml | 284 ++++++++++++ .../openai_responses_post_af61e856.yaml | 127 ++++++ .../openai_responses_post_c7d7528a.yaml | 128 ++++++ .../openai_responses_post_cecd80bb.yaml | 128 ++++++ .../openai_v1_responses_post_f671d1c2.yaml | 53 +++ .../test/llmobs/openai-agents-utils.spec.js | 321 ++++++++++++++ .../dd-trace/test/llmobs/openai-utils.spec.js | 70 +++ .../plugins/openai-agents/index.spec.js | 313 ++++++++++++++ packages/dd-trace/test/llmobs/tagger.spec.js | 8 + packages/dd-trace/test/plugins/externals.js | 14 + .../test/plugins/versions/package.json | 1 + .../helpers/plugin-test-helpers/index.js | 2 +- supported_versions_output.json | 125 +++--- supported_versions_table.csv | 107 ++--- 41 files changed, 4222 insertions(+), 122 deletions(-) create mode 100644 packages/datadog-instrumentations/src/openai-agents.js create mode 100644 packages/datadog-plugin-openai-agents/src/index.js create mode 100644 packages/datadog-plugin-openai-agents/src/integration.js create mode 100644 packages/datadog-plugin-openai-agents/src/processor.js create mode 100644 packages/datadog-plugin-openai-agents/src/util.js create mode 100644 packages/datadog-plugin-openai-agents/test/index.spec.js create mode 100644 packages/datadog-plugin-openai-agents/test/integration.spec.js create mode 100644 packages/datadog-plugin-openai-agents/test/processor.spec.js create mode 100644 packages/datadog-plugin-openai-agents/test/test-setup.js create mode 100644 packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_1e503f45.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_25ba1894.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_5004ba66.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_56b13001.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_7bac988c.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_9ff3470b.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_af61e856.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c7d7528a.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_cecd80bb.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/openai/openai_v1_responses_post_f671d1c2.yaml create mode 100644 packages/dd-trace/test/llmobs/openai-agents-utils.spec.js create mode 100644 packages/dd-trace/test/llmobs/openai-utils.spec.js create mode 100644 packages/dd-trace/test/llmobs/plugins/openai-agents/index.spec.js diff --git a/.github/workflows/llmobs.yml b/.github/workflows/llmobs.yml index bdeba8bf96..370a87db33 100644 --- a/.github/workflows/llmobs.yml +++ b/.github/workflows/llmobs.yml @@ -303,3 +303,33 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: ./.github/actions/plugins/test + + openai-agents: + runs-on: ubuntu-latest + permissions: + id-token: write + env: + PLUGINS: openai-agents + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/oldest-maintenance-lts + - uses: ./.github/actions/install + - run: npm run test:plugins:ci + - run: npm run test:llmobs:plugins:ci + shell: bash + - uses: ./.github/actions/node/latest + - run: npm run test:plugins:ci + - run: npm run test:llmobs:plugins:ci + shell: bash + - uses: ./.github/actions/coverage + with: + flags: llmobs-${{ github.job }} + - if: always() + uses: ./.github/actions/testagent/logs + with: + suffix: llmobs-${{ github.job }} + - uses: ./.github/actions/upload-junit-artifacts + if: "!cancelled()" + with: + id: ${{ github.job }} diff --git a/docs/API.md b/docs/API.md index ab18e688d1..7ccfb8f7ba 100644 --- a/docs/API.md +++ b/docs/API.md @@ -86,6 +86,7 @@ tracer.use('pg', {
+
@@ -170,6 +171,7 @@ tracer.use('pg', { * [next](./interfaces/export_.plugins.next.html) * [nyc](./interfaces/export_.plugins.nyc.html) * [openai](./interfaces/export_.plugins.openai.html) +* [openai-agents](./interfaces/export_.plugins.openai_agents.html) * [opensearch](./interfaces/export_.plugins.opensearch.html) * [oracledb](./interfaces/export_.plugins.oracledb.html) * [pg](./interfaces/export_.plugins.pg.html) diff --git a/docs/test.ts b/docs/test.ts index c8fc30d164..6ab75f373c 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -391,6 +391,7 @@ tracer.use('nats'); tracer.use('net'); tracer.use('next'); tracer.use('next', nextOptions); +tracer.use('openai-agents'); tracer.use('opensearch'); tracer.use('opensearch', openSearchOptions); tracer.use('oracledb'); diff --git a/index.d.ts b/index.d.ts index 5590386b3f..6f703250bd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -291,6 +291,7 @@ interface Plugins { "next": tracer.plugins.next; "nyc": tracer.plugins.nyc; "openai": tracer.plugins.openai; + "openai-agents": tracer.plugins.openai_agents; "opensearch": tracer.plugins.opensearch; "oracledb": tracer.plugins.oracledb; "playwright": tracer.plugins.playwright; @@ -2968,6 +2969,12 @@ declare namespace tracer { */ interface openai extends Instrumentation {} + /** + * This plugin automatically instruments the + * [@openai/agents](https://www.npmjs.com/package/@openai/agents) library. + */ + interface openai_agents extends Instrumentation {} + /** * This plugin automatically instruments the * [opensearch](https://github.com/opensearch-project/opensearch-js) module. diff --git a/index.d.v5.ts b/index.d.v5.ts index 0aa1604770..14a7680153 100644 --- a/index.d.v5.ts +++ b/index.d.v5.ts @@ -293,6 +293,7 @@ interface Plugins { "next": tracer.plugins.next; "nyc": tracer.plugins.nyc; "openai": tracer.plugins.openai; + "openai-agents": tracer.plugins.openai_agents; "opensearch": tracer.plugins.opensearch; "oracledb": tracer.plugins.oracledb; "playwright": tracer.plugins.playwright; @@ -3138,6 +3139,12 @@ declare namespace tracer { */ interface openai extends Instrumentation {} + /** + * This plugin automatically instruments the + * [@openai/agents](https://www.npmjs.com/package/@openai/agents) library. + */ + interface openai_agents extends Instrumentation {} + /** * This plugin automatically instruments the * [opensearch](https://github.com/opensearch-project/opensearch-js) module. diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index b82309f408..f22a3b629d 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -21,6 +21,8 @@ module.exports = { '@apollo/gateway': () => require('../apollo'), '@langchain/langgraph': { esmFirst: true, fn: () => require('../langgraph') }, '@modelcontextprotocol/sdk': { esmFirst: true, fn: () => require('../modelcontextprotocol-sdk') }, + '@openai/agents': () => require('../openai-agents'), + '@openai/agents-openai': () => require('../openai-agents'), 'apollo-server-core': () => require('../apollo-server-core'), '@aws-sdk/smithy-client': () => require('../aws-sdk'), '@aws/durable-execution-sdk-js': () => require('../aws-durable-execution-sdk-js'), diff --git a/packages/datadog-instrumentations/src/openai-agents.js b/packages/datadog-instrumentations/src/openai-agents.js new file mode 100644 index 0000000000..1337e9cd29 --- /dev/null +++ b/packages/datadog-instrumentations/src/openai-agents.js @@ -0,0 +1,109 @@ +'use strict' + +const { channel } = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') + +// `WeakSet` keyed by module exports — replaces the underscored +// `mod._datadogPatched` flag while keeping dedupe semantics. Mods are kept +// alive by `require.cache` anyway, so this doesn't add lifetime to anything. +const patchedMods = new WeakSet() + +// Plugin subscribes to this and registers its TracingProcessor when +// `@openai/agents` loads. Publishing from here keeps this file free of +// any cross-package import from the plugin. +const agentsCoreLoadedCh = channel('apm:openai-agents:agents-core:loaded') + +// Plugin subscribes here to keep track of the OpenAI-compatible client's +// baseURL — used to resolve `model_provider` (openai / azure_openai / +// deepseek / unknown). +const responseClientCh = channel('apm:openai-agents:response:client') + +// Plugin uses addBind on this channel so that legacyStorage.run(store, fn) wraps +// the model call — including async iterator advancement for streaming responses. +// This ensures the active dd-trace span is visible to the openai plugin when it +// creates its openai.request span, correctly parenting it under the agent span. +const modelStartCh = channel('apm:openai-agents:model:start') + +// Reference to the loaded @openai/agents module, captured in the first hook +// so that wrapResponseMethod can call getCurrentSpan() without an additional +// require (and without triggering n/no-missing-require on agents-core internals). +let agentsMod + +// @openai/agents >=0.8.0 moved addTraceProcessor / getCurrentSpan out of the +// top-level re-exports. The new public surface uses getGlobalTraceProvider(): +// provider.registerProcessor(processor) (replaces addTraceProcessor) +// provider.getCurrentSpan() (replaces getCurrentSpan) +// Both APIs are tried so this file works across the full supported version range. +// The plugin subscriber (index.js) handles processor registration via the channel. +function getCurrentSpanId (mod) { + if (typeof mod?.getCurrentSpan === 'function') { + return mod.getCurrentSpan()?.spanId + } + if (typeof mod?.getGlobalTraceProvider === 'function') { + return mod.getGlobalTraceProvider().getCurrentSpan()?.spanId + } +} + +addHook({ name: '@openai/agents', versions: ['>=0.7.0'] }, (mod) => { + if (patchedMods.has(mod)) return mod + if (typeof mod?.addTraceProcessor !== 'function' && typeof mod?.getGlobalTraceProvider !== 'function') return mod + patchedMods.add(mod) + agentsMod = mod + agentsCoreLoadedCh.publish({ mod }) + return mod +}) + +function wrapResponseMethod (original) { + return function (...args) { + const agentsCoreSpanId = getCurrentSpanId(agentsMod) + publishClientBaseURL(this) + return modelStartCh.runStores({ agentsCoreSpanId }, () => original.apply(this, args)) + } +} + +function wrapStreamedResponseMethod (original) { + return function (...args) { + const agentsCoreSpanId = getCurrentSpanId(agentsMod) + publishClientBaseURL(this) + const iterator = modelStartCh.runStores({ agentsCoreSpanId }, () => original.apply(this, args)) + return wrapAsyncIterator(iterator, agentsCoreSpanId) + } +} + +function publishClientBaseURL (model) { + const baseURL = model?.client?.baseURL ?? model?._client?.baseURL + if (baseURL) responseClientCh.publish({ baseURL }) +} + +function wrapAsyncIterator (iterator, agentsCoreSpanId) { + if (!iterator || typeof iterator !== 'object') return iterator + + return { + next () { + return modelStartCh.runStores({ agentsCoreSpanId }, () => iterator.next.apply(iterator, arguments)) + }, + throw () { + if (typeof iterator.throw !== 'function') return Promise.reject(arguments[0]) + return modelStartCh.runStores({ agentsCoreSpanId }, () => iterator.throw.apply(iterator, arguments)) + }, + return () { + if (typeof iterator.return !== 'function') return Promise.resolve({ done: true, value: arguments[0] }) + return modelStartCh.runStores({ agentsCoreSpanId }, () => iterator.return.apply(iterator, arguments)) + }, + [Symbol.asyncIterator] () { + return this + }, + } +} + +addHook({ name: '@openai/agents-openai', versions: ['>=0.7.0'] }, (mod) => { + if (patchedMods.has(mod)) return mod + const proto = mod?.OpenAIResponsesModel?.prototype + if (!proto) return mod + + patchedMods.add(mod) + shimmer.wrap(proto, 'getResponse', wrapResponseMethod) + shimmer.wrap(proto, 'getStreamedResponse', wrapStreamedResponseMethod) + return mod +}) diff --git a/packages/datadog-plugin-openai-agents/src/index.js b/packages/datadog-plugin-openai-agents/src/index.js new file mode 100644 index 0000000000..60b7ddc561 --- /dev/null +++ b/packages/datadog-plugin-openai-agents/src/index.js @@ -0,0 +1,74 @@ +'use strict' + +const { storage } = require('../../datadog-core') +const Plugin = require('../../dd-trace/src/plugins/plugin') +const { OpenAIAgentsIntegration } = require('./integration') +const { DDOpenAIAgentsProcessor } = require('./processor') + +const legacyStorage = storage('legacy') + +/** + * Drives the openai-agents integration through agents-core's + * `TracingProcessor` interface. The instrumentation hook publishes the + * loaded `@openai/agents` module on a channel; this plugin subscribes + * during its constructor (which runs synchronously between `loadChannel`'s + * publish and the addHook callback) and registers the processor. + * + * The instrumentation also publishes the OpenAI-compatible client's baseURL on + * each model response call, so the integration can resolve `model_provider`. + * + * The integration's `enabled` flag follows this plugin's configure() + * lifecycle. Each loaded version of the agents package replaces all processors + * via setTraceProcessors() on module load, so the plugin re-registers a + * fresh DDOpenAIAgentsProcessor for each module version that fires the channel. + */ +class OpenaiAgentsPlugin extends Plugin { + static id = 'openai-agents' + + #integration + + constructor (tracer, tracerConfig) { + super(tracer, tracerConfig) + this.#integration = new OpenAIAgentsIntegration({ + tracer: this.tracer, + config: tracerConfig, + }) + + // Register a new processor each time @openai/agents fires the channel. + // Each module version calls setTraceProcessors() on load (which replaces + // all processors), so we must re-register after every new version loads. + // The instrumentation's patchedMods WeakSet ensures each module instance + // fires the channel exactly once, so no duplicates accumulate. + this.addSub('apm:openai-agents:agents-core:loaded', ({ mod }) => { + const processor = new DDOpenAIAgentsProcessor(() => this.#integration) + if (typeof mod?.addTraceProcessor === 'function') { + mod.addTraceProcessor(processor) + } else { + mod.getGlobalTraceProvider().registerProcessor(processor) + } + }) + + this.addSub('apm:openai-agents:response:client', ({ baseURL }) => { + if (!this.#integration.enabled) return + this.#integration.setClientBaseURL(baseURL) + }) + + // Activate the current agent's dd-trace span in legacyStorage for the + // duration of model response calls and stream iterator advancement. This + // makes the openai plugin's shimmer see the correct parent when it creates its + // openai.request span, so all spans land in the same trace. + this.addBind('apm:openai-agents:model:start', ({ agentsCoreSpanId }) => { + if (!this.#integration.enabled || !agentsCoreSpanId) return legacyStorage.getStore() + const ddSpan = this.#integration.getDDSpan(agentsCoreSpanId) + if (!ddSpan) return legacyStorage.getStore() + return { ...legacyStorage.getStore(), span: ddSpan } + }) + } + + configure (config) { + super.configure(config) + this.#integration.setEnabled(!!config?.enabled) + } +} + +module.exports = OpenaiAgentsPlugin diff --git a/packages/datadog-plugin-openai-agents/src/integration.js b/packages/datadog-plugin-openai-agents/src/integration.js new file mode 100644 index 0000000000..2996be5c42 --- /dev/null +++ b/packages/datadog-plugin-openai-agents/src/integration.js @@ -0,0 +1,405 @@ +'use strict' + +const LLMObsTagger = require('../../dd-trace/src/llmobs/tagger') +const { getOpenAIModelProvider } = require('../../dd-trace/src/llmobs/plugins/openai/utils') +const { + extractInputMessages, + extractOutputMessages, + extractMetrics, + extractMetadata, +} = require('../../dd-trace/src/llmobs/plugins/openai-agents/utils') +const { AGENTS_ERROR_TYPE, applyError, deriveSpanName } = require('./util') + +const COMPONENT = 'openai-agents' +const DEFAULT_MODEL_PROVIDER = 'openai' + +const KIND_TO_SPAN_KIND = { + agent: 'internal', + tool: 'internal', + task: 'internal', + llm: 'client', +} + +/** + * @typedef {{ + * spanId: string, + * traceId: string, + * currentTopLevelAgentSpanId?: string, + * currentTopLevelAgentName?: string, + * inputOaiSpan?: object, + * inputMessages?: Array<{ role: string, content: string }>, + * outputOaiSpan?: object, + * metadata?: Record, + * groupId?: string, + * }} LLMObsTraceInfo + */ + +/** + * Owns tracer/tagger refs, maps agents-core span ids → dd-trace spans, and + * reconstructs workflow-level input/output from the first and last response + * spans of the top-level agent. + */ +class OpenAIAgentsIntegration { + #tracer + #enabled = false + #modelProvider = DEFAULT_MODEL_PROVIDER + /** + * LLMObs is gated independently of APM tracing: when DD_LLMOBS_ENABLED is + * false we keep emitting APM spans for the agent workflow but skip all + * LLMObs tagging and the work that feeds it. The tagger is the single + * source of truth for "is LLMObs on for this integration?". + * @type {LLMObsTagger | undefined} + */ + #tagger + /** @type {Map} */ + #oaiToDdSpan = new Map() + /** @type {Map} */ + #traceInfo = new Map() + + constructor ({ tracer, config } = {}) { + this.#tracer = tracer + this.#tagger = config?.llmobs?.DD_LLMOBS_ENABLED ? new LLMObsTagger(config, true) : undefined + } + + get enabled () { + return this.#enabled + } + + setEnabled (enabled) { + this.#enabled = enabled + } + + /** + * Update the model_provider tag based on the OpenAI-compatible client's + * baseURL captured by the agents-openai instrumentation hook. Single- + * provider-per-process is the assumed deployment shape; concurrent runs + * against different providers will see last-write-wins on this field. + * + * @param {string} baseURL + */ + setClientBaseURL (baseURL) { + if (typeof baseURL !== 'string' || baseURL.length === 0) return + this.#modelProvider = getOpenAIModelProvider(baseURL) + } + + /** + * @param {string} spanId agents-core spanId + * @returns {import('../../dd-trace/src/opentracing/span') | undefined} + */ + getDDSpan (spanId) { + return this.#oaiToDdSpan.get(spanId) + } + + clearState () { + // Finish any dd-trace spans still in-flight so we don't leak open traces + // when agents-core's TracingProcessor.shutdown() runs (e.g., process + // exiting mid-run). + for (const ddSpan of this.#oaiToDdSpan.values()) { + ddSpan.finish() + } + this.#oaiToDdSpan.clear() + this.#traceInfo.clear() + } + + // ── Trace lifecycle ───────────────────────────────────────────────────────── + + startTrace (oaiTrace) { + const traceId = oaiTrace.traceId + if (!traceId) return + + const name = oaiTrace.name || 'Agent workflow' + const ddSpan = this.#tracer.startSpan(name, { + tags: { + component: COMPONENT, + 'span.kind': 'internal', + }, + }) + + this.#oaiToDdSpan.set(traceId, ddSpan) + this.#traceInfo.set(traceId, { + spanId: ddSpan.context().toSpanId(), + traceId, + groupId: oaiTrace.groupId || undefined, + metadata: oaiTrace.metadata, + }) + + this.#tagger?.registerLLMObsSpan(ddSpan, { + kind: 'workflow', + name, + integration: COMPONENT, + sessionId: oaiTrace.groupId || undefined, + }) + } + + endTrace (oaiTrace) { + this.#completeWorkflowSpan(oaiTrace.traceId) + } + + /** + * Finish the workflow dd-trace span and clear its bookkeeping. Used by both + * agents-core's normal `Trace.end()` path and the orphan-recovery path + * (when `withTrace` skips its end callback because the body threw). When + * `rootAgentSpan` is provided, its `error` field is reflected onto the + * workflow span before finishing. + * + * @param {string | undefined} traceId + * @param {object} [rootAgentSpan] - parentless oai-span that ended in error. + */ + #completeWorkflowSpan (traceId, rootAgentSpan) { + if (!traceId) return + const ddSpan = this.#oaiToDdSpan.get(traceId) + if (!ddSpan) return + + if (rootAgentSpan?.error) { + ddSpan.setTag('error', true) + ddSpan.setTag('error.type', AGENTS_ERROR_TYPE) + if (rootAgentSpan.error.message) { + ddSpan.setTag('error.message', rootAgentSpan.error.message) + } + } + + if (this.#tagger) this.#setTraceAttributes(ddSpan, traceId) + ddSpan.finish() + this.#oaiToDdSpan.delete(traceId) + this.#traceInfo.delete(traceId) + } + + // ── Span lifecycle ────────────────────────────────────────────────────────── + + startSpan (oaiSpan, llmobsKind) { + const spanId = oaiSpan.spanId + if (!spanId) return + + const parentSpan = this.#resolveParent(oaiSpan) + const spanName = deriveSpanName(oaiSpan) + + const ddSpan = this.#tracer.startSpan(spanName, { + childOf: parentSpan, + tags: { + component: COMPONENT, + 'span.kind': KIND_TO_SPAN_KIND[llmobsKind] ?? 'internal', + }, + }) + + this.#oaiToDdSpan.set(spanId, ddSpan) + + if (this.#tagger) { + const llmobsOptions = { + kind: llmobsKind, + name: spanName, + integration: COMPONENT, + parent: parentSpan, + } + + if (oaiSpan.spanData?.type === 'response') { + // Model name only arrives with the response; tagged in + // `#setResponseAttributes` once known. Model provider is resolved from + // the agents-openai client's baseURL captured at getResponse time. + llmobsOptions.modelProvider = this.#modelProvider + } + + this.#tagger.registerLLMObsSpan(ddSpan, llmobsOptions) + this.#updateTraceInfoInput(oaiSpan, spanName) + } + } + + endSpan (oaiSpan) { + const spanId = oaiSpan.spanId + const ddSpan = this.#oaiToDdSpan.get(spanId) + if (!ddSpan) return + + applyError(ddSpan, oaiSpan) + + if (oaiSpan.spanData?.type === 'handoff') { + const spanName = deriveSpanName(oaiSpan) + ddSpan.setOperationName(spanName) + this.#tagger?.setName(ddSpan, spanName) + } + + if (this.#tagger) { + const spanData = oaiSpan.spanData + switch (spanData?.type) { + case 'response': + this.#setResponseAttributes(ddSpan, oaiSpan) + this.#updateTraceInfoOutput(oaiSpan) + break + case 'function': + this.#tagger.tagTextIO(ddSpan, spanData.input ?? '', spanData.output ?? '') + break + case 'handoff': + this.#tagger.tagTextIO(ddSpan, spanData.from_agent ?? '', spanData.to_agent ?? '') + break + case 'agent': + this.#setAgentAttributes(ddSpan, oaiSpan) + break + case 'custom': + if (spanData.data && typeof spanData.data === 'object') { + this.#tagger.tagMetadata(ddSpan, spanData.data) + } + break + } + } + + ddSpan.finish() + + // agents-core's withTrace skips Trace.end() when its callback throws, so an + // errored parentless span is our last chance to finalize the workflow. + if (oaiSpan.parentId == null && oaiSpan.error) { + this.#completeWorkflowSpan(oaiSpan.traceId, oaiSpan) + } + this.#oaiToDdSpan.delete(spanId) + } + + // ── Per-type attribute setters ────────────────────────────────────────────── + + #setResponseAttributes (ddSpan, oaiSpan) { + const response = oaiSpan.spanData?._response + const input = oaiSpan.spanData?._input + if (response?.model) { + this.#tagger.tagModelName(ddSpan, response.model) + } + + // Override the LLMObs span name to `{parent_agent_name} (LLM)` only when + // the response is a direct child of the top-level agent (Python parity: + // see `_llmobs_set_response_attributes` in dd-trace-py). For bare + // `withResponseSpan` calls outside a `Runner.run()` flow the default + // name (`openai_agents.response`) stays. + const parentAgentName = this.#llmSpanParentAgentName(oaiSpan) + if (parentAgentName) { + this.#tagger.setName(ddSpan, `${parentAgentName} (LLM)`) + } + + // Always tag LLM I/O so the LLMObs event shape is consistent across + // happy/error paths. The extract* helpers emit placeholder messages + // when their source is absent. + const inputMessages = extractInputMessages(input, response?.instructions) + this.#tagger.tagLLMIO(ddSpan, inputMessages, extractOutputMessages(response)) + + // Cache messages for the workflow span's trace-level input (Python + // parity: last message of the first response under the top-level agent). + // Avoids re-running extractInputMessages in #setTraceAttributes. + const info = this.#traceInfo.get(oaiSpan.traceId) + if (info && info.inputOaiSpan === oaiSpan) { + info.inputMessages = inputMessages + } + + if (response) { + const metrics = extractMetrics(response) + if (metrics) this.#tagger.tagMetrics(ddSpan, metrics) + + const metadata = extractMetadata(response) + if (metadata) this.#tagger.tagMetadata(ddSpan, metadata) + } + } + + /** + * If this response span's parent is the top-level agent span of the trace, + * return that agent's dd-trace span name. Used to set the LLMObs span name + * to `${agentName} (LLM)` (Python parity). + * + * @param {object} oaiSpan + * @returns {string | undefined} + */ + #llmSpanParentAgentName (oaiSpan) { + const traceInfo = this.#traceInfo.get(oaiSpan.traceId) + if (!traceInfo?.currentTopLevelAgentSpanId) return + if (oaiSpan.parentId !== traceInfo.currentTopLevelAgentSpanId) return + return traceInfo.currentTopLevelAgentName + } + + #setAgentAttributes (ddSpan, oaiSpan) { + const spanData = oaiSpan.spanData + let metadata + if (Array.isArray(spanData?.handoffs) && spanData.handoffs.length > 0) { + metadata = { handoffs: spanData.handoffs } + } + if (Array.isArray(spanData?.tools) && spanData.tools.length > 0) { + metadata ??= {} + metadata.tools = spanData.tools + } + if (spanData?.output_type) { + metadata ??= {} + metadata.output_type = spanData.output_type + } + if (metadata) this.#tagger.tagMetadata(ddSpan, metadata) + } + + #setTraceAttributes (ddSpan, traceId) { + const info = this.#traceInfo.get(traceId) + if (!info) return + + // Workflow-level input is the last input message of the first response + // span under the top-level agent; output is `response.output_text` of + // the last response span. Matches dd-trace-py's + // `OaiSpanAdapter.llmobs_trace_input` / `response_output_text`. The + // input messages were cached during #setResponseAttributes. + const lastInputMessage = info.inputMessages?.at(-1) + const inputValue = typeof lastInputMessage?.content === 'string' ? lastInputMessage.content : '' + const outputValue = info.outputOaiSpan?.spanData?._response?.output_text ?? '' + + this.#tagger.tagTextIO(ddSpan, inputValue, outputValue) + + if (info.metadata && Object.keys(info.metadata).length > 0) { + this.#tagger.tagMetadata(ddSpan, info.metadata) + } + } + + // ── Trace-info reconstruction (Python parity) ─────────────────────────────── + + #updateTraceInfoInput (oaiSpan, spanName) { + const info = this.#traceInfo.get(oaiSpan.traceId) + if (!info) return + + const parentId = oaiSpan.parentId + const type = oaiSpan.spanData?.type + + // Identify the first top-level agent span under the root trace and + // stash its display name so `${agentName} (LLM)` doesn't have to read + // the dd-trace span context's private fields later. + if (type === 'agent' && parentId == null) { + info.currentTopLevelAgentSpanId = oaiSpan.spanId + info.currentTopLevelAgentName = spanName + } + + // Capture the first response span whose parent is the top-level agent + // as the workflow-level input source. + if ( + type === 'response' && + parentId && + !info.inputOaiSpan && + parentId === info.currentTopLevelAgentSpanId + ) { + info.inputOaiSpan = oaiSpan + } + } + + #updateTraceInfoOutput (oaiSpan) { + const info = this.#traceInfo.get(oaiSpan.traceId) + if (!info) return + + if ( + oaiSpan.parentId && + info.currentTopLevelAgentSpanId && + oaiSpan.parentId === info.currentTopLevelAgentSpanId + ) { + info.outputOaiSpan = oaiSpan + } + } + + // ── Helpers ───────────────────────────────────────────────────────────────── + + #resolveParent (oaiSpan) { + const parentId = oaiSpan.parentId + const traceId = oaiSpan.traceId + if (parentId) { + const parent = this.#oaiToDdSpan.get(parentId) + if (parent) return parent + } + if (traceId) { + const root = this.#oaiToDdSpan.get(traceId) + if (root) return root + } + } +} + +module.exports = { OpenAIAgentsIntegration } diff --git a/packages/datadog-plugin-openai-agents/src/processor.js b/packages/datadog-plugin-openai-agents/src/processor.js new file mode 100644 index 0000000000..41be76eea0 --- /dev/null +++ b/packages/datadog-plugin-openai-agents/src/processor.js @@ -0,0 +1,107 @@ +'use strict' + +const log = require('../../dd-trace/src/log') + +const SPAN_KIND_BY_TYPE = { + agent: 'agent', + function: 'tool', + handoff: 'tool', + guardrail: 'task', + custom: 'task', + response: 'llm', +} + +// Lifecycle methods are awaited by agents-core. Share one resolved Promise +// across every callback so we don't allocate per span event. +const RESOLVED = Promise.resolve() + +/** + * dd-trace-js implementation of the agents-core `TracingProcessor` interface. + * Registered via `addTraceProcessor(new DDOpenAIAgentsProcessor(integration))` inside + * the `@openai/agents` module load hook. Mirrors Python's LLMObsTraceProcessor. + * + * Each agents-core Span / Trace lifecycle event turns into a dd-trace span + * (APM + LLMObs-annotated) keyed off the agents-core spanId / traceId. Parent + * hierarchy is resolved through the agents-core parentId chain, which gives us + * correct multi-agent handoff nesting that ctx-argument capture cannot provide. + * + * agents-core awaits the lifecycle methods, so each one returns a settled + * Promise even though the work is synchronous. + */ +class DDOpenAIAgentsProcessor { + /** + * @param {() => (import('./integration').OpenAIAgentsIntegration | undefined)} getIntegration - Lazy accessor for the + * current OpenAIAgentsIntegration singleton. Read on each lifecycle event + * so re-instantiating the plugin doesn't strand the processor against an + * old integration reference inside agents-core. + */ + constructor (getIntegration) { + this._getIntegration = getIntegration + } + + onTraceStart (oaiTrace) { + const integration = this._getIntegration() + if (!integration?.enabled) return RESOLVED + try { + integration.startTrace(oaiTrace) + } catch (err) { + log.warn('[openai-agents] onTraceStart failed: %s', err) + } + return RESOLVED + } + + onTraceEnd (oaiTrace) { + const integration = this._getIntegration() + if (!integration?.enabled) return RESOLVED + try { + integration.endTrace(oaiTrace) + } catch (err) { + log.warn('[openai-agents] onTraceEnd failed: %s', err) + } + return RESOLVED + } + + onSpanStart (oaiSpan) { + const integration = this._getIntegration() + if (!integration?.enabled) return RESOLVED + if (!oaiSpan?.spanData) return RESOLVED // guard NoopSpan + const kind = SPAN_KIND_BY_TYPE[oaiSpan.spanData.type] + if (!kind) return RESOLVED // span types without an LLMObs kind are not traced + try { + integration.startSpan(oaiSpan, kind) + } catch (err) { + log.warn('[openai-agents] onSpanStart failed: %s', err) + } + return RESOLVED + } + + onSpanEnd (oaiSpan) { + const integration = this._getIntegration() + if (!integration?.enabled) return RESOLVED + if (!oaiSpan?.spanData) return RESOLVED + try { + integration.endSpan(oaiSpan) + } catch (err) { + log.warn('[openai-agents] onSpanEnd failed: %s', err) + } + return RESOLVED + } + + forceFlush () { + // dd-trace exports on its own schedule; nothing to force here. + return RESOLVED + } + + shutdown () { + const integration = this._getIntegration() + if (!integration) return RESOLVED + try { + integration.clearState() + } catch (err) { + log.warn('[openai-agents] shutdown cleanup failed: %s', err) + } + return RESOLVED + } +} + +module.exports = { DDOpenAIAgentsProcessor } diff --git a/packages/datadog-plugin-openai-agents/src/util.js b/packages/datadog-plugin-openai-agents/src/util.js new file mode 100644 index 0000000000..062163b808 --- /dev/null +++ b/packages/datadog-plugin-openai-agents/src/util.js @@ -0,0 +1,60 @@ +'use strict' + +// agents-core's `error` is a plain `{ message, data }` object, not a JS Error +// — there's no constructor to name and no stack. We tag a stable type constant +// and stringify `data` into the message so the LLMObs error shape stays +// consistent with other integrations. +const AGENTS_ERROR_TYPE = 'AgentsCoreError' + +/** + * Build the dd-trace span name from an agents-core oai-span. Handoffs collapse + * the target agent name into snake_case under a `transfer_to_` prefix; other + * span types use the SDK-provided name, falling back to + * `openai_agents.` (or `openai_agents.request` when even the type is + * missing). + * + * @param {object} oaiSpan + * @returns {string} + */ +function deriveSpanName (oaiSpan) { + const spanData = oaiSpan.spanData + if (spanData?.type === 'handoff') { + const toAgent = spanData.to_agent || '' + if (toAgent) return `transfer_to_${toAgent.replaceAll(' ', '_').toLowerCase()}` + } + if (spanData?.name) return spanData.name + return spanData?.type ? `openai_agents.${spanData.type}` : 'openai_agents.request' +} + +/** + * Apply agents-core's error shape onto a dd-trace span. No-op when the + * oai-span has no error attached. + * + * @param {object} ddSpan + * @param {object} oaiSpan + */ +function applyError (ddSpan, oaiSpan) { + const err = oaiSpan.error + if (!err) return + + ddSpan.setTag('error', true) + + let errorMessage = err.message || 'Error' + if (err.data) { + try { + errorMessage = JSON.stringify(err.data) + } catch { + // circular / non-serializable — fall back to the raw message + } + } + + ddSpan.setTag('error.type', AGENTS_ERROR_TYPE) + ddSpan.setTag('error.message', errorMessage) + ddSpan.setTag('error.stack', err.stack || '') +} + +module.exports = { + AGENTS_ERROR_TYPE, + deriveSpanName, + applyError, +} diff --git a/packages/datadog-plugin-openai-agents/test/index.spec.js b/packages/datadog-plugin-openai-agents/test/index.spec.js new file mode 100644 index 0000000000..d9329e5498 --- /dev/null +++ b/packages/datadog-plugin-openai-agents/test/index.spec.js @@ -0,0 +1,157 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { createIntegrationTestSuite } = require('../../dd-trace/test/setup/helpers/plugin-test-helpers') +const { ANY_STRING, assertObjectContains } = require('../../../integration-tests/helpers') +const TestSetup = require('./test-setup') + +const testSetup = new TestSetup() + +function findSpan (traces, predicate) { + return traces.flat().find(predicate) +} + +/** + * Integration test suite for the processor-driven openai-agents integration. + * + * agents-core only creates Span objects inside real agent-execution flows + * (run(), withTrace(), withAgentSpan(), withResponseSpan(), etc.) — direct + * calls to helpers like invokeFunctionTool / onInvokeHandoff / runToolGuardrails + * do NOT emit Spans because agents-core's span creation happens in the runner, + * not in those helpers. The tests below exercise real flows. + */ +createIntegrationTestSuite('openai-agents', '@openai/agents', { + category: 'generative-ai', + additionalPlugins: ['openai'], +}, (meta) => { + const { agent } = meta + + before(async () => { + await testSetup.setup(meta.mod, meta.version) + }) + + after(async () => { + await testSetup.teardown() + }) + + describe('run() — single-agent workflow', () => { + it('emits a workflow span with correct component/kind tags', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const workflowSpan = findSpan(traces, s => s.name === 'Agent workflow') + assertObjectContains(workflowSpan, { + name: 'Agent workflow', + service: ANY_STRING, + meta: { + component: 'openai-agents', + 'span.kind': 'internal', + }, + }) + }) + + const result = await testSetup.run() + assert.ok(result, 'run() should return a result object') + assert.notStrictEqual(result.finalOutput, undefined, 'run() should have finalOutput') + + return traceAssertion + }) + + it('emits an agent span named after the running agent', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const agentSpan = findSpan(traces, s => s.name === 'test_agent') + assertObjectContains(agentSpan, { + name: 'test_agent', + meta: { component: 'openai-agents' }, + }) + }) + + await testSetup.run() + return traceAssertion + }) + + it('parents the openai.request span under the agent span', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const flat = traces.flat() + const agentSpan = flat.find(s => s.name === 'test_agent') + const openaiSpan = flat.find(s => s.name === 'openai.request') + + assert.ok(agentSpan, 'expected an agent span') + assert.ok(openaiSpan, 'expected an openai.request span') + assert.equal( + openaiSpan.parent_id.toString(), + agentSpan.span_id.toString(), + 'openai.request should be a child of the agent span' + ) + }) + + await testSetup.run() + return traceAssertion + }) + + it('parents the streamed openai.request span under the agent span', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const flat = traces.flat() + const agentSpan = flat.find(s => s.name === 'test_agent') + const openaiSpan = flat.find(s => s.name === 'openai.request') + + assert.ok(agentSpan, 'expected an agent span') + assert.ok(openaiSpan, 'expected an openai.request span') + assert.equal( + openaiSpan.parent_id.toString(), + agentSpan.span_id.toString(), + 'streamed openai.request should be a child of the agent span' + ) + }) + + await testSetup.runStreamed() + return traceAssertion + }) + + it('finishes the workflow span even when the underlying agent throws', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const workflowSpan = findSpan(traces, s => s.name === 'Agent workflow') + assertObjectContains(workflowSpan, { + name: 'Agent workflow', + meta: { component: 'openai-agents' }, + }) + }) + + try { + await testSetup.runError() + } catch (err) { + // errorAgent triggers an intentional error from the mocked model + } + + return traceAssertion + }) + }) + + describe('multi-agent handoff hierarchy', () => { + it('keeps both agents in the workflow across a real Runner.run() handoff', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const flat = traces.flat() + + const workflow = flat.find(s => s.name === 'Agent workflow') + const agentA = flat.find(s => s.name === 'agent_a') + const handoff = flat.find(s => s.name === 'transfer_to_agent_b') + const agentB = flat.find(s => s.name === 'agent_b') + + assert.ok(workflow, 'expected a workflow span') + assert.ok(agentA, 'expected an agent span named agent_a') + assert.ok(handoff, 'expected a handoff span named transfer_to_agent_b') + assert.ok(agentB, 'expected an agent span named agent_b') + + assert.equal(agentA.parent_id.toString(), workflow.span_id.toString(), + 'agent_a should be a child of the workflow span') + assert.equal(handoff.parent_id.toString(), agentA.span_id.toString(), + 'handoff should be a child of agent_a') + assert.equal(agentB.parent_id.toString(), workflow.span_id.toString(), + 'agent_b should be a child of the workflow span') + }) + + const result = await testSetup.multiAgentHandoff() + assert.strictEqual(result.finalOutput, 'done') + return traceAssertion + }) + }) +}) diff --git a/packages/datadog-plugin-openai-agents/test/integration.spec.js b/packages/datadog-plugin-openai-agents/test/integration.spec.js new file mode 100644 index 0000000000..c3ccd9a4cb --- /dev/null +++ b/packages/datadog-plugin-openai-agents/test/integration.spec.js @@ -0,0 +1,323 @@ +'use strict' + +const assert = require('node:assert/strict') +const sinon = require('sinon') + +const LLMObsTagger = require('../../dd-trace/src/llmobs/tagger') +const { OpenAIAgentsIntegration } = require('../src/integration') + +function makeFakeSpan (spanId = 'dd-span-id') { + const context = { + _trace: { tags: {} }, + toSpanId: () => spanId, + toTraceId: () => '00000000000000001111111111111111', + } + + return { + setTag: sinon.stub(), + setOperationName: sinon.stub(), + finish: sinon.stub(), + context: () => context, + } +} + +function makeFakeTracer (preseededSpans = []) { + const startSpan = sinon.stub() + preseededSpans.forEach((span, i) => startSpan.onCall(i).returns(span)) + startSpan.callsFake(() => makeFakeSpan()) + return { startSpan } +} + +function build ({ + tracerSpans = [], + config = { llmobs: { DD_LLMOBS_ENABLED: false } }, +} = {}) { + const tracer = makeFakeTracer(tracerSpans) + const integration = new OpenAIAgentsIntegration({ + tracer, + config, + }) + return { integration, tracer } +} + +afterEach(() => sinon.restore()) + +describe('OpenAIAgentsIntegration', () => { + describe('enabled flag', () => { + it('starts disabled and flips via setEnabled', () => { + const { integration } = build() + assert.strictEqual(integration.enabled, false) + integration.setEnabled(true) + assert.strictEqual(integration.enabled, true) + integration.setEnabled(false) + assert.strictEqual(integration.enabled, false) + }) + }) + + describe('setClientBaseURL', () => { + it('ignores non-string baseURL values', () => { + const { integration, tracer } = build() + integration.setClientBaseURL(undefined) + integration.setClientBaseURL(null) + integration.setClientBaseURL(123) + sinon.assert.notCalled(tracer.startSpan) + }) + + it('ignores the empty string', () => { + const { integration } = build() + integration.setClientBaseURL('') + }) + + it('accepts a recognised URL', () => { + const { integration } = build() + integration.setClientBaseURL('https://my-resource.openai.azure.com/openai') + }) + }) + + describe('startTrace', () => { + it('does nothing when traceId is missing', () => { + const { integration, tracer } = build() + integration.startTrace({}) + sinon.assert.notCalled(tracer.startSpan) + }) + + it('falls back to the default workflow name when oaiTrace.name is empty', () => { + const { integration, tracer } = build() + integration.startTrace({ traceId: 't1' }) + sinon.assert.calledOnce(tracer.startSpan) + assert.strictEqual(tracer.startSpan.firstCall.args[0], 'Agent workflow') + }) + + it('uses oaiTrace.name when provided', () => { + const { integration, tracer } = build() + integration.startTrace({ traceId: 't1', name: 'My workflow', groupId: 'g1' }) + assert.strictEqual(tracer.startSpan.firstCall.args[0], 'My workflow') + }) + + it('registers LLMObs spans when DD_LLMOBS_ENABLED is true', () => { + const workflowSpan = makeFakeSpan() + const { integration } = build({ + tracerSpans: [workflowSpan], + config: { + llmobs: { + DD_LLMOBS_ENABLED: true, + mlApp: 'test', + sampleRate: 1, + }, + }, + }) + + integration.startTrace({ traceId: 't1' }) + + assert.strictEqual(LLMObsTagger.tagMap.get(workflowSpan)['_ml_obs.meta.span.kind'], 'workflow') + }) + }) + + describe('endTrace / #completeWorkflowSpan', () => { + it('does nothing when traceId is missing', () => { + const { integration } = build() + integration.endTrace({}) + }) + + it('does nothing when no span is mapped to the traceId', () => { + const { integration } = build() + integration.endTrace({ traceId: 'unknown' }) + }) + + it('applies a rootAgentSpan error with a message onto the workflow span', () => { + const workflowSpan = makeFakeSpan() + const { integration } = build({ tracerSpans: [workflowSpan, makeFakeSpan()] }) + integration.startTrace({ traceId: 't1' }) + integration.startSpan( + { spanId: 's1', traceId: 't1', parentId: null, spanData: { type: 'agent' } }, + 'agent' + ) + integration.endSpan({ + spanId: 's1', + traceId: 't1', + parentId: null, + spanData: { type: 'agent' }, + error: { message: 'oh no' }, + }) + sinon.assert.calledWith(workflowSpan.setTag, 'error', true) + sinon.assert.calledWith(workflowSpan.setTag, 'error.type', sinon.match.string) + sinon.assert.calledWith(workflowSpan.setTag, 'error.message', 'oh no') + sinon.assert.called(workflowSpan.finish) + }) + + it('still flags an error when rootAgentSpan.error has no message', () => { + const workflowSpan = makeFakeSpan() + const { integration } = build({ tracerSpans: [workflowSpan, makeFakeSpan()] }) + integration.startTrace({ traceId: 't1' }) + integration.startSpan( + { spanId: 's1', traceId: 't1', parentId: null, spanData: { type: 'agent' } }, + 'agent' + ) + integration.endSpan({ + spanId: 's1', + traceId: 't1', + parentId: null, + spanData: { type: 'agent' }, + error: {}, + }) + sinon.assert.calledWith(workflowSpan.setTag, 'error', true) + const messageCalls = workflowSpan.setTag.getCalls().filter(c => c.args[0] === 'error.message') + assert.strictEqual(messageCalls.length, 0) + }) + + it('keeps the workflow open when a successful top-level agent ends', () => { + const workflowSpan = makeFakeSpan() + const agentSpan = makeFakeSpan() + const { integration } = build({ tracerSpans: [workflowSpan, agentSpan] }) + integration.startTrace({ traceId: 't1' }) + integration.startSpan( + { spanId: 's1', traceId: 't1', parentId: null, spanData: { type: 'agent' } }, + 'agent' + ) + + integration.endSpan({ + spanId: 's1', + traceId: 't1', + parentId: null, + spanData: { type: 'agent' }, + }) + + sinon.assert.notCalled(workflowSpan.finish) + integration.endTrace({ traceId: 't1' }) + sinon.assert.calledOnce(workflowSpan.finish) + + integration.endTrace({ traceId: 't1' }) + sinon.assert.calledOnce(workflowSpan.finish) + }) + }) + + describe('startSpan', () => { + it('does nothing when spanId is missing', () => { + const { integration, tracer } = build() + integration.startSpan({}, 'agent') + sinon.assert.notCalled(tracer.startSpan) + }) + + it('defaults span.kind to internal for unknown LLMObs kinds', () => { + const { integration, tracer } = build() + integration.startSpan( + { spanId: 's1', traceId: 't1', spanData: { type: 'custom' } }, + 'unknown-kind' + ) + const tags = tracer.startSpan.firstCall.args[1].tags + assert.strictEqual(tags['span.kind'], 'internal') + }) + + it('maps llm kind to span.kind=client', () => { + const { integration, tracer } = build() + integration.startSpan( + { spanId: 's1', traceId: 't1', spanData: { type: 'response' } }, + 'llm' + ) + const tags = tracer.startSpan.firstCall.args[1].tags + assert.strictEqual(tags['span.kind'], 'client') + }) + + it('maps agent kind to span.kind=internal', () => { + const { integration, tracer } = build() + integration.startSpan( + { spanId: 's1', traceId: 't1', spanData: { type: 'agent' } }, + 'agent' + ) + const tags = tracer.startSpan.firstCall.args[1].tags + assert.strictEqual(tags['span.kind'], 'internal') + }) + }) + + describe('endSpan', () => { + it('does nothing when spanId is unknown', () => { + const { integration } = build() + integration.endSpan({ spanId: 'missing', spanData: { type: 'function' } }) + }) + + it('finishes the dd-trace span on end', () => { + const ddSpan = makeFakeSpan() + const { integration } = build({ tracerSpans: [ddSpan] }) + integration.startSpan( + { spanId: 's1', traceId: 't1', spanData: { type: 'function' } }, + 'tool' + ) + integration.endSpan({ spanId: 's1', spanData: { type: 'function' } }) + sinon.assert.called(ddSpan.finish) + }) + + it('renames handoff spans after the target agent is available', () => { + const ddSpan = makeFakeSpan() + const { integration } = build({ tracerSpans: [ddSpan] }) + const oaiSpan = { + spanId: 's1', + traceId: 't1', + spanData: { type: 'handoff', from_agent: 'agent_a' }, + } + integration.startSpan(oaiSpan, 'tool') + oaiSpan.spanData.to_agent = 'Agent B' + + integration.endSpan(oaiSpan) + + sinon.assert.calledWith(ddSpan.setOperationName, 'transfer_to_agent_b') + }) + }) + + describe('#resolveParent', () => { + it('uses the parent dd-trace span when parentId is mapped', () => { + const parentSpan = makeFakeSpan() + const childSpan = makeFakeSpan() + const { integration, tracer } = build({ tracerSpans: [parentSpan, childSpan] }) + integration.startSpan( + { spanId: 'p1', traceId: 't1', spanData: { type: 'agent' } }, + 'agent' + ) + integration.startSpan( + { spanId: 'c1', traceId: 't1', parentId: 'p1', spanData: { type: 'function' } }, + 'tool' + ) + assert.strictEqual(tracer.startSpan.secondCall.args[1].childOf, parentSpan) + }) + + it('falls back to the trace root span when parentId has no mapping', () => { + const root = makeFakeSpan() + const child = makeFakeSpan() + const { integration, tracer } = build({ tracerSpans: [root, child] }) + integration.startTrace({ traceId: 't1' }) + integration.startSpan( + { spanId: 's1', traceId: 't1', parentId: 'unknown-parent', spanData: { type: 'agent' } }, + 'agent' + ) + assert.strictEqual(tracer.startSpan.secondCall.args[1].childOf, root) + }) + + it('returns undefined when neither parent nor trace root is mapped', () => { + const orphan = makeFakeSpan() + const { integration, tracer } = build({ tracerSpans: [orphan] }) + integration.startSpan( + { spanId: 's1', traceId: 'no-trace', parentId: 'no-parent', spanData: { type: 'agent' } }, + 'agent' + ) + assert.strictEqual(tracer.startSpan.firstCall.args[1].childOf, undefined) + }) + }) + + describe('clearState', () => { + it('finishes every in-flight dd-trace span and clears bookkeeping', () => { + const workflow = makeFakeSpan() + const agentSpan = makeFakeSpan() + const { integration } = build({ tracerSpans: [workflow, agentSpan] }) + integration.startTrace({ traceId: 't1' }) + integration.startSpan( + { spanId: 's1', traceId: 't1', spanData: { type: 'agent' } }, + 'agent' + ) + integration.clearState() + sinon.assert.called(workflow.finish) + sinon.assert.called(agentSpan.finish) + // After clearState, a second endTrace should be a no-op because the + // span map was cleared. + integration.endTrace({ traceId: 't1' }) + }) + }) +}) diff --git a/packages/datadog-plugin-openai-agents/test/processor.spec.js b/packages/datadog-plugin-openai-agents/test/processor.spec.js new file mode 100644 index 0000000000..456c4c2107 --- /dev/null +++ b/packages/datadog-plugin-openai-agents/test/processor.spec.js @@ -0,0 +1,166 @@ +'use strict' + +const assert = require('node:assert/strict') +const sinon = require('sinon') + +const { DDOpenAIAgentsProcessor } = require('../src/processor') +const log = require('../../dd-trace/src/log') + +describe('DDOpenAIAgentsProcessor', () => { + let warnStub + + beforeEach(() => { + warnStub = sinon.stub(log, 'warn') + }) + + afterEach(() => { + sinon.restore() + }) + + function makeIntegration (overrides = {}) { + return { + enabled: true, + startTrace: sinon.stub(), + endTrace: sinon.stub(), + startSpan: sinon.stub(), + endSpan: sinon.stub(), + clearState: sinon.stub(), + ...overrides, + } + } + + describe('when no integration is registered', () => { + const processor = new DDOpenAIAgentsProcessor(() => undefined) + + it('returns a resolved promise from every lifecycle method without throwing', async () => { + await processor.onTraceStart({}) + await processor.onTraceEnd({}) + await processor.onSpanStart({ spanData: { type: 'agent' } }) + await processor.onSpanEnd({ spanData: { type: 'agent' } }) + await processor.forceFlush() + await processor.shutdown() + }) + + it('shutdown skips clearState when the integration is missing', async () => { + await processor.shutdown() + // No throw, nothing to assert beyond the absence of work. + }) + }) + + describe('when the integration is disabled', () => { + const integration = makeIntegration({ enabled: false }) + const processor = new DDOpenAIAgentsProcessor(() => integration) + + it('skips startTrace/endTrace/startSpan/endSpan', async () => { + await processor.onTraceStart({}) + await processor.onTraceEnd({}) + await processor.onSpanStart({ spanData: { type: 'agent' } }) + await processor.onSpanEnd({ spanData: { type: 'agent' } }) + sinon.assert.notCalled(integration.startTrace) + sinon.assert.notCalled(integration.endTrace) + sinon.assert.notCalled(integration.startSpan) + sinon.assert.notCalled(integration.endSpan) + }) + }) + + describe('onSpanStart guards', () => { + const integration = makeIntegration() + const processor = new DDOpenAIAgentsProcessor(() => integration) + + it('returns without calling startSpan when spanData is absent (NoopSpan guard)', async () => { + await processor.onSpanStart({}) + sinon.assert.notCalled(integration.startSpan) + }) + + it('returns without calling startSpan for span types that have no LLMObs kind', async () => { + for (const type of ['generation']) { + integration.startSpan.resetHistory() + await processor.onSpanStart({ spanData: { type } }) + sinon.assert.notCalled(integration.startSpan) + } + }) + + it('maps recognised span types to the expected LLMObs kind', async () => { + const cases = [ + ['agent', 'agent'], + ['function', 'tool'], + ['handoff', 'tool'], + ['guardrail', 'task'], + ['custom', 'task'], + ['response', 'llm'], + ] + for (const [type, expectedKind] of cases) { + integration.startSpan.resetHistory() + await processor.onSpanStart({ spanData: { type } }) + sinon.assert.calledOnce(integration.startSpan) + assert.strictEqual(integration.startSpan.firstCall.args[1], expectedKind) + } + }) + }) + + describe('onSpanEnd guards', () => { + const integration = makeIntegration() + const processor = new DDOpenAIAgentsProcessor(() => integration) + + it('returns without calling endSpan when spanData is absent', async () => { + await processor.onSpanEnd({}) + sinon.assert.notCalled(integration.endSpan) + }) + }) + + describe('error handling', () => { + const err = new Error('boom') + + function processorWithThrowing (method) { + const integration = makeIntegration() + integration[method] = sinon.stub().throws(err) + return [new DDOpenAIAgentsProcessor(() => integration), integration] + } + + it('logs and swallows startTrace failures', async () => { + const [p] = processorWithThrowing('startTrace') + await p.onTraceStart({}) + sinon.assert.calledOnce(warnStub) + }) + + it('logs and swallows endTrace failures', async () => { + const [p] = processorWithThrowing('endTrace') + await p.onTraceEnd({}) + sinon.assert.calledOnce(warnStub) + }) + + it('logs and swallows startSpan failures', async () => { + const [p] = processorWithThrowing('startSpan') + await p.onSpanStart({ spanData: { type: 'agent' } }) + sinon.assert.calledOnce(warnStub) + }) + + it('logs and swallows endSpan failures', async () => { + const [p] = processorWithThrowing('endSpan') + await p.onSpanEnd({ spanData: { type: 'agent' } }) + sinon.assert.calledOnce(warnStub) + }) + + it('logs and swallows shutdown clearState failures', async () => { + const [p] = processorWithThrowing('clearState') + await p.shutdown() + sinon.assert.calledOnce(warnStub) + }) + }) + + it('forceFlush resolves without doing work', async () => { + const processor = new DDOpenAIAgentsProcessor(() => makeIntegration()) + await processor.forceFlush() + }) + + it('reads the integration lazily on each lifecycle event', async () => { + let calls = 0 + const integrations = [makeIntegration(), makeIntegration()] + const processor = new DDOpenAIAgentsProcessor(() => integrations[calls++ % 2]) + + await processor.onTraceStart({}) + await processor.onTraceStart({}) + sinon.assert.calledOnce(integrations[0].startTrace) + sinon.assert.calledOnce(integrations[1].startTrace) + }) +}) diff --git a/packages/datadog-plugin-openai-agents/test/test-setup.js b/packages/datadog-plugin-openai-agents/test/test-setup.js new file mode 100644 index 0000000000..f160c8622e --- /dev/null +++ b/packages/datadog-plugin-openai-agents/test/test-setup.js @@ -0,0 +1,231 @@ +'use strict' + +const path = require('node:path') + +function createStreamResponse (status) { + return { + id: 'resp_test', + object: 'response', + created_at: 0, + status, + output: status === 'completed' + ? [{ + id: 'msg_test', + type: 'message', + status: 'completed', + role: 'assistant', + content: [{ type: 'output_text', text: 'hello', annotations: [] }], + }] + : [], + usage: status === 'completed' + ? { + input_tokens: 1, + output_tokens: 1, + total_tokens: 2, + input_tokens_details: { cached_tokens: 0 }, + output_tokens_details: { reasoning_tokens: 0 }, + } + : null, + model: 'gpt-4-0613', + parallel_tool_calls: true, + temperature: 1, + text: { format: { type: 'text' } }, + tool_choice: 'auto', + tools: [], + top_p: 1, + truncation: 'disabled', + metadata: {}, + } +} + +function createStreamEvent (event, data) { + return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` +} + +function createStreamingFetch () { + return async () => { + const responseStarted = createStreamResponse('in_progress') + const responseCompleted = createStreamResponse('completed') + const body = [ + createStreamEvent('response.created', { + type: 'response.created', + response: responseStarted, + sequence_number: 0, + }), + createStreamEvent('response.output_text.delta', { + type: 'response.output_text.delta', + content_index: 0, + delta: 'hello', + item_id: 'msg_test', + output_index: 0, + sequence_number: 1, + }), + createStreamEvent('response.completed', { + type: 'response.completed', + response: responseCompleted, + sequence_number: 2, + }), + 'data: [DONE]\n\n', + ].join('') + + return new Response(body, { + status: 200, + headers: { + 'content-type': 'text/event-stream', + 'x-request-id': 'req_test', + }, + }) + } +} + +function createResponseFetch () { + return async () => { + return new Response(JSON.stringify(createStreamResponse('completed')), { + status: 200, + headers: { + 'content-type': 'application/json', + 'x-request-id': 'req_test', + }, + }) + } +} + +function createHandoffFetch () { + let request = 0 + + return async () => { + const response = createStreamResponse('completed') + response.output = request++ === 0 + ? [{ + id: 'fc_handoff', + type: 'function_call', + call_id: 'call_handoff', + name: 'transfer_to_agent_b', + arguments: '{}', + status: 'completed', + }] + : [{ + id: 'msg_final', + type: 'message', + status: 'completed', + role: 'assistant', + content: [{ type: 'output_text', text: 'done', annotations: [] }], + }] + + return new Response(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + 'x-request-id': `req_handoff_${request}`, + }, + }) + } +} + +class OpenaiAgentsTestSetup { + async setup (clientModule, version) { + this.module = clientModule + + const agentsOpenaiDir = path.join(__dirname, '..', '..', '..', 'versions', '@openai', `agents-openai@${version}`) + const { OpenAIResponsesModel } = require(agentsOpenaiDir).get() + const openaiPath = require.resolve('openai', { + paths: [path.join(__dirname, '..', '..', '..', 'versions', 'node_modules', '@openai', 'agents-openai')], + }) + const { OpenAI } = require(openaiPath) + + const mockClient = new OpenAI({ + apiKey: 'test', + baseURL: 'https://api.openai.com/v1', + fetch: createResponseFetch(), + }) + + clientModule.setDefaultModelProvider({ + createModel: (modelName) => new OpenAIResponsesModel(mockClient, modelName), + }) + + const mockErrorClient = { + baseURL: 'https://api.openai.com/v1', + responses: { + create: async () => { + throw new Error('Intentional error for testing') + }, + }, + } + + const fakeModel = new OpenAIResponsesModel(mockClient, 'gpt-4') + const streamModel = new OpenAIResponsesModel(new OpenAI({ + apiKey: 'test', + baseURL: 'https://api.openai.com/v1', + fetch: createStreamingFetch(), + }), 'gpt-4') + const errorModel = new OpenAIResponsesModel(mockErrorClient, 'gpt-4') + const handoffModel = new OpenAIResponsesModel(new OpenAI({ + apiKey: 'test', + baseURL: 'https://api.openai.com/v1', + fetch: createHandoffFetch(), + }), 'gpt-4') + + this.agent = new clientModule.Agent({ + name: 'test_agent', + instructions: 'You are a test agent', + model: fakeModel, + }) + + this.streamAgent = new clientModule.Agent({ + name: 'test_agent', + instructions: 'You are a test agent', + model: streamModel, + }) + + this.errorAgent = new clientModule.Agent({ + name: 'error_agent', + instructions: 'You are an error test agent', + model: errorModel, + }) + + this.handoffAgentB = new clientModule.Agent({ + name: 'agent_b', + instructions: 'Finish the request', + model: handoffModel, + }) + this.handoffAgentA = new clientModule.Agent({ + name: 'agent_a', + instructions: 'Hand the request to agent_b', + model: handoffModel, + handoffs: [this.handoffAgentB], + }) + } + + async teardown () { + this.module = undefined + this.agent = undefined + this.streamAgent = undefined + this.errorAgent = undefined + this.handoffAgentA = undefined + this.handoffAgentB = undefined + } + + async run () { + return this.module.run(this.agent, 'hello', { maxTurns: 2 }) + } + + async runStreamed () { + const result = await this.module.run(this.streamAgent, 'hello', { maxTurns: 2, stream: true }) + for await (const event of result) { + // Drain the stream so the SDK finishes the underlying response span. + if (event === undefined) continue + } + await result.completed + return result + } + + async runError () { + return this.module.run(this.errorAgent, 'hello', { maxTurns: 1 }) + } + + async multiAgentHandoff () { + return this.module.run(this.handoffAgentA, 'start', { maxTurns: 2 }) + } +} + +module.exports = OpenaiAgentsTestSetup diff --git a/packages/dd-trace/src/config/generated-config-types.d.ts b/packages/dd-trace/src/config/generated-config-types.d.ts index ef991ae73e..8a386bd45d 100644 --- a/packages/dd-trace/src/config/generated-config-types.d.ts +++ b/packages/dd-trace/src/config/generated-config-types.d.ts @@ -333,6 +333,7 @@ export interface GeneratedConfig { DD_TRACE_NODE_SERIALIZE_ENABLED: boolean; DD_TRACE_NYC_ENABLED: boolean; DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP: string; + DD_TRACE_OPENAI_AGENTS_ENABLED: boolean; DD_TRACE_OPENAI_ENABLED: boolean; DD_TRACE_OPENSEARCH_ENABLED: boolean; DD_TRACE_OPENSEARCH_PROJECT_OPENSEARCH_ENABLED: boolean; @@ -1028,6 +1029,7 @@ export interface GeneratedEnvVarConfig { DD_TRACE_NODE_SERIALIZE_ENABLED: boolean; DD_TRACE_NYC_ENABLED: boolean; DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP: string; + DD_TRACE_OPENAI_AGENTS_ENABLED: boolean; DD_TRACE_OPENAI_ENABLED: boolean; DD_TRACE_OPENSEARCH_ENABLED: boolean; DD_TRACE_OPENSEARCH_PROJECT_OPENSEARCH_ENABLED: boolean; diff --git a/packages/dd-trace/src/config/supported-configurations.json b/packages/dd-trace/src/config/supported-configurations.json index 30d6866b1d..ad34c141bf 100644 --- a/packages/dd-trace/src/config/supported-configurations.json +++ b/packages/dd-trace/src/config/supported-configurations.json @@ -3452,6 +3452,13 @@ "default": "true" } ], + "DD_TRACE_OPENAI_AGENTS_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "true" + } + ], "DD_TRACE_OPENSEARCH_ENABLED": [ { "implementation": "A", diff --git a/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js b/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js new file mode 100644 index 0000000000..e55d75d657 --- /dev/null +++ b/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js @@ -0,0 +1,211 @@ +'use strict' + +/** + * Extracts input messages for an LLM span. agents-openai stores only + * `request.input` on `spanData._input` (string or message-array), and the + * system instructions are echoed back on the response as `instructions`. + * + * @param {string|Array} input - The raw `request.input` (`spanData._input`). + * @param {string} [instructions] - System instructions echoed on `response.instructions`. + * @returns {Array<{ role: string, content: string }>} + */ +function extractInputMessages (input, instructions) { + const messages = [] + + if (instructions) { + messages.push({ role: 'system', content: instructions }) + } + + if (typeof input === 'string') { + messages.push({ role: 'user', content: input }) + } else if (Array.isArray(input)) { + for (const item of input) { + if (item.type === 'message') { + const role = item.role + if (!role) continue + + let content = '' + if (Array.isArray(item.content)) { + const textParts = item.content + .filter(c => c.type === 'input_text' || c.type === 'text') + .map(c => c.text) + content = textParts.join('') + } else if (typeof item.content === 'string') { + content = item.content + } + + if (content) { + messages.push({ role, content }) + } + } else if (item.type === 'function_call') { + let args = item.arguments + if (typeof args === 'string') { + try { + args = JSON.parse(args) + } catch { + args = {} + } + } + messages.push({ + role: 'assistant', + toolCalls: [{ + toolId: item.call_id, + name: item.name, + arguments: args, + type: item.type, + }], + }) + } else if (item.type === 'function_call_output') { + messages.push({ + role: 'user', + toolResults: [{ + toolId: item.call_id, + result: item.output, + name: item.name || '', + type: item.type, + }], + }) + } + } + } + + return messages.length > 0 ? messages : [{ role: 'user', content: '' }] +} + +/** + * Extracts output messages from the model response. + * + * @param {{ output?: Array }} result - The model response + * @returns {Array<{ role: string, content: string }>} + */ +function extractOutputMessages (result) { + const messages = [] + + if (result?.output) { + for (const item of result.output) { + if (item.type === 'message') { + let content = '' + if (Array.isArray(item.content)) { + const textParts = item.content + .filter(c => c.type === 'output_text') + .map(c => c.text) + content = textParts.join('') + } else if (typeof item.content === 'string') { + content = item.content + } + + messages.push({ role: item.role || 'assistant', content }) + } else if (item.type === 'function_call') { + let args = item.arguments + if (typeof args === 'string') { + try { + args = JSON.parse(args) + } catch { + args = {} + } + } + messages.push({ + role: 'assistant', + toolCalls: [{ + toolId: item.call_id, + name: item.name, + arguments: args, + type: item.type, + }], + }) + } + } + } + + return messages.length > 0 ? messages : [{ content: '', role: '' }] +} + +/** + * Extracts token usage metrics from the model response. Returns `undefined` + * when there's nothing to tag, so callers can skip the tagger call without + * allocating an Object.keys array. + * + * @param {{ usage?: { inputTokens?: number, outputTokens?: number, totalTokens?: number, + * outputTokensDetails?: { reasoningTokens?: number } } }} result + * @returns {{ inputTokens?: number, outputTokens?: number, totalTokens?: number, + * reasoningOutputTokens?: number } | undefined} + */ +function extractMetrics (result) { + const usage = result?.usage + if (!usage) return + + const inputTokens = usage.inputTokens ?? usage.input_tokens + const outputTokens = usage.outputTokens ?? usage.output_tokens + const totalTokens = usage.totalTokens ?? usage.total_tokens + const reasoningTokens = usage.outputTokensDetails?.reasoningTokens ?? + usage.output_tokens_details?.reasoning_tokens + + if (inputTokens === undefined && outputTokens === undefined && + totalTokens === undefined && !reasoningTokens) return + + const metrics = {} + if (inputTokens !== undefined) metrics.inputTokens = inputTokens + if (outputTokens !== undefined) metrics.outputTokens = outputTokens + // Tagger maps `reasoningOutputTokens` → `reasoning_output_tokens` in the + // LLMObs span event. Skip when zero — emitting a zero just adds noise. + if (reasoningTokens) metrics.reasoningOutputTokens = reasoningTokens + + if (totalTokens !== undefined) { + metrics.totalTokens = totalTokens + } else if (metrics.inputTokens !== undefined && metrics.outputTokens !== undefined) { + metrics.totalTokens = metrics.inputTokens + metrics.outputTokens + } + + return metrics +} + +// Fields the OpenAI Responses API echoes back from the request configuration. +// agents-openai only stores `request.input` on the span — the user's +// `modelSettings` aren't directly observable, so we read the response-echoed +// values. Matches dd-trace-py's openai-agents integration (see +// `OaiSpanAdapter.llmobs_metadata`); both ship without filtering OpenAI's +// default values. +const RESPONSE_METADATA_FIELDS = [ + 'temperature', + 'max_output_tokens', + 'top_p', + 'tools', + 'tool_choice', + 'truncation', +] + +/** + * Extracts metadata from the model response. Mirrors Python's + * `OaiSpanAdapter.llmobs_metadata` — emits all response-echoed configuration + * fields plus `text` when present. Returns `undefined` when nothing was + * captured, so callers can skip the tagger call without allocating. + * + * @param {object | undefined} response + * @returns {object | undefined} + */ +function extractMetadata (response) { + if (!response) return + + let metadata + for (const field of RESPONSE_METADATA_FIELDS) { + const value = response[field] + if (value !== undefined && value !== null) { + metadata ??= {} + metadata[field] = value + } + } + + if (response.text) { + metadata ??= {} + metadata.text = response.text + } + + return metadata +} + +module.exports = { + extractInputMessages, + extractOutputMessages, + extractMetrics, + extractMetadata, +} diff --git a/packages/dd-trace/src/llmobs/plugins/openai/index.js b/packages/dd-trace/src/llmobs/plugins/openai/index.js index 9dc881d022..090a36ae0b 100644 --- a/packages/dd-trace/src/llmobs/plugins/openai/index.js +++ b/packages/dd-trace/src/llmobs/plugins/openai/index.js @@ -5,7 +5,6 @@ const { PROMPT_TRACKING_INSTRUMENTATION_METHOD, PROMPT_MULTIMODAL, INSTRUMENTATION_METHOD_AUTO, - UNKNOWN_MODEL_PROVIDER, } = require('../../constants/tags') const { audioMimeTypeFromFormat, formatAudioPart, safeJsonParse } = require('../../util') const { AUDIO_MIME_TYPES } = require('./constants') @@ -15,6 +14,7 @@ const { extractTextFromContentItem, extractContentParts, hasMultimodalInputs, + getOpenAIModelProvider, } = require('./utils') const allowedParamKeys = new Set([ @@ -108,14 +108,10 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin { } _getModelProviderAndClient (baseUrl = '') { - if (baseUrl.includes('azure')) { - return { modelProvider: 'azure_openai', client: 'AzureOpenAI' } - } else if (baseUrl.includes('openai')) { - return { modelProvider: 'openai', client: 'OpenAI' } - } else if (baseUrl.includes('deepseek')) { - return { modelProvider: 'deepseek', client: 'DeepSeek' } - } - return { modelProvider: UNKNOWN_MODEL_PROVIDER, client: 'OpenAI' } + const modelProvider = getOpenAIModelProvider(baseUrl) + if (modelProvider === 'azure_openai') return { modelProvider, client: 'AzureOpenAI' } + if (modelProvider === 'deepseek') return { modelProvider, client: 'DeepSeek' } + return { modelProvider, client: 'OpenAI' } } _extractMetrics (response) { diff --git a/packages/dd-trace/src/llmobs/plugins/openai/utils.js b/packages/dd-trace/src/llmobs/plugins/openai/utils.js index bb0014ba0c..17a10a12cc 100644 --- a/packages/dd-trace/src/llmobs/plugins/openai/utils.js +++ b/packages/dd-trace/src/llmobs/plugins/openai/utils.js @@ -1,5 +1,6 @@ 'use strict' +const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags') const { audioMimeTypeFromFormat, formatAudioPart } = require('../../util') const { INPUT_TYPE_IMAGE, @@ -160,10 +161,29 @@ function extractContentParts (parts) { return { content: extracted.join('\n'), audioParts } } +/** + * Maps an OpenAI-compatible base URL to a model provider string. Covers + * OpenAI, Azure OpenAI, and DeepSeek; falls back to UNKNOWN_MODEL_PROVIDER + * for unrecognised hosts (e.g. local proxies or custom deployments). + * + * Shared with the openai-agents integration since both consume the same + * client baseURL convention. + * + * @param {string} baseUrl + * @returns {string} + */ +function getOpenAIModelProvider (baseUrl = '') { + if (baseUrl.includes('azure')) return 'azure_openai' + if (baseUrl.includes('deepseek')) return 'deepseek' + if (baseUrl.includes('openai')) return 'openai' + return UNKNOWN_MODEL_PROVIDER +} + module.exports = { extractChatTemplateFromInstructions, normalizePromptVariables, extractTextFromContentItem, extractContentParts, hasMultimodalInputs, + getOpenAIModelProvider, } diff --git a/packages/dd-trace/src/llmobs/tagger.js b/packages/dd-trace/src/llmobs/tagger.js index 6bdc55c9cd..735dc24f3c 100644 --- a/packages/dd-trace/src/llmobs/tagger.js +++ b/packages/dd-trace/src/llmobs/tagger.js @@ -500,6 +500,10 @@ class LLMObsTagger { this._setTag(span, MODEL_NAME, modelName) } + setName (span, name) { + this._setTag(span, NAME, name) + } + #tagText (span, data, key) { if (data) { if (typeof data === 'string') { diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index b91919fcc4..591df8171d 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -37,6 +37,7 @@ const plugins = { get '@smithy/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') }, get '@vitest/runner' () { return require('../../../datadog-plugin-vitest/src') }, get '@langchain/langgraph' () { return require('../../../datadog-plugin-langgraph/src') }, + get '@openai/agents' () { return require('../../../datadog-plugin-openai-agents/src') }, get aerospike () { return require('../../../datadog-plugin-aerospike/src') }, get ai () { return require('../../../datadog-plugin-ai/src') }, get amqp10 () { return require('../../../datadog-plugin-amqp10/src') }, diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_1e503f45.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_1e503f45.yaml new file mode 100644 index 0000000000..6a76342ba2 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_1e503f45.yaml @@ -0,0 +1,141 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","instructions":"You are a calculator. Use the `add` + tool to answer math questions.","input":[{"role":"user","content":"What is the + sum of 1 and 2?"},{"id":"fc_0e5478180e8a08c50169f3fd16bb14819289e3160e1b1402a3","type":"function_call","name":"add","call_id":"call_CEwtyjHD7Mwsy0csHUiMkMQv","arguments":"{\"a\":1,\"b\":2}","status":"completed"},{"type":"function_call_output","call_id":"call_CEwtyjHD7Mwsy0csHUiMkMQv","output":"An + error occurred while running the tool. Please try again. Error: Error: Intentional + error for testing","status":"completed"}],"include":[],"tools":[{"type":"function","name":"add","description":"Adds + two numbers and returns the result.","parameters":{"type":"object","properties":{"a":{"type":"number","description":"First + number"},"b":{"type":"number","description":"Second number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"stream":false}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '918' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.35.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0e5478180e8a08c50169f3fd17ad78819292298277c9d1a36c\",\n + \ \"object\": \"response\",\n \"created_at\": 1777597719,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1777597720,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + \"You are a calculator. Use the `add` tool to answer math questions.\",\n + \ \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n + \ \"id\": \"fc_0e5478180e8a08c50169f3fd18cadc8192894e035a52eb5dc1\",\n + \ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\": + \"{\\\"a\\\":1,\\\"b\\\":2}\",\n \"call_id\": \"call_Afc0VH4AMnChHMC1JJyv65rH\",\n + \ \"name\": \"add\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": + 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": + \"in_memory\",\n \"reasoning\": {\n \"effort\": null,\n \"summary\": + null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n + \ \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": + {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n + \ \"tool_choice\": \"auto\",\n \"tools\": [\n {\n \"type\": \"function\",\n + \ \"description\": \"Adds two numbers and returns the result.\",\n \"name\": + \"add\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": + {\n \"a\": {\n \"type\": \"number\",\n \"description\": + \"First number\"\n },\n \"b\": {\n \"type\": + \"number\",\n \"description\": \"Second number\"\n }\n + \ },\n \"required\": [\n \"a\",\n \"b\"\n ],\n + \ \"additionalProperties\": false\n },\n \"strict\": true\n + \ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 123,\n \"input_tokens_details\": + {\n \"cached_tokens\": 0\n },\n \"output_tokens\": 18,\n \"output_tokens_details\": + {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 141\n },\n + \ \"user\": null,\n \"metadata\": {}\n}" + headers: + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 9f4ae5703ed68153-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 01 May 2026 01:08:41 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - datadog-4 + openai-processing-ms: + - '1313' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=3qywEquAgFctMZKPbXBF3t159B0cHO0NF4f44SCggG8-1777597719.0729408-1.0.1.1-nUgXWX7rRQySHN8fZLgAS5ijYxXyFCCWS6AyyTcqYuwdqGBgTkHEDoIiwSXmqv4TMJaSSBHln_DSEYfFiDvcq08aN3O3i2n3VojZj7brVdhZD81olz6i94BhZBW8UbP_; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 01 May 2026 + 01:38:41 GMT + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999660' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_bc2f4349a43846e29510af2993044c5c + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_25ba1894.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_25ba1894.yaml new file mode 100644 index 0000000000..e6c67fee82 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_25ba1894.yaml @@ -0,0 +1,186 @@ +interactions: +- request: + body: '{"model":"gpt-4","instructions":"test","input":[{"role":"user","content":"hello"}],"include":[],"tools":[],"stream":true,"text":{}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '131' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: 'event: response.created + + data: {"type":"response.created","response":{"id":"resp_04364b9e39a803600169c40e9dea74819cab03f85e6a862127","object":"response","created_at":1774456477,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"test","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4-0613","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + + event: response.in_progress + + data: {"type":"response.in_progress","response":{"id":"resp_04364b9e39a803600169c40e9dea74819cab03f85e6a862127","object":"response","created_at":1774456477,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"test","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4-0613","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + + event: response.output_item.added + + data: {"type":"response.output_item.added","item":{"id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + + event: response.content_part.added + + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"Hi","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"921dZ2mEE6LZDA","output_index":0,"sequence_number":4} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" there","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"7QMX9nwF2j","output_index":0,"sequence_number":5} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"IxnFnRnjHJpz7Qs","output_index":0,"sequence_number":6} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" How","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"HvCNSRjhvKOU","output_index":0,"sequence_number":7} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"jNvIDOTIqokT","output_index":0,"sequence_number":8} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"zb8n4Pp9a8iWtd","output_index":0,"sequence_number":9} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" assist","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"HRfpn6X9W","output_index":0,"sequence_number":10} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"ldsN4a6x1CGU","output_index":0,"sequence_number":11} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" today","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"TGXL09FzwC","output_index":0,"sequence_number":12} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"?","item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"obfuscation":"81zFFzsVHJw0aj6","output_index":0,"sequence_number":13} + + + event: response.output_text.done + + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","logprobs":[],"output_index":0,"sequence_number":14,"text":"Hi + there! How can I assist you today?"} + + + event: response.content_part.done + + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi + there! How can I assist you today?"},"sequence_number":15} + + + event: response.output_item.done + + data: {"type":"response.output_item.done","item":{"id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi + there! How can I assist you today?"}],"role":"assistant"},"output_index":0,"sequence_number":16} + + + event: response.completed + + data: {"type":"response.completed","response":{"id":"resp_04364b9e39a803600169c40e9dea74819cab03f85e6a862127","object":"response","created_at":1774456477,"status":"completed","background":false,"completed_at":1774456479,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"test","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4-0613","output":[{"id":"msg_04364b9e39a803600169c40e9ec1f8819cb92f585e552e937b","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi + there! How can I assist you today?"}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":12,"input_tokens_details":{"cached_tokens":0},"output_tokens":12,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":24},"user":null,"metadata":{}},"sequence_number":17} + + + ' + headers: + CF-RAY: + - 9e1f52f8eba982e6-IAD + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Wed, 25 Mar 2026 16:34:38 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '131' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=QE4xJnx_UuWjrZsfnzrnDkVoPNG8JSQeq11BEXIH6ko-1774456477.5905411-1.0.1.1-FDD0G9GLnPVbb0pZaO9tsmFSRhHRnIFbpbxA9MU7frPUuWGVu9.9JWBT1flHGaPFOfLASf3Ew0hB9lJvBgVjx5SeSWjD3VEOIMefLLBM7O3CYK58zRk._gWPP67EtY_7; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 17:04:38 GMT + x-request-id: + - req_133cb0502a414fe4b340eab3d2380cf0 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_5004ba66.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_5004ba66.yaml new file mode 100644 index 0000000000..fd03029832 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_5004ba66.yaml @@ -0,0 +1,139 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","instructions":"You are a calculator. Use the `add` + tool to answer math questions.","input":[{"role":"user","content":"What is the + sum of 1 and 2?"}],"include":[],"tools":[{"type":"function","name":"add","description":"Adds + two numbers and returns the result.","parameters":{"type":"object","properties":{"a":{"type":"number","description":"First + number"},"b":{"type":"number","description":"Second number"}},"required":["a","b"],"additionalProperties":false},"strict":true}],"stream":false}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '514' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.35.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0e5478180e8a08c50169f3fd12eab08192b02045d3c423e5c5\",\n + \ \"object\": \"response\",\n \"created_at\": 1777597714,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1777597718,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + \"You are a calculator. Use the `add` tool to answer math questions.\",\n + \ \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n + \ \"id\": \"fc_0e5478180e8a08c50169f3fd16bb14819289e3160e1b1402a3\",\n + \ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\": + \"{\\\"a\\\":1,\\\"b\\\":2}\",\n \"call_id\": \"call_CEwtyjHD7Mwsy0csHUiMkMQv\",\n + \ \"name\": \"add\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": + 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": + \"in_memory\",\n \"reasoning\": {\n \"effort\": null,\n \"summary\": + null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n + \ \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": + {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n + \ \"tool_choice\": \"auto\",\n \"tools\": [\n {\n \"type\": \"function\",\n + \ \"description\": \"Adds two numbers and returns the result.\",\n \"name\": + \"add\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": + {\n \"a\": {\n \"type\": \"number\",\n \"description\": + \"First number\"\n },\n \"b\": {\n \"type\": + \"number\",\n \"description\": \"Second number\"\n }\n + \ },\n \"required\": [\n \"a\",\n \"b\"\n ],\n + \ \"additionalProperties\": false\n },\n \"strict\": true\n + \ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 80,\n \"input_tokens_details\": + {\n \"cached_tokens\": 0\n },\n \"output_tokens\": 18,\n \"output_tokens_details\": + {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 98\n },\n + \ \"user\": null,\n \"metadata\": {}\n}" + headers: + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 9f4ae5527a1d25d8-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 01 May 2026 01:08:39 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + openai-organization: + - datadog-4 + openai-processing-ms: + - '3988' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=eKYElRGqGQuaIGgnStQcJEw31vLMZn4PZad3KU7oBFQ-1777597714.318872-1.0.1.1-OC7_nxkB71fXXwdAgSJ7du1KAnZgOkZlt_dr6gr2KKHsJuQ6COdufSB6Lqo7EdJcliLD7EcQ1MdovcZA2bCvGouJVIQmwCwURA_OJD5an3vkDwLJ8t5HCsUgi3becFOz; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 01 May 2026 + 01:38:39 GMT + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999702' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_8924f1d298ae4a628f5879914814ef61 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_56b13001.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_56b13001.yaml new file mode 100644 index 0000000000..0839016819 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_56b13001.yaml @@ -0,0 +1,130 @@ +interactions: +- request: + body: '{"model":"gpt-4","instructions":"You are helpful","input":[{"role":"user","content":[{"type":"input_text","text":"Tell + me a joke"}]}],"include":[],"tools":[],"temperature":0.5,"max_output_tokens":100,"stream":false,"text":{}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '225' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0cc2e93f0acc417d0169c44b4b1c9c819f8b0f9f6f38cf423b\",\n + \ \"object\": \"response\",\n \"created_at\": 1774472011,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1774472012,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + \"You are helpful\",\n \"max_output_tokens\": 100,\n \"max_tool_calls\": + null,\n \"model\": \"gpt-4-0613\",\n \"output\": [\n {\n \"id\": + \"msg_0cc2e93f0acc417d0169c44b4be34c819fb36771e8ff347e66\",\n \"type\": + \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n + \ \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": + [],\n \"text\": \"Sure, here's a classic for you:\\n\\nWhy don't + scientists trust atoms?\\n\\nBecause they make up everything!\"\n }\n + \ ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 0.5,\n \"text\": {\n + \ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": + 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 17,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 24,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 41\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9e20ce339b321c3f-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 25 Mar 2026 20:53:32 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '1362' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=l3.ko7zdIzqrA9_kI24BWLarfeizSkQfqjwWzwkqjTY-1774472010.8202507-1.0.1.1-aeaQcc60yFR4V6alqgThW7sJzgIvRr2GSei3Q4bMFRIiHi4A8KCWsUX9HlbkWWnJmcrJbS7hgGBnFCnfxYgpYp3mMwZe1ErzJvvM9sFOLgvVsa7aI_ftpJPt79WCSIdy; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 21:23:32 GMT + x-ratelimit-limit-requests: + - '1000' + x-ratelimit-limit-tokens: + - '600000' + x-ratelimit-remaining-requests: + - '999' + x-ratelimit-remaining-tokens: + - '599978' + x-ratelimit-reset-requests: + - 60ms + x-ratelimit-reset-tokens: + - 2ms + x-request-id: + - req_53f8b65db0cb4c68ae6ea6b53de4cc95 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_7bac988c.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_7bac988c.yaml new file mode 100644 index 0000000000..5410e8d0f0 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_7bac988c.yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: '{"model":"gpt-4","input":[{"role":"user","content":"What is 2 + 2?"}],"include":[],"tools":[],"stream":false,"text":{}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '119' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0227bf7009346efa0169c44b49bfe881978675058684f5120e\",\n + \ \"object\": \"response\",\n \"created_at\": 1774472009,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1774472010,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4-0613\",\n \"output\": [\n {\n \"id\": \"msg_0227bf7009346efa0169c44b4a7a7c8197accfbc837de21952\",\n + \ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": + [\n {\n \"type\": \"output_text\",\n \"annotations\": + [],\n \"logprobs\": [],\n \"text\": \"2 + 2 equals 4.\"\n + \ }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n + \ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": + 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 14,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 10,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 24\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9e20ce2adcc15e4b-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 25 Mar 2026 20:53:30 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '901' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=GJqc1QysrbFDjJV7M1HquOcXVnmZz5QLIfqMqcT.2Tw-1774472009.42027-1.0.1.1-bmP6nuJyYMTc20ws9loL9I0e8.Kt2E8RUQ_S9FB3YS1lm.0qNP53HR0clNRWJeW.oeyvfrdLm4EpRrSzo6kpht9pg3c0u0Ezpt7v4yePWzbWyH6YUsZw6fMC.hzLsWNn; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 21:23:30 GMT + x-ratelimit-limit-requests: + - '1000' + x-ratelimit-limit-tokens: + - '600000' + x-ratelimit-remaining-requests: + - '999' + x-ratelimit-remaining-tokens: + - '599981' + x-ratelimit-reset-requests: + - 60ms + x-ratelimit-reset-tokens: + - 1ms + x-request-id: + - req_cad034c549554445be66626c5d40cc18 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_9ff3470b.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_9ff3470b.yaml new file mode 100644 index 0000000000..f29c968e62 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_9ff3470b.yaml @@ -0,0 +1,284 @@ +interactions: +- request: + body: '{"model":"gpt-4","instructions":"You are helpful","input":[{"role":"user","content":"Stream + this"}],"include":[],"tools":[],"temperature":0.3,"stream":true,"text":{}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '166' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: 'event: response.created + + data: {"type":"response.created","response":{"id":"resp_011293af9477a3470169c44b4cbe6081959108ab2a2613e20e","object":"response","created_at":1774472012,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You + are helpful","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4-0613","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":0.3,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + + event: response.in_progress + + data: {"type":"response.in_progress","response":{"id":"resp_011293af9477a3470169c44b4cbe6081959108ab2a2613e20e","object":"response","created_at":1774472012,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You + are helpful","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4-0613","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":0.3,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + + event: response.output_item.added + + data: {"type":"response.output_item.added","item":{"id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + + event: response.content_part.added + + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"As","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"hlXBzvxREVSvYv","output_index":0,"sequence_number":4} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" an","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"MQuL1ybPMlt7c","output_index":0,"sequence_number":5} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" AI","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"4VRvQEFyKcsme","output_index":0,"sequence_number":6} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"YeEBrzcwweQKUyj","output_index":0,"sequence_number":7} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"sXevK5nZhj7eFR","output_index":0,"sequence_number":8} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"''m","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"n8bbBBE6vB2u8C","output_index":0,"sequence_number":9} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" unable","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"3aF2HZOkD","output_index":0,"sequence_number":10} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"JI7ZGROCIWtWO","output_index":0,"sequence_number":11} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" stream","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"S7GsT4L9Z","output_index":0,"sequence_number":12} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" content","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"lWJlUROL","output_index":0,"sequence_number":13} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"VgksSOpuuRh7vNK","output_index":0,"sequence_number":14} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" However","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"qmlMvQLv","output_index":0,"sequence_number":15} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"IvzpAvtp5Ts2dDK","output_index":0,"sequence_number":16} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"WzawlAD7ddnbJk","output_index":0,"sequence_number":17} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"''m","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"BPJuf3Tv1AVwhW","output_index":0,"sequence_number":18} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" here","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"Za5jJP8omja","output_index":0,"sequence_number":19} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" to","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"rMRgBWrNoMAGz","output_index":0,"sequence_number":20} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" assist","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"qMJsbItOz","output_index":0,"sequence_number":21} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"oA7fuSJzKkhy","output_index":0,"sequence_number":22} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" with","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"SN7uVIz92EF","output_index":0,"sequence_number":23} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" any","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"ldl0fIWk0vqj","output_index":0,"sequence_number":24} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" questions","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"KGU4U1","output_index":0,"sequence_number":25} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" or","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"ZeKvwlHfbFCx8","output_index":0,"sequence_number":26} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" tasks","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"aGXYPegVfm","output_index":0,"sequence_number":27} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"AlqUQSG7Aemd","output_index":0,"sequence_number":28} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" may","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"QzJRcpNsPshM","output_index":0,"sequence_number":29} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" have","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"P9xqSYG214k","output_index":0,"sequence_number":30} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"obfuscation":"O5zuNAAnNaSdIOd","output_index":0,"sequence_number":31} + + + event: response.output_text.done + + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","logprobs":[],"output_index":0,"sequence_number":32,"text":"As + an AI, I''m unable to stream content. However, I''m here to assist you with + any questions or tasks you may have."} + + + event: response.content_part.done + + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"As + an AI, I''m unable to stream content. However, I''m here to assist you with + any questions or tasks you may have."},"sequence_number":33} + + + event: response.output_item.done + + data: {"type":"response.output_item.done","item":{"id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"As + an AI, I''m unable to stream content. However, I''m here to assist you with + any questions or tasks you may have."}],"role":"assistant"},"output_index":0,"sequence_number":34} + + + event: response.completed + + data: {"type":"response.completed","response":{"id":"resp_011293af9477a3470169c44b4cbe6081959108ab2a2613e20e","object":"response","created_at":1774472012,"status":"completed","background":false,"completed_at":1774472013,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You + are helpful","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4-0613","output":[{"id":"msg_011293af9477a3470169c44b4d45dc8195b67972956b2b3583","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"As + an AI, I''m unable to stream content. However, I''m here to assist you with + any questions or tasks you may have."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":0.3,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":15,"input_tokens_details":{"cached_tokens":0},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":45},"user":null,"metadata":{}},"sequence_number":35} + + + ' + headers: + CF-RAY: + - 9e20ce3f5acb0c9c-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Wed, 25 Mar 2026 20:53:32 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '68' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=AZvfdsnqzjzcad66svqkkltb4dd4q8Te2sRxTYPJ5rk-1774472012.6990771-1.0.1.1-A8TJ9VYbkJiD1X9ao7noPk.31qLwZdH9G4Flne8nbftzMfw38eHaQD8lEl0jddteORsLvGLkti.5I.YoCJdSBHjBSzA3ZffMmFeOaryI5FcYjX_AFSNgjkEE2_EipcEw; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 21:23:32 GMT + x-request-id: + - req_eabb26f8d15646ddb17c085b172a3109 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_af61e856.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_af61e856.yaml new file mode 100644 index 0000000000..150b05796d --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_af61e856.yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: '{"model":"gpt-4","instructions":"test","input":[{"role":"user","content":"hello"}],"include":[],"tools":[],"stream":false,"text":{}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '132' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0a2893d9090f470e0169c40e98f744819e9e362f0925c20494\",\n + \ \"object\": \"response\",\n \"created_at\": 1774456472,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1774456474,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + \"test\",\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4-0613\",\n \"output\": [\n {\n \"id\": \"msg_0a2893d9090f470e0169c40e99df8c819e853abe8dd2018e97\",\n + \ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": + [\n {\n \"type\": \"output_text\",\n \"annotations\": + [],\n \"logprobs\": [],\n \"text\": \"Hello! How can I assist + you today?\"\n }\n ],\n \"role\": \"assistant\"\n }\n + \ ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": + null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": null,\n + \ \"reasoning\": {\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": + null,\n \"service_tier\": \"default\",\n \"store\": false,\n \"temperature\": + 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n + \ \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tools\": + [],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n + \ \"usage\": {\n \"input_tokens\": 12,\n \"input_tokens_details\": {\n + \ \"cached_tokens\": 0\n },\n \"output_tokens\": 11,\n \"output_tokens_details\": + {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 23\n },\n + \ \"user\": null,\n \"metadata\": {}\n}" + headers: + CF-RAY: + - 9e1f52d9fa372015-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 25 Mar 2026 16:34:34 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '1461' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=jbKJfvehx8H0GEEr.pXbENzMx_qVnc1.265VVOjJSQY-1774456472.6321006-1.0.1.1-gqyisrxOzd5I7ZonPoFnQ1E9Zl5f96sfrC017yMx_OWB9CuuYPMRjapXbvN7t8yz5E9Wn73QhQLQ9Q5sR1oLoi0LPZ7K1ERONUBYBpfOQmG2VWNVfZP.7TgsTejaqYVG; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 17:04:34 GMT + x-ratelimit-limit-requests: + - '1000' + x-ratelimit-limit-tokens: + - '600000' + x-ratelimit-remaining-requests: + - '999' + x-ratelimit-remaining-tokens: + - '599983' + x-ratelimit-reset-requests: + - 60ms + x-ratelimit-reset-tokens: + - 1ms + x-request-id: + - req_89fc716437ad4150a07d68e16851cfb5 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c7d7528a.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c7d7528a.yaml new file mode 100644 index 0000000000..1188b57f43 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c7d7528a.yaml @@ -0,0 +1,128 @@ +interactions: +- request: + body: '{"model":"gpt-4","instructions":"You are a helpful assistant","input":[{"role":"user","content":"Hello"}],"include":[],"tools":[],"temperature":0.7,"stream":false,"text":{}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '173' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_052e1e648197b69a0169c44b4864f481968cbb4090817deebe\",\n + \ \"object\": \"response\",\n \"created_at\": 1774472008,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1774472009,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + \"You are a helpful assistant\",\n \"max_output_tokens\": null,\n \"max_tool_calls\": + null,\n \"model\": \"gpt-4-0613\",\n \"output\": [\n {\n \"id\": + \"msg_052e1e648197b69a0169c44b490e58819683edf0147b11f20a\",\n \"type\": + \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n + \ \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": + [],\n \"text\": \"Hello! How can I assist you today?\"\n }\n + \ ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 0.7,\n \"text\": {\n + \ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": + 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 16,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 11,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 27\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9e20ce225fb6f21e-EWR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 25 Mar 2026 20:53:29 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '849' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=MaqyGhqWWz99H7isjTwek6F.xhWEvm30A22qLseJTVM-1774472008.054801-1.0.1.1-j8Z8EWycMIEwe9X37KiR.LsfxYAvpTAysXsVfPrKMFPhPY81RDcOu6tTby_EwLVXeFNoqacQMFBAe1f40Je27.VzE0FjotAxAK8AM1DkEVOkv7.q1HXbD5DY6j5Mlu12; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 21:23:29 GMT + x-ratelimit-limit-requests: + - '1000' + x-ratelimit-limit-tokens: + - '600000' + x-ratelimit-remaining-requests: + - '999' + x-ratelimit-remaining-tokens: + - '599979' + x-ratelimit-reset-requests: + - 60ms + x-ratelimit-reset-tokens: + - 2ms + x-request-id: + - req_0f581b4e674c4742a6bc9cca09916ca9 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_cecd80bb.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_cecd80bb.yaml new file mode 100644 index 0000000000..47d141a330 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_cecd80bb.yaml @@ -0,0 +1,128 @@ +interactions: +- request: + body: '{"model":"gpt-4","instructions":"You are a test agent","input":[{"role":"user","content":"hello"}],"include":[],"tools":[],"stream":false}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '138' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - Agents/JavaScript 0.7.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Arch + : - arm64 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Lang + : - js + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-OS + : - MacOS + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Package-Version + : - 6.32.0 + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Retry-Count + : - '0' + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime + : - node + ? !!python/object/apply:multidict._multidict.istr + - X-Stainless-Runtime-Version + : - v25.2.1 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0bc715f27ef7a52b0169c41cdb729481a1a267646efc815202\",\n + \ \"object\": \"response\",\n \"created_at\": 1774460123,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1774460124,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + \"You are a test agent\",\n \"max_output_tokens\": null,\n \"max_tool_calls\": + null,\n \"model\": \"gpt-4-0613\",\n \"output\": [\n {\n \"id\": + \"msg_0bc715f27ef7a52b0169c41cdc7a0881a18e350c2c5951716f\",\n \"type\": + \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n + \ \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": + [],\n \"text\": \"Hello! How can I assist you today?\"\n }\n + \ ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n + \ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": + 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 16,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 11,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 27\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9e1fabf99f43d648-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 25 Mar 2026 17:35:24 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '1260' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=MqXpjdBpiNZGV_0goWMs2folVRrXakVjrvRbNz22DPE-1774460123.1357949-1.0.1.1-kaJVP3kL8q5lHEvOra6c3IE4kzLbeiJotp2HkLYSLleSUVmr1lhDLn3fsoZd9Gt5xhvNJ8cN51Ie_pHSENQARBh.XM5GzxonJQt48ATUpDimpPfu9k8Kyy_chWKwESMI; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 18:05:24 GMT + x-ratelimit-limit-requests: + - '1000' + x-ratelimit-limit-tokens: + - '600000' + x-ratelimit-remaining-requests: + - '999' + x-ratelimit-remaining-tokens: + - '599979' + x-ratelimit-reset-requests: + - 60ms + x-ratelimit-reset-tokens: + - 2ms + x-request-id: + - req_936f8cbf19144abb8262322211040648 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_v1_responses_post_f671d1c2.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_v1_responses_post_f671d1c2.yaml new file mode 100644 index 0000000000..a6edd58a45 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_v1_responses_post_f671d1c2.yaml @@ -0,0 +1,53 @@ +interactions: +- request: + body: '{"model":"gpt-4","input":"hello"}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '33' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - curl/8.7.1 + method: POST + uri: https://api.openai.com/v1/v1/responses + response: + body: + string: '' + headers: + CF-RAY: + - 9e1f69338c2de615-IAD + Connection: + - keep-alive + Date: + - Wed, 25 Mar 2026 16:49:48 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + set-cookie: + - __cf_bm=nhkfItMxViQKMxp30DdsWzyiLSYG_V24gTrFp6sNzXE-1774457388.0853722-1.0.1.1-eBBM5j_quoBmfxp_1hGhHqj7_aecnvXnvg1evq8dIQefomFRoyuiMgKtVWdgUCOJU01jnnFLymfL0NAw2h5FhVE_5CoBDUXWRxSJag3Is7OnOlysXYcwgUD4.oIp866Q; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Wed, 25 Mar 2026 + 17:19:48 GMT + - _cfuvid=71ErowylG_JQ6Ez90TmQY7G70J3cdFTedJc1IQQNb8U-1774457388.0853722-1.0.1.1-FFHwkg8O2W_RxZWUyB4Fci.gtpBvCzK0OwVG13WoVIs; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com + status: + code: 404 + message: Not Found +version: 1 diff --git a/packages/dd-trace/test/llmobs/openai-agents-utils.spec.js b/packages/dd-trace/test/llmobs/openai-agents-utils.spec.js new file mode 100644 index 0000000000..e922b31a03 --- /dev/null +++ b/packages/dd-trace/test/llmobs/openai-agents-utils.spec.js @@ -0,0 +1,321 @@ +'use strict' + +const assert = require('node:assert/strict') +const { + extractInputMessages, + extractOutputMessages, + extractMetrics, + extractMetadata, +} = require('../../src/llmobs/plugins/openai-agents/utils') + +describe('openai-agents utils', () => { + describe('extractInputMessages', () => { + it('prepends a system message when instructions are provided', () => { + assert.deepStrictEqual( + extractInputMessages('hello', 'You are helpful'), + [ + { role: 'system', content: 'You are helpful' }, + { role: 'user', content: 'hello' }, + ] + ) + }) + + it('treats a bare string input as a user message', () => { + assert.deepStrictEqual( + extractInputMessages('hi'), + [{ role: 'user', content: 'hi' }] + ) + }) + + it('joins input_text and text parts on array message content', () => { + const input = [{ + type: 'message', + role: 'user', + content: [ + { type: 'input_text', text: 'foo ' }, + { type: 'text', text: 'bar' }, + { type: 'image_url', url: 'data:...' }, + ], + }] + assert.deepStrictEqual( + extractInputMessages(input), + [{ role: 'user', content: 'foo bar' }] + ) + }) + + it('skips array message items missing a role', () => { + const input = [{ type: 'message', content: 'orphan' }] + assert.deepStrictEqual( + extractInputMessages(input), + [{ role: 'user', content: '' }] + ) + }) + + it('drops array message items whose content is empty after extraction', () => { + const input = [{ type: 'message', role: 'user', content: [] }] + assert.deepStrictEqual( + extractInputMessages(input), + [{ role: 'user', content: '' }] + ) + }) + + it('accepts string content on array message items', () => { + const input = [{ type: 'message', role: 'assistant', content: 'reply' }] + assert.deepStrictEqual( + extractInputMessages(input), + [{ role: 'assistant', content: 'reply' }] + ) + }) + + it('parses function_call arguments as JSON when possible', () => { + const input = [{ + type: 'function_call', + call_id: 'c1', + name: 'lookup', + arguments: '{"q":"sf"}', + }] + const out = extractInputMessages(input) + assert.strictEqual(out.length, 1) + assert.strictEqual(out[0].role, 'assistant') + assert.deepStrictEqual(out[0].toolCalls[0], { + toolId: 'c1', + name: 'lookup', + arguments: { q: 'sf' }, + type: 'function_call', + }) + }) + + it('falls back to empty object args when function_call JSON is invalid', () => { + const input = [{ + type: 'function_call', + call_id: 'c2', + name: 'lookup', + arguments: '{not json}', + }] + const out = extractInputMessages(input) + assert.deepStrictEqual(out[0].toolCalls[0].arguments, {}) + }) + + it('preserves non-string function_call arguments verbatim', () => { + const input = [{ + type: 'function_call', + call_id: 'c3', + name: 'lookup', + arguments: { q: 'sf' }, + }] + const out = extractInputMessages(input) + assert.deepStrictEqual(out[0].toolCalls[0].arguments, { q: 'sf' }) + }) + + it('extracts function_call_output items into a tool-result user message', () => { + const input = [{ + type: 'function_call_output', + call_id: 'c1', + name: 'lookup', + output: '72F', + }] + assert.deepStrictEqual( + extractInputMessages(input), + [{ + role: 'user', + toolResults: [{ + toolId: 'c1', + result: '72F', + name: 'lookup', + type: 'function_call_output', + }], + }] + ) + }) + + it('defaults function_call_output name to empty string when missing', () => { + const input = [{ type: 'function_call_output', call_id: 'c1', output: '72F' }] + const out = extractInputMessages(input) + assert.strictEqual(out[0].toolResults[0].name, '') + }) + + it('returns a placeholder user message when input yields no messages', () => { + assert.deepStrictEqual( + extractInputMessages([]), + [{ role: 'user', content: '' }] + ) + }) + }) + + describe('extractOutputMessages', () => { + it('returns a placeholder when result is missing', () => { + assert.deepStrictEqual( + extractOutputMessages(undefined), + [{ content: '', role: '' }] + ) + }) + + it('joins output_text parts and defaults role to assistant', () => { + const result = { + output: [{ + type: 'message', + content: [ + { type: 'output_text', text: 'hello ' }, + { type: 'output_text', text: 'world' }, + { type: 'reasoning', text: 'ignored' }, + ], + }], + } + assert.deepStrictEqual( + extractOutputMessages(result), + [{ role: 'assistant', content: 'hello world' }] + ) + }) + + it('accepts string content on message items', () => { + const result = { output: [{ type: 'message', role: 'user', content: 'echo' }] } + assert.deepStrictEqual( + extractOutputMessages(result), + [{ role: 'user', content: 'echo' }] + ) + }) + + it('extracts function_call output items into assistant toolCalls', () => { + const result = { + output: [{ + type: 'function_call', + call_id: 'c1', + name: 'lookup', + arguments: '{"q":"sf"}', + }], + } + const out = extractOutputMessages(result) + assert.strictEqual(out[0].role, 'assistant') + assert.deepStrictEqual(out[0].toolCalls[0], { + toolId: 'c1', + name: 'lookup', + arguments: { q: 'sf' }, + type: 'function_call', + }) + }) + + it('swallows invalid function_call JSON arguments', () => { + const result = { + output: [{ type: 'function_call', call_id: 'c1', name: 'lookup', arguments: '{bad' }], + } + const out = extractOutputMessages(result) + assert.deepStrictEqual(out[0].toolCalls[0].arguments, {}) + }) + + it('preserves non-string function_call arguments verbatim', () => { + const result = { + output: [{ type: 'function_call', call_id: 'c1', name: 'lookup', arguments: { q: 'sf' } }], + } + const out = extractOutputMessages(result) + assert.deepStrictEqual(out[0].toolCalls[0].arguments, { q: 'sf' }) + }) + }) + + describe('extractMetrics', () => { + it('returns undefined when usage is absent', () => { + assert.strictEqual(extractMetrics({}), undefined) + assert.strictEqual(extractMetrics(undefined), undefined) + }) + + it('returns undefined when usage carries no recognised counters', () => { + assert.strictEqual(extractMetrics({ usage: {} }), undefined) + }) + + it('reads camelCase counters from usage', () => { + const metrics = extractMetrics({ + usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }, + }) + assert.deepStrictEqual(metrics, { + inputTokens: 10, + outputTokens: 20, + totalTokens: 30, + }) + }) + + it('reads snake_case counters from usage', () => { + const metrics = extractMetrics({ + usage: { input_tokens: 5, output_tokens: 7, total_tokens: 12 }, + }) + assert.deepStrictEqual(metrics, { + inputTokens: 5, + outputTokens: 7, + totalTokens: 12, + }) + }) + + it('includes reasoning tokens from camelCase nested details', () => { + const metrics = extractMetrics({ + usage: { + inputTokens: 1, + outputTokens: 2, + outputTokensDetails: { reasoningTokens: 3 }, + }, + }) + assert.strictEqual(metrics.reasoningOutputTokens, 3) + }) + + it('includes reasoning tokens from snake_case nested details', () => { + const metrics = extractMetrics({ + usage: { output_tokens_details: { reasoning_tokens: 4 } }, + }) + assert.strictEqual(metrics.reasoningOutputTokens, 4) + }) + + it('omits a zero reasoning token count', () => { + const metrics = extractMetrics({ + usage: { inputTokens: 1, outputTokens: 1, outputTokensDetails: { reasoningTokens: 0 } }, + }) + assert.ok(!('reasoningOutputTokens' in metrics)) + }) + + it('derives totalTokens from input+output when total is missing', () => { + const metrics = extractMetrics({ usage: { inputTokens: 4, outputTokens: 6 } }) + assert.strictEqual(metrics.totalTokens, 10) + }) + + it('omits totalTokens when input or output is missing', () => { + const metrics = extractMetrics({ usage: { inputTokens: 4 } }) + assert.ok(!('totalTokens' in metrics)) + }) + }) + + describe('extractMetadata', () => { + it('returns undefined when response is missing', () => { + assert.strictEqual(extractMetadata(undefined), undefined) + assert.strictEqual(extractMetadata(null), undefined) + }) + + it('returns undefined when no recognised fields are set', () => { + assert.strictEqual(extractMetadata({ unrelated: 1 }), undefined) + }) + + it('extracts recognised response config fields', () => { + const md = extractMetadata({ + temperature: 0.7, + max_output_tokens: 256, + top_p: 1, + tools: [{ name: 'lookup' }], + tool_choice: 'auto', + truncation: 'disabled', + }) + assert.deepStrictEqual(md, { + temperature: 0.7, + max_output_tokens: 256, + top_p: 1, + tools: [{ name: 'lookup' }], + tool_choice: 'auto', + truncation: 'disabled', + }) + }) + + it('ignores undefined and null values', () => { + const md = extractMetadata({ temperature: 0.5, top_p: null, tools: undefined }) + assert.deepStrictEqual(md, { temperature: 0.5 }) + }) + + it('includes response.text when present', () => { + const md = extractMetadata({ text: { format: { type: 'text' } } }) + assert.deepStrictEqual(md, { text: { format: { type: 'text' } } }) + }) + }) +}) diff --git a/packages/dd-trace/test/llmobs/openai-utils.spec.js b/packages/dd-trace/test/llmobs/openai-utils.spec.js new file mode 100644 index 0000000000..0df67b7ed5 --- /dev/null +++ b/packages/dd-trace/test/llmobs/openai-utils.spec.js @@ -0,0 +1,70 @@ +'use strict' + +const assert = require('node:assert/strict') +const { getOpenAIModelProvider } = require('../../src/llmobs/plugins/openai/utils') +const OpenAiLLMObsPlugin = require('../../src/llmobs/plugins/openai') +const { UNKNOWN_MODEL_PROVIDER } = require('../../src/llmobs/constants/tags') + +describe('getOpenAIModelProvider', () => { + it('returns openai for openai.com URLs', () => { + assert.strictEqual(getOpenAIModelProvider('https://api.openai.com/v1'), 'openai') + }) + + it('returns azure_openai for Azure URLs', () => { + assert.strictEqual( + getOpenAIModelProvider('https://my-resource.openai.azure.com/openai'), + 'azure_openai' + ) + }) + + it('returns deepseek for DeepSeek URLs', () => { + assert.strictEqual(getOpenAIModelProvider('https://api.deepseek.com/v1'), 'deepseek') + }) + + it('returns unknown provider for unrecognised URLs', () => { + assert.strictEqual(getOpenAIModelProvider('http://127.0.0.1:9126/vcr/proxy'), UNKNOWN_MODEL_PROVIDER) + }) + + it('defaults to unknown provider for an empty string', () => { + assert.strictEqual(getOpenAIModelProvider(''), UNKNOWN_MODEL_PROVIDER) + }) +}) + +describe('OpenAiLLMObsPlugin#_getModelProviderAndClient', () => { + const call = (baseUrl) => OpenAiLLMObsPlugin.prototype._getModelProviderAndClient(baseUrl) + + it('maps Azure URLs to AzureOpenAI', () => { + assert.deepStrictEqual( + call('https://my-resource.openai.azure.com/openai'), + { modelProvider: 'azure_openai', client: 'AzureOpenAI' } + ) + }) + + it('maps DeepSeek URLs to DeepSeek', () => { + assert.deepStrictEqual( + call('https://api.deepseek.com/v1'), + { modelProvider: 'deepseek', client: 'DeepSeek' } + ) + }) + + it('maps openai.com URLs to OpenAI', () => { + assert.deepStrictEqual( + call('https://api.openai.com/v1'), + { modelProvider: 'openai', client: 'OpenAI' } + ) + }) + + it('falls back to OpenAI client for unknown providers', () => { + assert.deepStrictEqual( + call('http://127.0.0.1:9126/vcr/proxy'), + { modelProvider: UNKNOWN_MODEL_PROVIDER, client: 'OpenAI' } + ) + }) + + it('defaults baseUrl to empty string', () => { + assert.deepStrictEqual( + OpenAiLLMObsPlugin.prototype._getModelProviderAndClient(), + { modelProvider: UNKNOWN_MODEL_PROVIDER, client: 'OpenAI' } + ) + }) +}) diff --git a/packages/dd-trace/test/llmobs/plugins/openai-agents/index.spec.js b/packages/dd-trace/test/llmobs/plugins/openai-agents/index.spec.js new file mode 100644 index 0000000000..efd1a02e6b --- /dev/null +++ b/packages/dd-trace/test/llmobs/plugins/openai-agents/index.spec.js @@ -0,0 +1,313 @@ +'use strict' + +const assert = require('node:assert/strict') +const path = require('node:path') + +const { withVersions } = require('../../../setup/mocha') + +const { + assertLlmObsSpanEvent, + MOCK_NOT_NULLISH, + MOCK_STRING, + useLlmObs, +} = require('../../util') + +const AGENT_INSTRUCTIONS = 'You are a test agent' + +function createResponse (output, model = 'gpt-4-0613') { + return { + id: 'resp_test', + object: 'response', + created_at: 0, + status: 'completed', + instructions: AGENT_INSTRUCTIONS, + output, + usage: { + input_tokens: 1, + output_tokens: 1, + total_tokens: 2, + input_tokens_details: { cached_tokens: 0 }, + output_tokens_details: { reasoning_tokens: 0 }, + }, + model, + parallel_tool_calls: true, + temperature: 1, + text: { format: { type: 'text' } }, + tool_choice: 'auto', + tools: [], + top_p: 1, + truncation: 'disabled', + metadata: {}, + } +} + +function createMessageOutput (text) { + return { + id: 'msg_test', + type: 'message', + status: 'completed', + role: 'assistant', + content: [{ type: 'output_text', text, annotations: [] }], + } +} + +function createFetch (responses) { + let index = 0 + + return async () => { + const response = responses[Math.min(index++, responses.length - 1)] + return new Response(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + 'x-request-id': 'req_test', + }, + }) + } +} + +describe('integrations', () => { + describe('openai-agents LLMObs', () => { + const { getEvents } = useLlmObs({ plugin: 'openai-agents' }) + + let agentsCore + let agent + let handoffAgent + let toolErrorAgent + + withVersions('openai-agents', '@openai/agents', (version) => { + before(() => { + agentsCore = require(`../../../../../../versions/@openai/agents@${version}`).get() + + const { OpenAIResponsesModel } = + require(`../../../../../../versions/@openai/agents-openai@${version}`).get() + + const agentsOpenaiDir = path.join( + __dirname, '..', '..', '..', '..', '..', '..', 'versions', 'node_modules', '@openai', 'agents-openai' + ) + const openaiPath = require.resolve('openai', { paths: [agentsOpenaiDir] }) + const { OpenAI } = require(openaiPath) + + const mockClient = new OpenAI({ + apiKey: 'test', + baseURL: 'https://api.openai.com/v1', + fetch: createFetch([createResponse([createMessageOutput('hello')])]), + }) + const toolErrorClient = new OpenAI({ + apiKey: 'test', + baseURL: 'https://api.openai.com/v1', + fetch: createFetch([ + createResponse([{ + id: 'fc_test', + type: 'function_call', + call_id: 'call_test', + name: 'add', + arguments: '{"a":1,"b":2}', + status: 'completed', + }], 'gpt-4o-mini'), + createResponse([createMessageOutput('done')], 'gpt-4o-mini'), + ]), + }) + const handoffClient = new OpenAI({ + apiKey: 'test', + baseURL: 'https://api.openai.com/v1', + fetch: createFetch([ + createResponse([{ + id: 'fc_handoff', + type: 'function_call', + call_id: 'call_handoff', + name: 'transfer_to_agent_b', + arguments: '{}', + status: 'completed', + }], 'gpt-4o-mini'), + createResponse([createMessageOutput('done')], 'gpt-4o-mini'), + ]), + }) + + agentsCore.setDefaultModelProvider({ + createModel: (modelName) => new OpenAIResponsesModel(mockClient, modelName), + }) + + agent = new agentsCore.Agent({ + name: 'test_agent', + instructions: AGENT_INSTRUCTIONS, + model: new OpenAIResponsesModel(mockClient, 'gpt-4'), + }) + + const handoffModel = new OpenAIResponsesModel(handoffClient, 'gpt-4o-mini') + const handoffAgentB = new agentsCore.Agent({ + name: 'agent_b', + instructions: 'Finish the request', + model: handoffModel, + }) + handoffAgent = new agentsCore.Agent({ + name: 'agent_a', + instructions: 'Hand the request to agent_b', + model: handoffModel, + handoffs: [handoffAgentB], + }) + + // Tool with a real parameter schema so the model has something to + // pass — the underlying `execute` always throws, exercising the + // tool-error path. Mirrors dd-trace-py's + // `addition_agent_with_tool_errors` setup. + const additionErrorTool = agentsCore.tool({ + name: 'add', + description: 'Adds two numbers and returns the result.', + parameters: { + type: 'object', + properties: { + a: { type: 'number', description: 'First number' }, + b: { type: 'number', description: 'Second number' }, + }, + required: ['a', 'b'], + additionalProperties: false, + }, + execute: async () => { + throw new Error('Intentional error for testing') + }, + }) + + toolErrorAgent = new agentsCore.Agent({ + name: 'addition_agent_with_tool_errors', + instructions: 'You are a calculator. Use the `add` tool to answer math questions.', + model: new OpenAIResponsesModel(toolErrorClient, 'gpt-4o-mini'), + tools: [additionErrorTool], + }) + }) + + // Response metadata mirrors Python's openai-agents integration: + // response-echoed configuration fields with no filtering of OpenAI + // defaults — see `OaiSpanAdapter.llmobs_metadata` in dd-trace-py. + const COMMON_RESPONSE_METADATA = { + temperature: MOCK_NOT_NULLISH, + top_p: MOCK_NOT_NULLISH, + tool_choice: MOCK_NOT_NULLISH, + tools: MOCK_NOT_NULLISH, + truncation: MOCK_NOT_NULLISH, + text: MOCK_NOT_NULLISH, + } + + describe('run', () => { + it('submits a workflow span for a basic run call', async () => { + // run() produces three LLMObs spans (workflow, agent, llm) — see + // dd-trace-py's `test_llmobs_single_agent`. We only assert the + // workflow span here; the LLM span shape is covered separately. + await agentsCore.run(agent, 'hello', { maxTurns: 1 }) + + const { apmSpans, llmobsSpans } = await getEvents(3) + const workflowEvent = llmobsSpans.find(s => s.meta?.['span.kind'] === 'workflow') + const workflowApmSpan = apmSpans.find(s => s.name === 'Agent workflow') + + assertLlmObsSpanEvent(workflowEvent, { + span: workflowApmSpan, + spanKind: 'workflow', + name: 'Agent workflow', + inputValue: 'hello', + outputValue: MOCK_STRING, + tags: { ml_app: 'test', integration: 'openai-agents' }, + }) + }) + + it('submits an llm span under the agent span with the response shape', async () => { + // Under `Runner.run`, the response oai-span is a direct child of + // the top-level agent span, so the LLMObs span name becomes + // `${agent_name} (LLM)` (Python parity). + await agentsCore.run(agent, 'hello', { maxTurns: 1 }) + + const { apmSpans, llmobsSpans } = await getEvents(3) + const llmEvent = llmobsSpans.find(s => s.meta?.['span.kind'] === 'llm') + const llmApmSpan = apmSpans.find(s => s.name === 'openai_agents.response') + const agentApmSpan = apmSpans.find(s => s.name === 'test_agent') + + assertLlmObsSpanEvent(llmEvent, { + span: llmApmSpan, + parentId: agentApmSpan?.span_id, + spanKind: 'llm', + name: 'test_agent (LLM)', + modelName: 'gpt-4-0613', + modelProvider: 'openai', + inputMessages: [ + { role: 'system', content: AGENT_INSTRUCTIONS }, + { role: 'user', content: 'hello' }, + ], + outputMessages: [ + { role: 'assistant', content: MOCK_STRING }, + ], + metrics: { + input_tokens: MOCK_NOT_NULLISH, + output_tokens: MOCK_NOT_NULLISH, + total_tokens: MOCK_NOT_NULLISH, + }, + metadata: COMMON_RESPONSE_METADATA, + tags: { ml_app: 'test', integration: 'openai-agents' }, + }) + }) + + it('keeps the workflow open through a real multi-agent handoff', async () => { + const result = await agentsCore.run(handoffAgent, 'start', { maxTurns: 2 }) + assert.strictEqual(result.finalOutput, 'done') + + const { apmSpans, llmobsSpans } = await getEvents(6) + const workflowApmSpan = apmSpans.find(s => s.name === 'Agent workflow') + const agentAApmSpan = apmSpans.find(s => s.name === 'agent_a') + const agentBApmSpan = apmSpans.find(s => s.name === 'agent_b') + const handoffApmSpan = apmSpans.find(s => s.name === 'transfer_to_agent_b') + const workflowEvent = llmobsSpans.find(s => s.meta?.['span.kind'] === 'workflow') + const handoffEvent = llmobsSpans.find(s => s.name === 'transfer_to_agent_b') + + assert.ok(workflowApmSpan, 'expected a workflow APM span') + assert.ok(agentAApmSpan, 'expected agent_a APM span') + assert.ok(agentBApmSpan, 'expected agent_b APM span') + assert.ok(handoffApmSpan, 'expected a handoff APM span') + assert.strictEqual(agentAApmSpan.parent_id.toString(), workflowApmSpan.span_id.toString()) + assert.strictEqual(agentBApmSpan.parent_id.toString(), workflowApmSpan.span_id.toString()) + assert.strictEqual(handoffApmSpan.parent_id.toString(), agentAApmSpan.span_id.toString()) + + assertLlmObsSpanEvent(workflowEvent, { + span: workflowApmSpan, + spanKind: 'workflow', + name: 'Agent workflow', + inputValue: 'start', + outputValue: 'done', + tags: { ml_app: 'test', integration: 'openai-agents' }, + }) + assertLlmObsSpanEvent(handoffEvent, { + span: handoffApmSpan, + parentId: agentAApmSpan.span_id, + spanKind: 'tool', + name: 'transfer_to_agent_b', + inputValue: 'agent_a', + outputValue: 'agent_b', + tags: { ml_app: 'test', integration: 'openai-agents' }, + }) + }) + + it('emits a tool span flagged as errored when the tool throws', async () => { + // Mirrors dd-trace-py's `test_llmobs_single_agent_with_tool_errors`: + // a `Runner.run()` flow where the model decides to call a tool that + // throws. The SDK catches the error, surfaces it on the function + // span, then calls the model again with the error context. + // + // This is the canonical error-path coverage for the + // trace-processor architecture — direct `getResponse` / + // `invokeFunctionTool` errors don't produce spans without going + // through the runner. + try { + await agentsCore.run(toolErrorAgent, 'What is the sum of 1 and 2?', { maxTurns: 2 }) + } catch (err) { + // Expected: model loops on the failing tool call until maxTurns. + } + + const { llmobsSpans } = await getEvents(5) + const toolEvent = llmobsSpans.find(s => s.meta?.['span.kind'] === 'tool') + + assert(toolEvent, 'expected a tool span event') + assert.strictEqual(toolEvent.meta['span.kind'], 'tool') + assert.strictEqual(toolEvent.name, 'add') + assert.strictEqual(toolEvent.status, 'error') + }) + }) + }) // withVersions + }) +}) diff --git a/packages/dd-trace/test/llmobs/tagger.spec.js b/packages/dd-trace/test/llmobs/tagger.spec.js index 0fc69388fc..615f364b8d 100644 --- a/packages/dd-trace/test/llmobs/tagger.spec.js +++ b/packages/dd-trace/test/llmobs/tagger.spec.js @@ -640,6 +640,14 @@ describe('tagger', () => { }) }) + describe('setName', () => { + it('sets the _ml_obs.name tag on the span', () => { + tagger._register(span) + tagger.setName(span, 'my-span-name') + assert.strictEqual(Tagger.tagMap.get(span)['_ml_obs.name'], 'my-span-name') + }) + }) + describe('tagCostTags', () => { it('validates and sets cost tags', () => { tagger._register(span) diff --git a/packages/dd-trace/test/plugins/externals.js b/packages/dd-trace/test/plugins/externals.js index 8bb46fb873..535ee2dc38 100644 --- a/packages/dd-trace/test/plugins/externals.js +++ b/packages/dd-trace/test/plugins/externals.js @@ -542,6 +542,20 @@ module.exports = { dep: true, }, ], + 'openai-agents': [ + { + name: '@openai/agents', + versions: ['>=0.7.0'], + }, + { + name: '@openai/agents-core', + versions: ['>=0.7.0'], + }, + { + name: '@openai/agents-openai', + versions: ['>=0.7.0'], + }, + ], passport: [ { name: 'express', diff --git a/packages/dd-trace/test/plugins/versions/package.json b/packages/dd-trace/test/plugins/versions/package.json index f4f5d498fc..bae164ff84 100644 --- a/packages/dd-trace/test/plugins/versions/package.json +++ b/packages/dd-trace/test/plugins/versions/package.json @@ -68,6 +68,7 @@ "@node-redis/client": "1.0.6", "@openai/agents": "0.13.5", "@openai/agents-core": "0.13.5", + "@openai/agents-openai": "0.13.5", "@openfeature/core": "1.11.0", "@openfeature/server-sdk": "1.22.0", "@opensearch-project/opensearch": "3.6.0", diff --git a/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js b/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js index 199ce0cdc8..f365520efa 100644 --- a/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js +++ b/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js @@ -8,7 +8,7 @@ function createIntegrationTestSuite (pluginName, packageName, options, testCallb describe('Plugin', () => { describe(pluginName, () => { withVersions(pluginName, packageName, version => { - const meta = { agent, tracer: null, mod: null } + const meta = { agent, tracer: null, mod: null, version } describe('without configuration', () => { before(async () => { diff --git a/supported_versions_output.json b/supported_versions_output.json index 1c2bee0a62..40565c0db7 100644 --- a/supported_versions_output.json +++ b/supported_versions_output.json @@ -3,21 +3,21 @@ "dependency": "@anthropic-ai/claude-agent-sdk", "integration": "claude-agent-sdk", "minimum_tracer_supported": "0.2.113", - "max_tracer_supported": "0.3.202", + "max_tracer_supported": "0.3.215", "auto-instrumented": "True" }, { "dependency": "@anthropic-ai/sdk", "integration": "anthropic", "minimum_tracer_supported": "0.14.0", - "max_tracer_supported": "0.105.0", + "max_tracer_supported": "0.112.3", "auto-instrumented": "True" }, { "dependency": "@apollo/gateway", "integration": "apollo", "minimum_tracer_supported": "2.3.0", - "max_tracer_supported": "2.14.0", + "max_tracer_supported": "2.14.2", "auto-instrumented": "True" }, { @@ -31,7 +31,7 @@ "dependency": "@aws/durable-execution-sdk-js", "integration": "aws-durable-execution-sdk-js", "minimum_tracer_supported": "1.1.0", - "max_tracer_supported": "2.0.0", + "max_tracer_supported": "2.2.0", "auto-instrumented": "True" }, { @@ -52,7 +52,7 @@ "dependency": "@azure/functions", "integration": "azure-functions", "minimum_tracer_supported": "4.0.0", - "max_tracer_supported": "4.16.1", + "max_tracer_supported": "4.16.2", "auto-instrumented": "True" }, { @@ -66,28 +66,28 @@ "dependency": "@confluentinc/kafka-javascript", "integration": "confluentinc-kafka-javascript", "minimum_tracer_supported": "1.0.0", - "max_tracer_supported": "1.9.1", + "max_tracer_supported": "1.10.0", "auto-instrumented": "True" }, { "dependency": "@cucumber/cucumber", "integration": "cucumber", "minimum_tracer_supported": "7.0.0", - "max_tracer_supported": "13.0.0", + "max_tracer_supported": "13.2.0", "auto-instrumented": "True" }, { "dependency": "@elastic/elasticsearch", "integration": "elasticsearch", "minimum_tracer_supported": "5.6.16", - "max_tracer_supported": "9.4.0", + "max_tracer_supported": "9.4.2", "auto-instrumented": "True" }, { "dependency": "@elastic/transport", "integration": "elasticsearch", "minimum_tracer_supported": "8.0.0", - "max_tracer_supported": "9.3.5", + "max_tracer_supported": "9.3.7", "auto-instrumented": "True" }, { @@ -108,14 +108,14 @@ "dependency": "@google/genai", "integration": "google-genai", "minimum_tracer_supported": "1.19.0", - "max_tracer_supported": "2.9.0", + "max_tracer_supported": "2.12.0", "auto-instrumented": "True" }, { "dependency": "@grpc/grpc-js", "integration": "grpc", "minimum_tracer_supported": "1.0.3", - "max_tracer_supported": "1.14.3", + "max_tracer_supported": "1.14.4", "auto-instrumented": "True" }, { @@ -157,21 +157,21 @@ "dependency": "@koa/router", "integration": "koa", "minimum_tracer_supported": "8.0.0", - "max_tracer_supported": "15.5.0", + "max_tracer_supported": "15.7.0", "auto-instrumented": "True" }, { "dependency": "@langchain/core", "integration": "langchain", "minimum_tracer_supported": "0.1.0", - "max_tracer_supported": "1.2.0", + "max_tracer_supported": "1.2.3", "auto-instrumented": "True" }, { "dependency": "@langchain/langgraph", "integration": "langgraph", "minimum_tracer_supported": "1.1.2", - "max_tracer_supported": "1.4.4", + "max_tracer_supported": "1.4.8", "auto-instrumented": "True" }, { @@ -202,6 +202,13 @@ "max_tracer_supported": "1.0.6", "auto-instrumented": "True" }, + { + "dependency": "@openai/agents", + "integration": "openai-agents", + "minimum_tracer_supported": "0.7.0", + "max_tracer_supported": "0.13.5", + "auto-instrumented": "True" + }, { "dependency": "@opensearch-project/opensearch", "integration": "opensearch", @@ -220,42 +227,35 @@ "dependency": "@redis/client", "integration": "redis", "minimum_tracer_supported": "1.1.0", - "max_tracer_supported": "5.12.1", + "max_tracer_supported": "6.1.0", "auto-instrumented": "True" }, { "dependency": "@smithy/core", "integration": "aws-sdk", "minimum_tracer_supported": "3.24.0", - "max_tracer_supported": "3.25.1", + "max_tracer_supported": "3.29.5", "auto-instrumented": "True" }, { "dependency": "@smithy/smithy-client", "integration": "aws-sdk", "minimum_tracer_supported": "1.0.3", - "max_tracer_supported": "4.14.1", - "auto-instrumented": "True" - }, - { - "dependency": "@vitest/runner", - "integration": "vitest", - "minimum_tracer_supported": "1.6.0", - "max_tracer_supported": "4.1.9", + "max_tracer_supported": "4.14.10", "auto-instrumented": "True" }, { "dependency": "aerospike", "integration": "aerospike", "minimum_tracer_supported": "4.0.0", - "max_tracer_supported": "6.7.0", + "max_tracer_supported": "6.7.1", "auto-instrumented": "True" }, { "dependency": "ai", "integration": "ai", "minimum_tracer_supported": "4.0.0", - "max_tracer_supported": "6.0.208", + "max_tracer_supported": "7.0.31", "auto-instrumented": "True" }, { @@ -290,7 +290,7 @@ "dependency": "bullmq", "integration": "bullmq", "minimum_tracer_supported": "5.66.0", - "max_tracer_supported": "5.79.1", + "max_tracer_supported": "5.80.9", "auto-instrumented": "True" }, { @@ -310,7 +310,7 @@ { "dependency": "child_process", "integration": "child_process", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, @@ -325,7 +325,7 @@ "dependency": "couchbase", "integration": "couchbase", "minimum_tracer_supported": "3.0.7", - "max_tracer_supported": "4.7.0", + "max_tracer_supported": "4.7.1", "auto-instrumented": "True" }, { @@ -338,7 +338,7 @@ { "dependency": "dns", "integration": "dns", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, @@ -346,7 +346,7 @@ "dependency": "durable-functions", "integration": "azure-durable-functions", "minimum_tracer_supported": "3.0.0", - "max_tracer_supported": "3.3.1", + "max_tracer_supported": "3.5.0", "auto-instrumented": "True" }, { @@ -374,7 +374,7 @@ "dependency": "fastify", "integration": "fastify", "minimum_tracer_supported": "1.0.0", - "max_tracer_supported": "5.8.5", + "max_tracer_supported": "5.10.0", "auto-instrumented": "True" }, { @@ -387,7 +387,7 @@ { "dependency": "fs", "integration": "fs", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, @@ -395,7 +395,7 @@ "dependency": "graphql", "integration": "graphql", "minimum_tracer_supported": "0.10.0", - "max_tracer_supported": "16.14.0", + "max_tracer_supported": "16.14.2", "auto-instrumented": "True" }, { @@ -409,27 +409,27 @@ "dependency": "hono", "integration": "hono", "minimum_tracer_supported": "4.0.0", - "max_tracer_supported": "4.12.19", + "max_tracer_supported": "4.12.30", "auto-instrumented": "True" }, { "dependency": "http", "integration": "http", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "http2", "integration": "http2", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "https", "integration": "http", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, @@ -437,7 +437,7 @@ "dependency": "ioredis", "integration": "ioredis", "minimum_tracer_supported": "2.0.0", - "max_tracer_supported": "5.10.1", + "max_tracer_supported": "5.11.1", "auto-instrumented": "True" }, { @@ -500,7 +500,7 @@ "dependency": "koa", "integration": "koa", "minimum_tracer_supported": "2.0.0", - "max_tracer_supported": "3.2.0", + "max_tracer_supported": "3.2.1", "auto-instrumented": "True" }, { @@ -524,6 +524,13 @@ "max_tracer_supported": "2.2.2", "auto-instrumented": "True" }, + { + "dependency": "mercurius", + "integration": "graphql", + "minimum_tracer_supported": "13.0.0", + "max_tracer_supported": "16.10.0", + "auto-instrumented": "True" + }, { "dependency": "microgateway-core", "integration": "microgateway-core", @@ -556,7 +563,7 @@ "dependency": "mongodb", "integration": "mongodb-core", "minimum_tracer_supported": "3.3.0", - "max_tracer_supported": "7.2.0", + "max_tracer_supported": "7.5.0", "auto-instrumented": "True" }, { @@ -570,7 +577,7 @@ "dependency": "mongoose", "integration": "mongoose", "minimum_tracer_supported": "4.6.4", - "max_tracer_supported": "9.6.2", + "max_tracer_supported": "9.7.4", "auto-instrumented": "True" }, { @@ -584,13 +591,13 @@ "dependency": "mysql2", "integration": "mysql2", "minimum_tracer_supported": "1.0.0", - "max_tracer_supported": "3.22.3", + "max_tracer_supported": "3.23.0", "auto-instrumented": "True" }, { "dependency": "net", "integration": "net", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, @@ -604,42 +611,42 @@ { "dependency": "node:dns", "integration": "dns", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "node:fs", "integration": "fs", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "node:http", "integration": "http", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "node:http2", "integration": "http2", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "node:https", "integration": "http", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, { "dependency": "node:net", "integration": "net", - "minimum_tracer_supported": "18.0.0", + "minimum_tracer_supported": "22.0.0", "max_tracer_supported": "26.4.0", "auto-instrumented": "True" }, @@ -654,21 +661,21 @@ "dependency": "openai", "integration": "openai", "minimum_tracer_supported": "3.0.0", - "max_tracer_supported": "6.44.0", + "max_tracer_supported": "6.48.0", "auto-instrumented": "True" }, { "dependency": "oracledb", "integration": "oracledb", "minimum_tracer_supported": "5.0.0", - "max_tracer_supported": "6.10.0", + "max_tracer_supported": "7.0.1", "auto-instrumented": "True" }, { "dependency": "pg", "integration": "pg", "minimum_tracer_supported": "8.0.3", - "max_tracer_supported": "8.21.0", + "max_tracer_supported": "8.22.0", "auto-instrumented": "True" }, { @@ -696,14 +703,14 @@ "dependency": "protobufjs", "integration": "protobufjs", "minimum_tracer_supported": "6.8.0", - "max_tracer_supported": "8.6.4", + "max_tracer_supported": "8.7.1", "auto-instrumented": "True" }, { "dependency": "redis", "integration": "redis", "minimum_tracer_supported": "0.12.0", - "max_tracer_supported": "5.12.1", + "max_tracer_supported": "6.1.0", "auto-instrumented": "True" }, { @@ -738,14 +745,14 @@ "dependency": "sharedb", "integration": "sharedb", "minimum_tracer_supported": "1.0.0", - "max_tracer_supported": "5.2.2", + "max_tracer_supported": "6.0.1", "auto-instrumented": "True" }, { "dependency": "tedious", "integration": "tedious", "minimum_tracer_supported": "1.0.0", - "max_tracer_supported": "19.2.1", + "max_tracer_supported": "20.0.0", "auto-instrumented": "True" }, { @@ -759,7 +766,7 @@ "dependency": "undici", "integration": "undici", "minimum_tracer_supported": "4.4.1", - "max_tracer_supported": "8.3.0", + "max_tracer_supported": "8.7.0", "auto-instrumented": "True" }, { @@ -787,7 +794,7 @@ "dependency": "ws", "integration": "ws", "minimum_tracer_supported": "8.0.0", - "max_tracer_supported": "8.20.1", + "max_tracer_supported": "8.21.1", "auto-instrumented": "True" } ] diff --git a/supported_versions_table.csv b/supported_versions_table.csv index d23d6c9904..5cd8e5ca78 100644 --- a/supported_versions_table.csv +++ b/supported_versions_table.csv @@ -1,67 +1,67 @@ dependency,integration,minimum_tracer_supported,max_tracer_supported,auto-instrumented -@anthropic-ai/claude-agent-sdk,claude-agent-sdk,0.2.113,0.3.202,True -@anthropic-ai/sdk,anthropic,0.14.0,0.105.0,True -@apollo/gateway,apollo,2.3.0,2.14.0,True +@anthropic-ai/claude-agent-sdk,claude-agent-sdk,0.2.113,0.3.215,True +@anthropic-ai/sdk,anthropic,0.14.0,0.112.3,True +@apollo/gateway,apollo,2.3.0,2.14.2,True @aws-sdk/smithy-client,aws-sdk,3.0.0,3.374.0,True -@aws/durable-execution-sdk-js,aws-durable-execution-sdk-js,1.1.0,2.0.0,True +@aws/durable-execution-sdk-js,aws-durable-execution-sdk-js,1.1.0,2.2.0,True @azure/cosmos,azure-cosmos,4.4.1,4.9.3,True @azure/event-hubs,azure-event-hubs,6.0.0,6.0.4,True -@azure/functions,azure-functions,4.0.0,4.16.1,True +@azure/functions,azure-functions,4.0.0,4.16.2,True @azure/service-bus,azure-service-bus,7.9.2,7.9.5,True -@confluentinc/kafka-javascript,confluentinc-kafka-javascript,1.0.0,1.9.1,True -@cucumber/cucumber,cucumber,7.0.0,13.0.0,True -@elastic/elasticsearch,elasticsearch,5.6.16,9.4.0,True -@elastic/transport,elasticsearch,8.0.0,9.3.5,True +@confluentinc/kafka-javascript,confluentinc-kafka-javascript,1.0.0,1.10.0,True +@cucumber/cucumber,cucumber,7.0.0,13.2.0,True +@elastic/elasticsearch,elasticsearch,5.6.16,9.4.2,True +@elastic/transport,elasticsearch,8.0.0,9.3.7,True @google-cloud/pubsub,google-cloud-pubsub,1.2.0,5.3.1,True @google-cloud/vertexai,google-cloud-vertexai,1.0.0,1.12.0,True -@google/genai,google-genai,1.19.0,2.9.0,True -@grpc/grpc-js,grpc,1.0.3,1.14.3,True +@google/genai,google-genai,1.19.0,2.12.0,True +@grpc/grpc-js,grpc,1.0.3,1.14.4,True @hapi/hapi,hapi,17.9.0,21.4.9,True @happy-dom/jest-environment,jest,10.0.0,20.10.6,True @jest/core,jest,28.0.0,30.4.2,True @jest/test-sequencer,jest,28.0.0,30.4.1,True @jest/transform,jest,28.0.0,30.4.1,True -@koa/router,koa,8.0.0,15.5.0,True -@langchain/core,langchain,0.1.0,1.2.0,True -@langchain/langgraph,langgraph,1.1.2,1.4.4,True +@koa/router,koa,8.0.0,15.7.0,True +@langchain/core,langchain,0.1.0,1.2.3,True +@langchain/langgraph,langgraph,1.1.2,1.4.8,True @modelcontextprotocol/sdk,modelcontextprotocol-sdk,1.27.1,1.29.0,True @nats-io/nats-core,nats,3.0.0,3.4.0,True @nats-io/transport-node,nats,3.0.0,3.4.0,True @node-redis/client,redis,1.0.0,1.0.6,True +@openai/agents,openai-agents,0.7.0,0.13.5,True @opensearch-project/opensearch,opensearch,1.0.0,3.6.0,True @prisma/client,prisma,6.1.0,7.8.0,True -@redis/client,redis,1.1.0,5.12.1,True -@smithy/core,aws-sdk,3.24.0,3.25.1,True -@smithy/smithy-client,aws-sdk,1.0.3,4.14.1,True -@vitest/runner,vitest,1.6.0,4.1.9,True -aerospike,aerospike,4.0.0,6.7.0,True -ai,ai,4.0.0,6.0.208,True +@redis/client,redis,1.1.0,6.1.0,True +@smithy/core,aws-sdk,3.24.0,3.29.5,True +@smithy/smithy-client,aws-sdk,1.0.3,4.14.10,True +aerospike,aerospike,4.0.0,6.7.1,True +ai,ai,4.0.0,7.0.31,True amqp10,amqp10,3.0.0,3.6.0,True amqplib,amqplib,0.5.0,2.0.1,True avsc,avsc,5.0.0,5.7.9,True aws-sdk,aws-sdk,2.1.35,2.1693.0,True -bullmq,bullmq,5.66.0,5.79.1,True +bullmq,bullmq,5.66.0,5.80.9,True bunyan,bunyan,1.0.0,2.0.5,True cassandra-driver,cassandra-driver,3.0.0,4.9.0,True -child_process,child_process,18.0.0,26.4.0,True +child_process,child_process,22.0.0,26.4.0,True connect,connect,2.2.2,3.7.0,True -couchbase,couchbase,3.0.7,4.7.0,True +couchbase,couchbase,3.0.7,4.7.1,True cypress,cypress,12.0.0,15.16.0,True -dns,dns,18.0.0,26.4.0,True -durable-functions,azure-durable-functions,3.0.0,3.3.1,True +dns,dns,22.0.0,26.4.0,True +durable-functions,azure-durable-functions,3.0.0,3.5.0,True elasticsearch,elasticsearch,10.0.0,16.7.3,True electron,electron,37.0.0,42.1.0,True express,express,4.0.0,5.2.1,True -fastify,fastify,1.0.0,5.8.5,True +fastify,fastify,1.0.0,5.10.0,True find-my-way,find-my-way,1.0.0,9.6.0,True -fs,fs,18.0.0,26.4.0,True -graphql,graphql,0.10.0,16.14.0,True +fs,fs,22.0.0,26.4.0,True +graphql,graphql,0.10.0,16.14.2,True hapi,hapi,16.0.0,18.1.0,True -hono,hono,4.0.0,4.12.19,True -http,http,18.0.0,26.4.0,True -http2,http2,18.0.0,26.4.0,True -https,http,18.0.0,26.4.0,True -ioredis,ioredis,2.0.0,5.10.1,True +hono,hono,4.0.0,4.12.30,True +http,http,22.0.0,26.4.0,True +http2,http2,22.0.0,26.4.0,True +https,http,22.0.0,26.4.0,True +ioredis,ioredis,2.0.0,5.11.1,True iovalkey,iovalkey,0.0.1,0.3.3,True jest-circus,jest,28.0.0,30.4.2,True jest-config,jest,28.0.0,30.4.2,True @@ -70,45 +70,46 @@ jest-environment-node,jest,28.0.0,30.4.1,True jest-runtime,jest,28.0.0,30.4.2,True jest-worker,jest,28.0.0,30.4.1,True kafkajs,kafkajs,1.4.0,2.2.4,True -koa,koa,2.0.0,3.2.0,True +koa,koa,2.0.0,3.2.1,True koa-router,koa,7.0.0,14.0.0,True mariadb,mariadb,2.0.4,3.4.5,True memcached,memcached,2.2.0,2.2.2,True +mercurius,graphql,13.0.0,16.10.0,True microgateway-core,microgateway-core,2.1.0,3.3.7,True mocha,mocha,8.0.0,11.7.6,True mocha-each,mocha,2.0.1,2.0.1,True moleculer,moleculer,0.14.0,0.15.0,True -mongodb,mongodb-core,3.3.0,7.2.0,True +mongodb,mongodb-core,3.3.0,7.5.0,True mongodb-core,mongodb-core,2.0.0,3.2.7,True -mongoose,mongoose,4.6.4,9.6.2,True +mongoose,mongoose,4.6.4,9.7.4,True mysql,mysql,2.0.0,2.18.1,True -mysql2,mysql2,1.0.0,3.22.3,True -net,net,18.0.0,26.4.0,True +mysql2,mysql2,1.0.0,3.23.0,True +net,net,22.0.0,26.4.0,True next,next,10.2.0,16.2.6,True -node:dns,dns,18.0.0,26.4.0,True -node:fs,fs,18.0.0,26.4.0,True -node:http,http,18.0.0,26.4.0,True -node:http2,http2,18.0.0,26.4.0,True -node:https,http,18.0.0,26.4.0,True -node:net,net,18.0.0,26.4.0,True +node:dns,dns,22.0.0,26.4.0,True +node:fs,fs,22.0.0,26.4.0,True +node:http,http,22.0.0,26.4.0,True +node:http2,http2,22.0.0,26.4.0,True +node:https,http,22.0.0,26.4.0,True +node:net,net,22.0.0,26.4.0,True nyc,nyc,17.0.0,18.0.0,True -openai,openai,3.0.0,6.44.0,True -oracledb,oracledb,5.0.0,6.10.0,True -pg,pg,8.0.3,8.21.0,True +openai,openai,3.0.0,6.48.0,True +oracledb,oracledb,5.0.0,7.0.1,True +pg,pg,8.0.3,8.22.0,True pino,pino,2.0.0,10.3.1,True pino-pretty,pino,1.0.0,13.1.3,True playwright,playwright,1.38.0,1.61.0,True -protobufjs,protobufjs,6.8.0,8.6.4,True -redis,redis,0.12.0,5.12.1,True +protobufjs,protobufjs,6.8.0,8.7.1,True +redis,redis,0.12.0,6.1.0,True restify,restify,3.0.0,11.1.0,True rhea,rhea,1.0.0,3.0.5,True router,router,1.0.0,2.2.0,True selenium-webdriver,selenium,4.11.0,4.44.0,True -sharedb,sharedb,1.0.0,5.2.2,True -tedious,tedious,1.0.0,19.2.1,True +sharedb,sharedb,1.0.0,6.0.1,True +tedious,tedious,1.0.0,20.0.0,True tinypool,vitest,0.8.0,2.1.0,True -undici,undici,4.4.1,8.3.0,True +undici,undici,4.4.1,8.7.0,True vitest,vitest,1.6.0,4.1.9,True winston,winston,1.0.0,3.19.0,True workerpool,mocha,6.0.0,10.0.2,True -ws,ws,8.0.0,8.20.1,True +ws,ws,8.0.0,8.21.1,True From 335e91e6685a6a9ea72e1dfe6ca860dfd5c8bde4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:30:24 -0400 Subject: [PATCH 4/9] chore(deps-dev): bump @vercel/nft from 0.29.4 to 1.10.2 (#9477) --- package.json | 2 +- .../test/openfeature/file-tracing.spec.js | 11 +++++++++- yarn.lock | 22 +++++++++---------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 307fe575aa..886c7d18c1 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^18.19.106", "@types/sinon": "^22.0.0", - "@vercel/nft": "^0.29.4", + "@vercel/nft": "^1.10.2", "axios": "^1.18.1", "benchmark": "^2.1.4", "body-parser": "^2.3.0", diff --git a/packages/dd-trace/test/openfeature/file-tracing.spec.js b/packages/dd-trace/test/openfeature/file-tracing.spec.js index f5d1c179b1..1dce1f9817 100644 --- a/packages/dd-trace/test/openfeature/file-tracing.spec.js +++ b/packages/dd-trace/test/openfeature/file-tracing.spec.js @@ -6,9 +6,10 @@ const { mkdirSync, mkdtempSync, rmSync, symlinkSync } = require('node:fs') const { tmpdir } = require('node:os') const path = require('node:path') -const { nodeFileTrace } = require('@vercel/nft') const { describe, it } = require('mocha') +const { NODE_MAJOR } = require('../../../../version') + const repoRoot = path.resolve(__dirname, '../../../..') const expectedPackageFiles = [ 'node_modules/@datadog/openfeature-node-server/package.json', @@ -16,6 +17,14 @@ const expectedPackageFiles = [ 'node_modules/spark-md5/package.json', ] +if (NODE_MAJOR < 20) { + describe.skip('OpenFeature file tracing (requires @vercel/nft, which needs Node.js >= 20)') + return +} + +// eslint-disable-next-line import/order +const { nodeFileTrace } = require('@vercel/nft') + /** * @param {string} entrypoint */ diff --git a/yarn.lock b/yarn.lock index 9bf9e895cd..e2cddd5d37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1214,10 +1214,10 @@ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz#72da0da48d72b1e87831b9c0308931d3f4669027" integrity sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA== -"@vercel/nft@^0.29.4": - version "0.29.4" - resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-0.29.4.tgz#e56b07d193776bcf67b31ac4da065ceb8e8d362e" - integrity sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA== +"@vercel/nft@^1.10.2": + version "1.10.2" + resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-1.10.2.tgz#01aefaddc079a19bfe7d499872b2d6dd3cd729e1" + integrity sha512-w+WyX5Ulmj4dtTZrxaulqrjaLZHSbnPzx75SJsTNYmotKsqn1JlLnDJa+lz5hn90HJofhl/2MAtw0mCrgM3qYw== dependencies: "@mapbox/node-pre-gyp" "^2.0.0" "@rollup/pluginutils" "^5.1.3" @@ -1226,7 +1226,7 @@ async-sema "^3.1.1" bindings "^1.4.0" estree-walker "2.0.2" - glob "^10.4.5" + glob "^13.0.0" graceful-fs "^4.2.9" node-gyp-build "^4.2.2" picomatch "^4.0.2" @@ -1381,7 +1381,7 @@ balanced-match@^4.0.2: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== -baseline-browser-mapping@^2.10.44, baseline-browser-mapping@^2.9.0: +baseline-browser-mapping@^2.10.44: version "2.11.1" resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz#390b4714558634093df77add4acca0c5c0c1605e" integrity sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A== @@ -1571,7 +1571,7 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001806: +caniuse-lite@^1.0.30001806: version "1.0.30001806" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz#1bc8e502b723fa393455dfbedd5ccec0c29bb74e" integrity sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw== @@ -1851,7 +1851,7 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.5.263, electron-to-chromium@^1.5.393: +electron-to-chromium@^1.5.393: version "1.5.395" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz#b82e88344bdc9e5c00d280140e7ca278c7325661" integrity sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA== @@ -2451,7 +2451,7 @@ glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^13.0.3, glob@^13.0.6: +glob@^13.0.0, glob@^13.0.3, glob@^13.0.6: version "13.0.6" resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d" integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw== @@ -3189,7 +3189,7 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^2.0.27, node-releases@^2.0.51: +node-releases@^2.0.51: version "2.0.51" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.51.tgz#cdc08433577f5b32ad01694481726e22eeb54aef" integrity sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ== @@ -4204,7 +4204,7 @@ unrs-resolver@^1.9.2: "@unrs/resolver-binding-win32-ia32-msvc" "1.12.2" "@unrs/resolver-binding-win32-x64-msvc" "1.12.2" -update-browserslist-db@^1.2.0, update-browserslist-db@^1.2.3: +update-browserslist-db@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== From c4b749d9ad305b3d1dd7a5b7fa9b12873358fc83 Mon Sep 17 00:00:00 2001 From: Sam Brenner <106700075+sabrenner@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:28:22 -0400 Subject: [PATCH 5/9] fix(llmobs): llm observability traces have custom trace IDs (#9460) * separate llmobs trace ids * update exportSpan * tests * add tagger test * additional fixups * update tests from rebase --- packages/dd-trace/src/llmobs/index.js | 10 +- packages/dd-trace/src/llmobs/sdk.js | 3 +- .../dd-trace/src/llmobs/span_processor.js | 9 +- packages/dd-trace/src/llmobs/tagger.js | 22 ++- packages/dd-trace/src/llmobs/util.js | 59 ++++++- packages/dd-trace/test/llmobs/index.spec.js | 53 +++++- .../dd-trace/test/llmobs/sdk/index.spec.js | 156 +++++++++--------- .../test/llmobs/sdk/integration.spec.js | 10 +- .../test/llmobs/span_processor.spec.js | 4 +- packages/dd-trace/test/llmobs/tagger.spec.js | 93 ++++++----- packages/dd-trace/test/llmobs/util.spec.js | 45 +++++ 11 files changed, 323 insertions(+), 141 deletions(-) diff --git a/packages/dd-trace/src/llmobs/index.js b/packages/dd-trace/src/llmobs/index.js index 88439e175e..dfd3c41759 100644 --- a/packages/dd-trace/src/llmobs/index.js +++ b/packages/dd-trace/src/llmobs/index.js @@ -16,6 +16,8 @@ const { SAMPLING_DECISION, PROPAGATED_SAMPLE_RATE_KEY, PROPAGATED_SAMPLING_DECISION_KEY, + TRACE_ID, + PROPAGATED_TRACE_ID_KEY, } = require('./constants/tags') const { storage } = require('./storage') const telemetry = require('./telemetry') @@ -25,6 +27,7 @@ const LLMObsTagger = require('./tagger') const LLMObsSpanWriter = require('./writers/spans') const { setAgentStrategy } = require('./writers/util') const { INCOMPATIBLE_INITIALIZATION } = require('./constants/text') +const { llmObsTraceIdToWire } = require('./util') const spanFinishCh = channel('dd-trace:span:finish') const evalMetricAppendCh = channel('llmobs:eval-metric:append') @@ -137,8 +140,12 @@ function handleLLMObsInjection ({ carrier }) { mlObsSpanTags?.[SESSION_ID] ?? parentContext?._trace?.tags?.[SESSION_ID_TRACE_DEFAULT_KEY] ?? parentContext?._trace?.tags?.[PROPAGATED_SESSION_ID_KEY] + const llmobsTraceId = mlObsSpanTags?.[TRACE_ID] + const propagatedTraceId = llmobsTraceId === undefined + ? parentContext?._trace?.tags?.[PROPAGATED_TRACE_ID_KEY] + : llmObsTraceIdToWire(llmobsTraceId) - if (!parentId && !mlApp && samplingDecision == null && !sessionId) return + if (!parentId && !mlApp && samplingDecision == null && !sessionId && !propagatedTraceId) return // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*` // tags, so it may be undefined here — coalesce before appending. @@ -149,6 +156,7 @@ function handleLLMObsInjection ({ carrier }) { if (sessionId) tags += `${tags ? ',' : ''}${PROPAGATED_SESSION_ID_KEY}=${sessionId}` if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}` if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}` + if (propagatedTraceId != null) tags += `${tags ? ',' : ''}${PROPAGATED_TRACE_ID_KEY}=${propagatedTraceId}` if (tags !== existing) carrier['x-datadog-tags'] = tags } diff --git a/packages/dd-trace/src/llmobs/sdk.js b/packages/dd-trace/src/llmobs/sdk.js index f7869545f5..a93bcaf05a 100644 --- a/packages/dd-trace/src/llmobs/sdk.js +++ b/packages/dd-trace/src/llmobs/sdk.js @@ -11,6 +11,7 @@ const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE, + TRACE_ID, } = require('./constants/tags') const { getFunctionArguments, @@ -345,7 +346,7 @@ class LLMObs extends NoopLLMObs { } try { return { - traceId: span.context().toTraceId(true), + traceId: LLMObsTagger.tagMap.get(span)[TRACE_ID], spanId: span.context().toSpanId(), } } catch { diff --git a/packages/dd-trace/src/llmobs/span_processor.js b/packages/dd-trace/src/llmobs/span_processor.js index a41a629928..d22eef4a86 100644 --- a/packages/dd-trace/src/llmobs/span_processor.js +++ b/packages/dd-trace/src/llmobs/span_processor.js @@ -35,6 +35,7 @@ const { LLMOBS_SUBMITTED_TAG_KEY, SAMPLE_RATE, SAMPLING_DECISION, + TRACE_ID, } = require('./constants/tags') const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text') const telemetry = require('./telemetry') @@ -236,8 +237,11 @@ class LLMObsSpanProcessor { meta.input.prompt = prompt } + const apmTraceId = span.context().toTraceId(true) + const llmobsTraceId = mlObsTags[TRACE_ID] ?? apmTraceId + const llmObsSpanEvent = { - trace_id: span.context().toTraceId(true), + trace_id: llmobsTraceId, span_id: span.context().toSpanId(), parent_id: parentId, name, @@ -249,9 +253,10 @@ class LLMObsSpanProcessor { metrics, _dd: { span_id: span.context().toSpanId(), - trace_id: span.context().toTraceId(true), + trace_id: apmTraceId, sample_rate: mlObsTags[SAMPLE_RATE], sampling_decision: mlObsTags[SAMPLING_DECISION], + apm_trace_id: apmTraceId, }, } diff --git a/packages/dd-trace/src/llmobs/tagger.js b/packages/dd-trace/src/llmobs/tagger.js index 735dc24f3c..a1ec7a1bb7 100644 --- a/packages/dd-trace/src/llmobs/tagger.js +++ b/packages/dd-trace/src/llmobs/tagger.js @@ -51,9 +51,18 @@ const { SAMPLING_DECISION_DROPPED, PROPAGATED_SAMPLE_RATE_KEY, PROPAGATED_SAMPLING_DECISION_KEY, + TRACE_ID, + PROPAGATED_TRACE_ID_KEY, } = require('./constants/tags') const { storage } = require('./storage') -const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags, validateToolDefinitions } = require('./util') +const { + findGenAIAncestorSpanId, + validateCostTags, + writeBridgeTags, + validateToolDefinitions, + generateLlmObsTraceId, + normalizeLlmObsTraceId, +} = require('./util') // global registry of LLMObs spans // maps LLMObs spans to their annotations @@ -126,12 +135,20 @@ class LLMObsTagger { this._register(span) + const traceTags = span.context()._trace.tags + + const llmobsTraceId = + registry.get(parent)?.[TRACE_ID] ?? + normalizeLlmObsTraceId(traceTags[PROPAGATED_TRACE_ID_KEY]) ?? + generateLlmObsTraceId(span._startTime) + this._setTag(span, TRACE_ID, llmobsTraceId) + // When the registering span sits below an OTel `gen_ai.*` ancestor, use // that ancestor as the parent_id fallback and suppress the bridge // parent_id tag so the indexer doesn't invert the trace. const genAIAncestorSpanId = findGenAIAncestorSpanId(span) - writeBridgeTags(span, { includeParentId: genAIAncestorSpanId === null }) + writeBridgeTags(span, { includeParentId: genAIAncestorSpanId === null, llmobsTraceId }) this._setTag(span, ML_APP, spanMlApp) @@ -141,7 +158,6 @@ class LLMObsTagger { if (modelName) this.tagModelName(span, modelName) if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider) - const traceTags = span.context()._trace.tags sessionId = sessionId || registry.get(parent)?.[SESSION_ID] || traceTags[SESSION_ID_TRACE_DEFAULT_KEY] || diff --git a/packages/dd-trace/src/llmobs/util.js b/packages/dd-trace/src/llmobs/util.js index 2cdf80d26f..a1e399575e 100644 --- a/packages/dd-trace/src/llmobs/util.js +++ b/packages/dd-trace/src/llmobs/util.js @@ -1,5 +1,6 @@ 'use strict' +const id = require('../id') const log = require('../log') const { LLMOBS_PARENT_ID_BRIDGE_KEY, @@ -7,6 +8,10 @@ const { SPAN_KINDS, } = require('./constants/tags') +const DECIMAL_TRACE_ID_REGEX = /^\d+$/ +const HEX_TRACE_ID_REGEX = /^[0-9a-f]{32}$/i +const MAX_UINT_64 = (1n << 64n) - 1n + // LLM I/O is overwhelmingly ASCII (English prompts and code). Walk once // looking for the first non-ASCII char; if there is none, hand the input // straight back. Otherwise pick up the slow path from the byte that needed @@ -312,12 +317,12 @@ function safeJsonParse (value, fallback) { // LLMObs root and hoists the gen_ai ancestors under it, inverting the trace. /** * @param {import('../opentracing/span')} span - * @param {{ includeParentId?: boolean }} [opts] + * @param {{ includeParentId?: boolean, llmobsTraceId?: string }} [opts] */ -function writeBridgeTags (span, { includeParentId = true } = {}) { +function writeBridgeTags (span, { includeParentId = true, llmobsTraceId } = {}) { const traceTags = span?.context?.()._trace?.tags if (!traceTags || traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY]) return - traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = span.context().toTraceId(true) + traceTags[LLMOBS_TRACE_ID_BRIDGE_KEY] = llmobsTraceId ?? span.context().toTraceId(true) if (includeParentId) { traceTags[LLMOBS_PARENT_ID_BRIDGE_KEY] = span.context().toSpanId() } @@ -362,6 +367,51 @@ function findGenAIAncestorSpanId (span) { return null } +/** + * Generate a 128-bit LLMObs trace ID with the span start time encoded in its high bits. + * @param {number} startTime + * @returns {string} + */ +function generateLlmObsTraceId (startTime) { + const identifier = id() + const traceIdHigh = Math.floor(startTime / 1000) + .toString(16) + .padStart(8, '0') + .padEnd(16, '0') + + return identifier.toTraceIdHex(traceIdHigh).padStart(32, '0') +} + +/** + * Convert an internally stored hexadecimal LLMObs trace ID to its distributed wire representation. + * @param {string | undefined} traceId + * @returns {string | undefined} + */ +function llmObsTraceIdToWire (traceId) { + if (!traceId) return + if (!HEX_TRACE_ID_REGEX.test(traceId)) return traceId + + return BigInt(`0x${traceId}`).toString(10) +} + +/** + * Normalize a distributed LLMObs trace ID to the representation expected by LLMObs span events. + * @param {string | undefined} traceId + * @returns {string | undefined} + */ +function normalizeLlmObsTraceId (traceId) { + if (!traceId) return + + if (HEX_TRACE_ID_REGEX.test(traceId) && (traceId[0] === '0' || !DECIMAL_TRACE_ID_REGEX.test(traceId))) { + return traceId + } + + if (!DECIMAL_TRACE_ID_REGEX.test(traceId)) return traceId + + const identifier = BigInt(traceId) + return identifier > MAX_UINT_64 ? identifier.toString(16).padStart(32, '0') : traceId +} + // Maps an audio `format` (e.g. "wav", "mp3") to a MIME type. Defaults to `audio/wav` when the // format is missing. Provider-specific overrides (e.g. OpenAI's mp3 -> audio/mpeg) are passed in // via `mimeTypeLookup` so this stays provider-agnostic. A non-string `format` is treated as missing @@ -396,6 +446,9 @@ module.exports = { audioMimeTypeFromFormat, encodeUnicode, findGenAIAncestorSpanId, + generateLlmObsTraceId, + llmObsTraceIdToWire, + normalizeLlmObsTraceId, formatAudioPart, validateCostTags, validateKind, diff --git a/packages/dd-trace/test/llmobs/index.spec.js b/packages/dd-trace/test/llmobs/index.spec.js index 573e8c89d4..da3020bfa6 100644 --- a/packages/dd-trace/test/llmobs/index.spec.js +++ b/packages/dd-trace/test/llmobs/index.spec.js @@ -10,7 +10,13 @@ const sinon = require('sinon') const { DD_MAJOR } = require('../../../../version') const { INCOMPATIBLE_INITIALIZATION } = require('../../src/llmobs/constants/text') const LLMObsTagger = require('../../src/llmobs/tagger') -const { SAMPLE_RATE, SAMPLING_DECISION, SESSION_ID } = require('../../src/llmobs/constants/tags') +const { + PROPAGATED_TRACE_ID_KEY, + SAMPLE_RATE, + SAMPLING_DECISION, + SESSION_ID, + TRACE_ID, +} = require('../../src/llmobs/constants/tags') const { getConfigFresh } = require('../helpers/config') const { removeDestroyHandler } = require('./util') @@ -189,6 +195,51 @@ describe('module', () => { ) }) + it('converts the local LLMObs trace id to decimal for propagation', () => { + llmobsModule.enable({ llmobs: { mlApp: 'test', agentlessEnabled: false } }) + store.span = { + context () { + return { + _trace: { tags: {} }, + toSpanId () { return 'parent-id' }, + } + }, + } + LLMObsTagger.tagMap.set(store.span, { + [TRACE_ID]: '6a5f76e7000000001973227978d8110b', + }) + + const carrier = { 'x-datadog-tags': '' } + injectCh.publish({ carrier }) + + assert.strictEqual( + carrier['x-datadog-tags'], + // eslint-disable-next-line @stylistic/max-len + '_dd.p.llmobs_parent_id=parent-id,_dd.p.llmobs_ml_app=test,_dd.p.llmobs_trace_id=141393847380800662846519802803680448779' + ) + }) + + it('forwards an extracted LLMObs trace id without reinterpreting it', () => { + llmobsModule.enable({ llmobs: { mlApp: 'test', agentlessEnabled: false } }) + const wireTraceId = '12345678901234567890123456789012' + store.span = { + context () { + return { + _trace: { tags: { [PROPAGATED_TRACE_ID_KEY]: wireTraceId } }, + toSpanId () { return 'parent-id' }, + } + }, + } + + const carrier = { 'x-datadog-tags': '' } + injectCh.publish({ carrier }) + + assert.strictEqual( + carrier['x-datadog-tags'], + `_dd.p.llmobs_parent_id=parent-id,_dd.p.llmobs_ml_app=test,_dd.p.llmobs_trace_id=${wireTraceId}` + ) + }) + it('does not inject LLMObs parent ID info when there is no parent LLMObs span', () => { llmobsModule.enable({ llmobs: { mlApp: 'test', agentlessEnabled: false } }) diff --git a/packages/dd-trace/test/llmobs/sdk/index.spec.js b/packages/dd-trace/test/llmobs/sdk/index.spec.js index 62c4d0394f..55720f9448 100644 --- a/packages/dd-trace/test/llmobs/sdk/index.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/index.spec.js @@ -15,6 +15,7 @@ const agent = require('../../plugins/agent') const { getConfigFresh } = require('../../helpers/config') const tracerVersion = require('../../../../../package.json').version const { removeDestroyHandler } = require('../util') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') const injectCh = channel('dd-trace:span:inject') @@ -262,8 +263,7 @@ describe('sdk', () => { }) describe('parentage', () => { - // TODO: need to implement custom trace IDs - it.skip('starts a span with a distinct trace id', () => { + it('starts a span with a distinct trace id', () => { llmobs.trace({ kind: 'workflow', name: 'test' }, span => { const traceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] assert.ok(traceId) @@ -278,9 +278,11 @@ describe('sdk', () => { LLMObsTagger.tagMap.get(innerLLMSpan)['_ml_obs.llmobs_parent_id'], outerLLMSpan.context().toSpanId() ) - // TODO: need to implement custom trace IDs - // expect(innerLLMSpan.context()._tags['_ml_obs.trace_id']) - // .to.equal(outerLLMSpan.context()._tags['_ml_obs.trace_id']) + + assert.equal( + LLMObsTagger.tagMap.get(innerLLMSpan)['_ml_obs.trace_id'], + LLMObsTagger.tagMap.get(outerLLMSpan)['_ml_obs.trace_id'] + ) }) }) }) @@ -307,17 +309,16 @@ describe('sdk', () => { }) }) - // TODO: need to implement custom trace IDs - it.skip('starts different traces for llmobs spans as child spans of an apm root span', () => { + it('starts different traces for llmobs spans as child spans of an apm root span', () => { let apmTraceId, traceId1, traceId2 tracer.trace('apmRootSpan', apmRootSpan => { apmTraceId = apmRootSpan.context().toTraceId(true) - llmobs.trace('workflow', llmobsSpan1 => { - traceId1 = llmobsSpan1.context().getTag('_ml_obs.trace_id') + llmobs.trace({ kind: 'workflow' }, llmobsSpan1 => { + traceId1 = LLMObsTagger.tagMap.get(llmobsSpan1)['_ml_obs.trace_id'] }) - llmobs.trace('workflow', llmobsSpan2 => { - traceId2 = llmobsSpan2.context().getTag('_ml_obs.trace_id') + llmobs.trace({ kind: 'workflow' }, llmobsSpan2 => { + traceId2 = LLMObsTagger.tagMap.get(llmobsSpan2)['_ml_obs.trace_id'] }) }) @@ -357,7 +358,7 @@ describe('sdk', () => { span = _span }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'workflow', @@ -373,8 +374,9 @@ describe('sdk', () => { it('writes llmobs_trace_id and llmobs_parent_id to _trace.tags on first sdk activate', () => { llmobs.trace({ kind: 'workflow', name: 'wf' }, span => { const traceTags = span.context()._trace.tags - assert.strictEqual(traceTags.llmobs_trace_id, span.context().toTraceId(true)) - assert.strictEqual(traceTags.llmobs_parent_id, span.context().toSpanId()) + const llmobsTraceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] + + assert.strictEqual(traceTags.llmobs_trace_id, llmobsTraceId) }) }) @@ -488,7 +490,7 @@ describe('sdk', () => { wrappedMyLLM('input') - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -508,7 +510,7 @@ describe('sdk', () => { wrappedMyEmbedding('input') - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'embedding', @@ -529,7 +531,7 @@ describe('sdk', () => { wrappedMyRetrieval('input') - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'retrieval', @@ -556,7 +558,7 @@ describe('sdk', () => { const wrappedMyWorkflow = llmobs.wrap({ kind: 'workflow' }, myWorkflow) wrappedMyWorkflow(circular) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'workflow', @@ -578,7 +580,7 @@ describe('sdk', () => { assert.throws(() => wrappedMyTask('foo', 'bar')) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'task', @@ -599,7 +601,7 @@ describe('sdk', () => { return wrappedMyTask('foo', 'bar') .catch(() => { - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'task', @@ -627,7 +629,7 @@ describe('sdk', () => { clock.tick(1000) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'workflow', @@ -655,7 +657,7 @@ describe('sdk', () => { clock.tick(1000) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'workflow', @@ -683,7 +685,7 @@ describe('sdk', () => { clock.tick(1000) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'workflow', @@ -739,17 +741,17 @@ describe('sdk', () => { }) describe('parentage', () => { - // TODO: need to implement custom trace IDs - it.skip('starts a span with a distinct trace id', () => { - const fn = llmobs.wrap('workflow', { name: 'test' }, () => { - const span = llmobs._active() - - const traceId = span.context().getTag('_ml_obs.trace_id') - assert.ok(traceId) - assert.notStrictEqual(traceId, span.context().toTraceId(true)) + it('starts a span with a distinct trace id', () => { + let span + const fn = llmobs.wrap({ kind: 'workflow', name: 'test' }, () => { + span = llmobs._active() }) fn() + + const llmobsTraceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] + assert.ok(llmobsTraceId) + assert.notStrictEqual(llmobsTraceId, span.context().toTraceId(true)) }) it('sets span parentage correctly', () => { @@ -766,9 +768,11 @@ describe('sdk', () => { LLMObsTagger.tagMap.get(innerLLMSpan)['_ml_obs.llmobs_parent_id'], outerLLMSpan.context().toSpanId() ) - // TODO: need to implement custom trace IDs - // expect(innerLLMSpan.context()._tags['_ml_obs.trace_id']) - // .to.equal(outerLLMSpan.context()._tags['_ml_obs.trace_id']) + + assert.equal( + LLMObsTagger.tagMap.get(innerLLMSpan)['_ml_obs.trace_id'], + LLMObsTagger.tagMap.get(outerLLMSpan)['_ml_obs.trace_id'] + ) } const outerWrapped = llmobs.wrap({ kind: 'workflow' }, outer) @@ -797,9 +801,11 @@ describe('sdk', () => { LLMObsTagger.tagMap.get(innerLLMObsSpan)['_ml_obs.llmobs_parent_id'], outerLLMObsSpan.context().toSpanId() ) - // TODO: need to implement custom trace IDs - // expect(innerLLMObsSpan.context()._tags['_ml_obs.trace_id']) - // .to.equal(outerLLMObsSpan.context()._tags['_ml_obs.trace_id']) + + assert.equal( + LLMObsTagger.tagMap.get(innerLLMObsSpan)['_ml_obs.trace_id'], + LLMObsTagger.tagMap.get(outerLLMObsSpan)['_ml_obs.trace_id'] + ) } const outerWrapped = llmobs.wrap({ kind: 'workflow' }, outerLLMObs) @@ -809,8 +815,7 @@ describe('sdk', () => { outerWrapped() }) - // TODO: need to implement custom trace IDs - it.skip('starts different traces for llmobs spans as child spans of an apm root span', () => { + it('starts different traces for llmobs spans as child spans of an apm root span', () => { let traceId1, traceId2, apmTraceId function apm () { apmTraceId = tracer.scope().active().context().toTraceId(true) @@ -887,7 +892,7 @@ describe('sdk', () => { fn() - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'workflow', @@ -922,7 +927,7 @@ describe('sdk', () => { assert.throws(() => llmobs.annotate(span)) // span should still exist in the registry, just with no annotations - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1002,7 +1007,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ inputData, outputData }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1025,7 +1030,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ inputData, outputData }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1056,7 +1061,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'embedding', name: 'test' }, span => { llmobs.annotate({ inputData, outputData }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'embedding', @@ -1075,7 +1080,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'retrieval', name: 'test' }, span => { llmobs.annotate({ inputData, outputData }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'retrieval', @@ -1093,7 +1098,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ metadata }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1110,7 +1115,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ metrics }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1127,7 +1132,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1144,7 +1149,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags, costTags: ['team', 'feature'] }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1167,7 +1172,7 @@ describe('sdk', () => { costTags: ['feature', 'project'], }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1183,7 +1188,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags: { team: 'ml' }, costTags: ['team', 'missing', 123] }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1199,7 +1204,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags: { team: 'ml' }, costTags: 'team' }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1214,7 +1219,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags: { team: 'ml' }, costTags: [] }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1229,7 +1234,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags: { team: 'ml' }, costTags: null }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1249,7 +1254,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ toolDefinitions }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1268,7 +1273,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ toolDefinitions }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1288,7 +1293,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ toolDefinitions }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1322,7 +1327,7 @@ describe('sdk', () => { it('applies costTags to spans created in the context', () => { llmobs.annotationContext({ tags: { team: 'ml', feature: 'chatbot' }, costTags: ['team', 'feature'] }, () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1340,7 +1345,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ tags: { team: 'ml' } }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { + assertObjectContains(LLMObsTagger.tagMap.get(span), { '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', '_ml_obs.meta.span.kind': 'llm', @@ -1361,14 +1366,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'llm', name: 'test' }, span => { llmobs.annotate({ toolDefinitions }) - assert.deepStrictEqual(LLMObsTagger.tagMap.get(span), { - '_ml_obs.sample_rate': '1', - '_ml_obs.sampling_decision': '1', - '_ml_obs.meta.span.kind': 'llm', - '_ml_obs.meta.ml_app': 'mlApp', - '_ml_obs.llmobs_parent_id': 'undefined', - '_ml_obs.meta.tool_definitions': toolDefinitions, - }) + assert.deepStrictEqual(LLMObsTagger.tagMap.get(span)['_ml_obs.meta.tool_definitions'], toolDefinitions) }) }) @@ -1399,7 +1397,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'workflow', name: 'test' }, span => { const spanCtx = llmobs.exportSpan(span) - const traceId = span.context().toTraceId(true) + const traceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] const spanId = span.context().toSpanId() assert.deepStrictEqual(spanCtx, { traceId, spanId }) @@ -1410,7 +1408,7 @@ describe('sdk', () => { llmobs.trace({ kind: 'workflow', name: 'test' }, span => { const spanCtx = llmobs.exportSpan() - const traceId = span.context().toTraceId(true) + const traceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] const spanId = span.context().toSpanId() assert.deepStrictEqual(spanCtx, { traceId, spanId }) @@ -1422,23 +1420,13 @@ describe('sdk', () => { tracer.trace('apmSpan', () => { const spanCtx = llmobs.exportSpan() - const traceId = llmobsSpan.context().toTraceId(true) + const traceId = LLMObsTagger.tagMap.get(llmobsSpan)['_ml_obs.trace_id'] const spanId = llmobsSpan.context().toSpanId() assert.deepStrictEqual(spanCtx, { traceId, spanId }) }) }) }) - - it('returns undefined if the provided span is not a span', () => { - llmobs.trace({ kind: 'workflow', name: 'test' }, fakeSpan => { - fakeSpan.context().toTraceId = undefined // something that would throw - LLMObsTagger.tagMap.set(fakeSpan, {}) - const spanCtx = llmobs.exportSpan(fakeSpan) - - assert.strictEqual(spanCtx, undefined) - }) - }) }) describe('submitEvaluation', () => { @@ -1803,19 +1791,23 @@ describe('sdk', () => { describe('distributed', () => { it('adds the current llmobs span id and sampling decision to the injection context', () => { const carrier = { 'x-datadog-tags': '' } - let parentId, span + let parentId, span, traceId llmobs.trace({ kind: 'workflow', name: 'myWorkflow' }, _span => { span = _span parentId = span.context().toSpanId() + traceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] // simulate injection from http integration or from tracer // something that triggers the text_map injection injectCh.publish({ carrier }) }) + const wireTraceId = BigInt(`0x${traceId}`).toString(10) + assert.strictEqual( carrier['x-datadog-tags'], - `_dd.p.llmobs_parent_id=${parentId},_dd.p.llmobs_ml_app=mlApp,_dd.p.llmobs_sr=1,_dd.p.llmobs_sd=1` + // eslint-disable-next-line @stylistic/max-len + `_dd.p.llmobs_parent_id=${parentId},_dd.p.llmobs_ml_app=mlApp,_dd.p.llmobs_sr=1,_dd.p.llmobs_sd=1,_dd.p.llmobs_trace_id=${wireTraceId}` ) }) }) diff --git a/packages/dd-trace/test/llmobs/sdk/integration.spec.js b/packages/dd-trace/test/llmobs/sdk/integration.spec.js index 59436b98c5..be0affa2a6 100644 --- a/packages/dd-trace/test/llmobs/sdk/integration.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/integration.spec.js @@ -166,19 +166,17 @@ describe('end to end sdk integration tests', () => { describe('otel correlation bridge tags', () => { it('writes llmobs_trace_id, llmobs_parent_id, and _dd.llmobs.submitted to apm span meta', async () => { - let workflowSpanCtx llmobs.trace({ kind: 'workflow', name: 'wf' }, span => { - workflowSpanCtx = { traceId: span.context().toTraceId(true), spanId: span.context().toSpanId() } llmobs.trace({ kind: 'task', name: 'inner' }, () => {}) }) - const { apmSpans } = await getEvents(2) + const { apmSpans, llmobsSpans } = await getEvents(2) assert.equal(apmSpans.length, 2) // The first span in the chunk carries _trace.tags, including the bridge tags. const firstSpan = apmSpans[0] - assert.equal(firstSpan.meta.llmobs_trace_id, workflowSpanCtx.traceId) - assert.equal(firstSpan.meta.llmobs_parent_id, workflowSpanCtx.spanId) + assert.equal(firstSpan.meta.llmobs_trace_id, llmobsSpans[0].trace_id) + assert.equal(firstSpan.meta.llmobs_parent_id, llmobsSpans[0].span_id) // Every SDK-tagged apm span carries the submitted marker. for (const apmSpan of apmSpans) { @@ -224,6 +222,8 @@ describe('end to end sdk integration tests', () => { assert.equal(getTag(llmobsSpans[0], 'ml_app'), 'test') assert.equal(getTag(llmobsSpans[1], 'ml_app'), 'test') + assert.equal(llmobsSpans[0].trace_id, llmobsSpans[1].trace_id) + assert.match(llmobsSpans[0].trace_id, /^[0-9a-f]{32}$/) }) it('injects the local mlApp', async () => { diff --git a/packages/dd-trace/test/llmobs/span_processor.spec.js b/packages/dd-trace/test/llmobs/span_processor.spec.js index b6892f5f20..0311a671b5 100644 --- a/packages/dd-trace/test/llmobs/span_processor.spec.js +++ b/packages/dd-trace/test/llmobs/span_processor.spec.js @@ -75,13 +75,14 @@ describe('span processor', () => { '_ml_obs.llmobs_parent_id': '1234', '_ml_obs.sample_rate': '1', '_ml_obs.sampling_decision': '1', + '_ml_obs.trace_id': 'mlob123', }) processor.process(span) const payload = writer.append.getCall(0).firstArg assert.deepStrictEqual(payload, { - trace_id: '123', + trace_id: 'mlob123', span_id: '456', parent_id: '1234', name: 'test', @@ -116,6 +117,7 @@ describe('span processor', () => { span_id: '456', sample_rate: '1', sampling_decision: '1', + apm_trace_id: '123', }, }) diff --git a/packages/dd-trace/test/llmobs/tagger.spec.js b/packages/dd-trace/test/llmobs/tagger.spec.js index 615f364b8d..fe5aef758d 100644 --- a/packages/dd-trace/test/llmobs/tagger.spec.js +++ b/packages/dd-trace/test/llmobs/tagger.spec.js @@ -6,7 +6,8 @@ const { beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') const { INPUT_PROMPT } = require('../../src/llmobs/constants/tags') -const { writeBridgeTags, findGenAIAncestorSpanId } = require('../../src/llmobs/util') +const { writeBridgeTags, findGenAIAncestorSpanId, normalizeLlmObsTraceId } = require('../../src/llmobs/util') +const { assertObjectContains } = require('../../../../integration-tests/helpers') function unserializableObject () { const obj = {} @@ -43,9 +44,10 @@ describe('tagger', () => { // existing tests get the "no gen_ai ancestor" branch; individual tests // can call `.returns(id)` on the stub to exercise suppression. util = { - generateTraceId: sinon.stub().returns('0123'), + generateLlmObsTraceId: sinon.stub().returns('0123456789abcdef0123456789abcdef'), writeBridgeTags, findGenAIAncestorSpanId: sinon.stub().returns(null), + normalizeLlmObsTraceId, } logger = { @@ -74,7 +76,7 @@ describe('tagger', () => { it('tags an llm obs span with basic and default properties', () => { tagger.registerLLMObsSpan(span, { kind: 'workflow' }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'workflow', '_ml_obs.meta.ml_app': 'my-default-ml-app', '_ml_obs.llmobs_parent_id': 'undefined', // no parent id provided @@ -92,7 +94,7 @@ describe('tagger', () => { mlApp: 'my-app', }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'llm', '_ml_obs.meta.model_name': 'my-model', '_ml_obs.meta.model_provider': 'my-provider', @@ -149,7 +151,7 @@ describe('tagger', () => { it('uses the name if provided', () => { tagger.registerLLMObsSpan(span, { kind: 'llm', name: 'my-span-name' }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'llm', '_ml_obs.meta.ml_app': 'my-default-ml-app', '_ml_obs.llmobs_parent_id': 'undefined', @@ -162,7 +164,7 @@ describe('tagger', () => { it('defaults parent id to undefined', () => { tagger.registerLLMObsSpan(span, { kind: 'llm' }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'llm', '_ml_obs.meta.ml_app': 'my-default-ml-app', '_ml_obs.llmobs_parent_id': 'undefined', @@ -189,7 +191,7 @@ describe('tagger', () => { // The parent carries no sampling decision, so the child inherits none // (it does not start a fresh decision mid-trace). - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'llm', '_ml_obs.meta.ml_app': 'my-ml-app', '_ml_obs.session_id': 'my-session', @@ -198,15 +200,14 @@ describe('tagger', () => { }) it('uses the propagated trace id if provided', () => { + spanContext._trace.tags['_dd.p.llmobs_trace_id'] = '141393847380800662846519802803680448779' + tagger.registerLLMObsSpan(span, { kind: 'llm' }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { - '_ml_obs.meta.span.kind': 'llm', - '_ml_obs.meta.ml_app': 'my-default-ml-app', - '_ml_obs.llmobs_parent_id': 'undefined', - '_ml_obs.sample_rate': '1', - '_ml_obs.sampling_decision': '1', - }) + assert.strictEqual( + Tagger.tagMap.get(span)['_ml_obs.trace_id'], + '6a5f76e7000000001973227978d8110b' + ) }) it('uses the propagated parent id if provided', () => { @@ -215,7 +216,7 @@ describe('tagger', () => { tagger.registerLLMObsSpan(span, { kind: 'llm' }) // Propagated parent with no propagated sampling info: inherit none. - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'llm', '_ml_obs.meta.ml_app': 'my-default-ml-app', '_ml_obs.llmobs_parent_id': '-567', @@ -228,6 +229,13 @@ describe('tagger', () => { assert.strictEqual(Tagger.tagMap.get(span), undefined) }) + it('creates a custom trace id', () => { + tagger.registerLLMObsSpan(span, { kind: 'workflow' }) + const llmobsTraceId = Tagger.tagMap.get(span)['_ml_obs.trace_id'] + assert.strictEqual(llmobsTraceId, '0123456789abcdef0123456789abcdef') + assert.notEqual(llmobsTraceId, span.context().toTraceId(true)) + }) + describe('sampling', () => { it('records a SAMPLED decision and the rate on a root span when sampleRate is 1', () => { tagger.registerLLMObsSpan(span, { kind: 'llm' }) @@ -364,7 +372,7 @@ describe('tagger', () => { it('writes llmobs_trace_id and llmobs_parent_id to _trace.tags after a successful register', () => { tagger.registerLLMObsSpan(span, { kind: 'workflow' }) - assert.strictEqual(spanContext._trace.tags.llmobs_trace_id, '00000000000000001111111111111111') + assert.strictEqual(spanContext._trace.tags.llmobs_trace_id, Tagger.tagMap.get(span)['_ml_obs.trace_id']) assert.strictEqual(spanContext._trace.tags.llmobs_parent_id, '2222222222222222') }) @@ -381,7 +389,7 @@ describe('tagger', () => { tagger.registerLLMObsSpan(secondSpan, { kind: 'task' }) - assert.strictEqual(spanContext._trace.tags.llmobs_trace_id, '00000000000000001111111111111111') + assert.strictEqual(spanContext._trace.tags.llmobs_trace_id, Tagger.tagMap.get(span)['_ml_obs.trace_id']) assert.strictEqual(spanContext._trace.tags.llmobs_parent_id, '2222222222222222') }) @@ -417,7 +425,7 @@ describe('tagger', () => { it('writes llmobs_trace_id but omits llmobs_parent_id', () => { tagger.registerLLMObsSpan(span, { kind: 'llm' }) - assert.strictEqual(spanContext._trace.tags.llmobs_trace_id, '00000000000000001111111111111111') + assert.strictEqual(spanContext._trace.tags.llmobs_trace_id, Tagger.tagMap.get(span)['_ml_obs.trace_id']) assert.strictEqual(spanContext._trace.tags.llmobs_parent_id, undefined) }) @@ -450,9 +458,10 @@ describe('tagger', () => { RealTagger = proxyquire('../../src/llmobs/tagger', { '../log': { warn () {} }, './util': { - generateTraceId: sinon.stub().returns('0123'), + generateLlmObsTraceId: sinon.stub().returns('0123456789abcdef0123456789abcdef'), writeBridgeTags, findGenAIAncestorSpanId, + normalizeLlmObsTraceId, }, }) realTagger = new RealTagger({ llmobs: { DD_LLMOBS_ENABLED: true, mlApp: 'test-app' } }) @@ -490,7 +499,7 @@ describe('tagger', () => { realTagger.registerLLMObsSpan(leafSpan, { kind: 'llm' }) - assert.strictEqual(traceTags.llmobs_trace_id, '00000000000000009999999999999999') + assert.strictEqual(traceTags.llmobs_trace_id, RealTagger.tagMap.get(leafSpan)['_ml_obs.trace_id']) assert.strictEqual(traceTags.llmobs_parent_id, undefined) assert.strictEqual(RealTagger.tagMap.get(leafSpan)['_ml_obs.llmobs_parent_id'], genAISpanId) }) @@ -502,7 +511,7 @@ describe('tagger', () => { it('tags a span with metadata', () => { tagger._register(span) tagger.tagMetadata(span, { a: 'foo', b: 'bar' }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.metadata': { a: 'foo', b: 'bar' }, }) }) @@ -510,7 +519,7 @@ describe('tagger', () => { it('updates instead of overriding', () => { Tagger.tagMap.set(span, { '_ml_obs.meta.metadata': { a: 'foo' } }) tagger.tagMetadata(span, { b: 'bar' }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.metadata': { a: 'foo', b: 'bar' }, }) }) @@ -520,7 +529,7 @@ describe('tagger', () => { it('tags a span with metrics', () => { tagger._register(span) tagger.tagMetrics(span, { a: 1, b: 2 }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.metrics': { a: 1, b: 2 }, }) }) @@ -533,7 +542,7 @@ describe('tagger', () => { totalTokens: 3, foo: 10, }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.metrics': { input_tokens: 1, output_tokens: 2, total_tokens: 3, foo: 10 }, }) }) @@ -552,7 +561,7 @@ describe('tagger', () => { it('updates instead of overriding', () => { Tagger.tagMap.set(span, { '_ml_obs.metrics': { a: 1 } }) tagger.tagMetrics(span, { b: 2 }) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.metrics': { a: 1, b: 2 }, }) }) @@ -570,7 +579,7 @@ describe('tagger', () => { ] tagger._register(span) tagger.tagToolDefinitions(span, toolDefinitions) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.tool_definitions': toolDefinitions, }) }) @@ -578,7 +587,7 @@ describe('tagger', () => { it('tags a span with only a name', () => { tagger._register(span) tagger.tagToolDefinitions(span, [{ name: 'get_time' }]) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.tool_definitions': [{ name: 'get_time' }], }) }) @@ -588,7 +597,7 @@ describe('tagger', () => { tagger.tagToolDefinitions(span, [ { name: 'get_weather', description: 123, schema: 'not-an-object', version: 456 }, ]) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.tool_definitions': [{ name: 'get_weather' }], }) }) @@ -599,7 +608,7 @@ describe('tagger', () => { { description: 'no name' }, { name: 'valid_tool' }, ]) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.tool_definitions': [{ name: 'valid_tool' }], }) }) @@ -625,7 +634,7 @@ describe('tagger', () => { const tags = { foo: 'bar' } tagger._register(span) tagger.tagSpanTags(span, tags) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.tags': { foo: 'bar' }, }) }) @@ -634,7 +643,7 @@ describe('tagger', () => { Tagger.tagMap.set(span, { '_ml_obs.tags': { a: 1 } }) const tags = { a: 2, b: 1 } tagger.tagSpanTags(span, tags) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.tags': { a: 2, b: 1 }, }) }) @@ -717,7 +726,7 @@ describe('tagger', () => { tagger._register(span) tagger.tagLLMIO(span, inputData, outputData) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.messages': [ { content: 'you are an amazing assistant', role: '' }, { content: 'hello! my name is foobar', role: '' }, @@ -765,7 +774,7 @@ describe('tagger', () => { tagger._register(span) tagger.tagLLMIO(span, inputData, outputData) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.messages': [ { content: 'hello', @@ -837,7 +846,7 @@ describe('tagger', () => { tagger._register(span) tagger.tagLLMIO(span, inputData) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.messages': [ { content: 'hello', @@ -909,7 +918,7 @@ describe('tagger', () => { tagger._register(span) tagger.tagLLMIO(span, messages, undefined) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.messages': [ { role: 'tool', content: 'The weather in San Francisco is sunny', tool_id: '123' }, ], @@ -1064,7 +1073,7 @@ describe('tagger', () => { const outputData = 'embedded documents' tagger._register(span) tagger.tagEmbeddingIO(span, inputData, outputData) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.documents': [ { text: 'my string document' }, { text: 'my object document' }, @@ -1131,7 +1140,7 @@ describe('tagger', () => { tagger._register(span) tagger.tagRetrievalIO(span, inputData, outputData) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.value': 'some query', '_ml_obs.meta.output.documents': [ { text: 'result 1' }, @@ -1165,7 +1174,7 @@ describe('tagger', () => { const outputData = 'some text' tagger._register(span) tagger.tagTextIO(span, inputData, outputData) - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.input.value': '{"some":"object"}', '_ml_obs.meta.output.value': 'some text', }) @@ -1181,20 +1190,20 @@ describe('tagger', () => { it('changes the span kind', () => { tagger._register(span) tagger._setTag(span, '_ml_obs.meta.span.kind', 'old-kind') - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'old-kind', }) tagger.changeKind(span, 'new-kind') - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'new-kind', }) }) it('sets the kind if it is not already set', () => { tagger._register(span) - assert.deepStrictEqual(Tagger.tagMap.get(span), {}) + assertObjectContains(Tagger.tagMap.get(span), {}) tagger.changeKind(span, 'new-kind') - assert.deepStrictEqual(Tagger.tagMap.get(span), { + assertObjectContains(Tagger.tagMap.get(span), { '_ml_obs.meta.span.kind': 'new-kind', }) }) diff --git a/packages/dd-trace/test/llmobs/util.spec.js b/packages/dd-trace/test/llmobs/util.spec.js index 2fd3d290dd..cdb7c7a247 100644 --- a/packages/dd-trace/test/llmobs/util.spec.js +++ b/packages/dd-trace/test/llmobs/util.spec.js @@ -9,8 +9,11 @@ const { audioMimeTypeFromFormat, encodeUnicode, findGenAIAncestorSpanId, + generateLlmObsTraceId, + llmObsTraceIdToWire, formatAudioPart, getFunctionArguments, + normalizeLlmObsTraceId, validateCostTags, safeJsonParse, validateKind, @@ -19,6 +22,48 @@ const { } = require('../../src/llmobs/util') describe('util', () => { + describe('LLMObs trace id propagation', () => { + const traceId = '6a5f76e7000000001973227978d8110b' + const wireTraceId = '141393847380800662846519802803680448779' + + it('converts a canonical hexadecimal trace id to decimal for propagation', () => { + assert.strictEqual(llmObsTraceIdToWire(traceId), wireTraceId) + }) + + it('normalizes a propagated decimal trace id to canonical hexadecimal', () => { + assert.strictEqual(normalizeLlmObsTraceId(wireTraceId), traceId) + }) + + it('preserves 64-bit decimal trace ids and converts the first 128-bit value', () => { + assert.strictEqual(normalizeLlmObsTraceId('18446744073709551615'), '18446744073709551615') + assert.strictEqual( + normalizeLlmObsTraceId('18446744073709551616'), + '00000000000000010000000000000000' + ) + }) + + it('preserves hexadecimal and custom trace ids while normalizing', () => { + assert.strictEqual(normalizeLlmObsTraceId(traceId), traceId) + assert.strictEqual(normalizeLlmObsTraceId('custom-trace-id'), 'custom-trace-id') + }) + + it('preserves custom trace ids for propagation', () => { + assert.strictEqual(llmObsTraceIdToWire('custom-trace-id'), 'custom-trace-id') + }) + + it('returns undefined for empty trace ids', () => { + assert.strictEqual(llmObsTraceIdToWire(''), undefined) + assert.strictEqual(normalizeLlmObsTraceId(''), undefined) + }) + + it('generates a 128-bit trace id containing the start time', () => { + const generatedTraceId = generateLlmObsTraceId(1_700_000_000_000) + + assert.match(generatedTraceId, /^[0-9a-f]{32}$/) + assert.strictEqual(generatedTraceId.slice(0, 16), '6553f10000000000') + }) + }) + describe('encodeUnicode', () => { it('should encode unicode characters', () => { assert.strictEqual(encodeUnicode('😀'), '\\ud83d\\ude00') From d4a09f52df54262f0d79137d195e9f2bf5a211bb Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 23 Jul 2026 20:33:35 -0600 Subject: [PATCH 6/9] fix(openfeature): allow custom agentless endpoints (#9481) * fix(openfeature): support custom agentless endpoints * fix(openfeature): never send API keys to custom endpoints * fix(openfeature): clarify default endpoint error --- .../openfeature-configuration-sources.spec.js | 2 +- .../agentless_configuration_source.js | 9 ++-- .../src/openfeature/configuration_source.js | 13 ++--- .../agentless_configuration_source.spec.js | 28 +++++++++-- .../openfeature/configuration_source.spec.js | 50 ++++++++++++------- 5 files changed, 64 insertions(+), 38 deletions(-) diff --git a/integration-tests/openfeature/openfeature-configuration-sources.spec.js b/integration-tests/openfeature/openfeature-configuration-sources.spec.js index 566c36639b..9923933241 100644 --- a/integration-tests/openfeature/openfeature-configuration-sources.spec.js +++ b/integration-tests/openfeature/openfeature-configuration-sources.spec.js @@ -341,7 +341,7 @@ function assertDeliveryTraffic (testCase) { for (const request of cdnRequests) { assert.strictEqual(request.url, `${AGENTLESS_PATH}?case=${testCase.identifier}`, testCase.label) assert.strictEqual(request.headers['accept-encoding'], 'gzip', testCase.label) - assert.strictEqual(request.headers['dd-api-key'], 'integration-api-key', testCase.label) + assert.strictEqual(request.headers['dd-api-key'], undefined, testCase.label) assert.strictEqual(request.headers['dd-client-library-language'], 'nodejs', testCase.label) assert.strictEqual(request.headers['dd-client-library-version'], VERSION, testCase.label) } diff --git a/packages/dd-trace/src/openfeature/agentless_configuration_source.js b/packages/dd-trace/src/openfeature/agentless_configuration_source.js index fc50bf1711..fd86a16acc 100644 --- a/packages/dd-trace/src/openfeature/agentless_configuration_source.js +++ b/packages/dd-trace/src/openfeature/agentless_configuration_source.js @@ -20,7 +20,7 @@ const RETRY_JITTER = 0.2 * @property {URL} endpoint * @property {number} pollIntervalMs * @property {number} requestTimeoutMs - * @property {string} apiKey + * @property {string | undefined} apiKey */ /** @@ -138,7 +138,7 @@ class AgentlessConfigurationSource { #request (signal) { const headers = getClientLibraryHeaders() headers['Accept-Encoding'] = 'gzip' - headers['DD-API-KEY'] = this.#config.apiKey + if (this.#config.apiKey) headers['DD-API-KEY'] = this.#config.apiKey if (this.#etag) headers['If-None-Match'] = this.#etag /** @@ -228,10 +228,7 @@ class AgentlessConfigurationSource { this.#failureWarnings.add(category) if (statusCode === 401 || statusCode === 403) { - log.warn( - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', - statusCode - ) + log.warn('Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', statusCode) } else if (statusCode) { log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, attempts) } else if (attempts > 1) { diff --git a/packages/dd-trace/src/openfeature/configuration_source.js b/packages/dd-trace/src/openfeature/configuration_source.js index 0bd7327953..4510cfdf79 100644 --- a/packages/dd-trace/src/openfeature/configuration_source.js +++ b/packages/dd-trace/src/openfeature/configuration_source.js @@ -1,6 +1,5 @@ 'use strict' -const { isLoopbackHost } = require('../exporters/common/url') const log = require('../log') const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server' @@ -27,9 +26,11 @@ function create (config, applyConfiguration) { return } + const hasCustomEndpoint = Boolean(baseUrl?.trim()) + try { - if (!config.DD_API_KEY) { - throw new Error('DD_API_KEY is required for Feature Flagging agentless delivery') + if (!hasCustomEndpoint && !config.DD_API_KEY) { + throw new Error('DD_API_KEY is required for the default Datadog Feature Flagging endpoint') } const AgentlessConfigurationSource = require('./agentless_configuration_source') @@ -37,7 +38,7 @@ function create (config, applyConfiguration) { endpoint: endpoint(config, baseUrl), pollIntervalMs: Math.min(pollIntervalSeconds, MAX_POLL_INTERVAL_SECONDS) * 1000, requestTimeoutMs: requestTimeoutSeconds * 1000, - apiKey: config.DD_API_KEY, + apiKey: hasCustomEndpoint ? undefined : config.DD_API_KEY, }, applyConfiguration) } catch (error) { log.error('Unable to configure Feature Flagging configuration source', error) @@ -74,10 +75,6 @@ function endpoint (config, configuredBaseUrl) { if (url.protocol !== 'https:' && url.protocol !== 'http:') { throw new Error('Feature Flagging agentless URL must use HTTP or HTTPS') } - if (url.protocol === 'http:' && !isLoopbackHost(url.hostname)) { - throw new Error('Feature Flagging agentless URL must use HTTPS unless it targets loopback') - } - if (url.pathname === '' || url.pathname === '/') { url.pathname = DEFAULT_AGENTLESS_PATH } diff --git a/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js b/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js index d8816abb8f..11d8032d82 100644 --- a/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/agentless_configuration_source.spec.js @@ -181,13 +181,15 @@ describe('AgentlessConfigurationSource', () => { clock.restore() clock = undefined const body = zlib.gzipSync(responseBody()) - nock('http://127.0.0.1:8080', { + config.endpoint = new URL('http://flags.dev.internal:8080/custom/ufc') + delete config.apiKey + nock('http://flags.dev.internal:8080', { + badheaders: ['dd-api-key'], reqheaders: { 'accept-encoding': 'gzip', - 'dd-api-key': 'test-api-key', }, }) - .get('/api/v2/feature-flagging/config/rules-based/server') + .get('/custom/ufc') .reply(200, body, { 'content-encoding': 'gzip', etag: '"real-path"', @@ -414,7 +416,7 @@ describe('AgentlessConfigurationSource', () => { 'third', ]) assert.deepStrictEqual(log.warn.thirdCall.args, [ - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', + 'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', 401, ]) }) @@ -480,7 +482,7 @@ describe('AgentlessConfigurationSource', () => { assert.strictEqual(requests.length, 1) sinon.assert.calledOnceWithExactly( log.warn, - 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid', + 'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', 401 ) @@ -491,6 +493,22 @@ describe('AgentlessConfigurationSource', () => { sinon.assert.notCalled(applyConfiguration) }) + it('omits a missing API key and reports the endpoint authentication failure', async () => { + delete config.apiKey + responses.push({ statusCode: 401 }) + + source().start() + await flush() + + assert.strictEqual(Object.hasOwn(requests[0].options.headers, 'DD-API-KEY'), false) + sinon.assert.calledOnceWithExactly( + log.warn, + 'Feature Flagging agentless endpoint returned HTTP %d; verify endpoint authentication', + 401 + ) + sinon.assert.notCalled(applyConfiguration) + }) + it('uses fixed-delay polling after a request completes', async () => { responses.push( { pending: true }, diff --git a/packages/dd-trace/test/openfeature/configuration_source.spec.js b/packages/dd-trace/test/openfeature/configuration_source.spec.js index 8ac81dba43..17aa2ae9b3 100644 --- a/packages/dd-trace/test/openfeature/configuration_source.spec.js +++ b/packages/dd-trace/test/openfeature/configuration_source.spec.js @@ -86,21 +86,37 @@ describe('OpenFeature configuration source', () => { it(`allows the loopback endpoint ${baseUrl}`, () => { config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = baseUrl + const resolved = createSourceConfig() assert.strictEqual( - createSourceConfig().endpoint.toString(), + resolved.endpoint.toString(), `${baseUrl}/api/v2/feature-flagging/config/rules-based/server` ) + assert.strictEqual(resolved.apiKey, undefined) }) } + it('allows a cleartext custom endpoint for local development and proxies', () => { + config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = + 'http://flags.dev.internal:8080' + + const resolved = createSourceConfig() + assert.strictEqual( + resolved.endpoint.toString(), + 'http://flags.dev.internal:8080/api/v2/feature-flagging/config/rules-based/server' + ) + assert.strictEqual(resolved.apiKey, undefined) + }) + it('preserves an exact configured path and query', () => { config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = 'https://example.com/custom/ufc?tenant=one' + const resolved = createSourceConfig() assert.strictEqual( - createSourceConfig().endpoint.toString(), + resolved.endpoint.toString(), 'https://example.com/custom/ufc?tenant=one' ) + assert.strictEqual(resolved.apiKey, undefined) }) it('derives and creates the managed GovCloud endpoint without hard-coding availability', () => { @@ -156,20 +172,6 @@ describe('OpenFeature configuration source', () => { sinon.assert.notCalled(AgentlessConfigurationSource) }) - it('rejects cleartext non-loopback endpoints', () => { - config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = 'http://flags.example.test' - - const source = configurationSource.create(config, sinon.spy()) - - assert.strictEqual(source, undefined) - sinon.assert.calledOnceWithMatch( - log.error, - 'Unable to configure Feature Flagging configuration source', - sinon.match.instanceOf(Error) - ) - sinon.assert.notCalled(AgentlessConfigurationSource) - }) - it('rejects malformed endpoints without logging their sensitive value', () => { const sentinel = 'sensitive-value' config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = `https://${sentinel} value` @@ -187,7 +189,19 @@ describe('OpenFeature configuration source', () => { assert.strictEqual(log.error.firstCall.args[1].cause, undefined) }) - it('requires an API key without enabling a source', () => { + it('creates a source for a custom API without a Datadog API key', () => { + delete config.DD_API_KEY + config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL = + 'https://flags.example.test/custom/ufc' + + const source = configurationSource.create(config, sinon.spy()) + + assert.ok(source instanceof AgentlessConfigurationSource) + assert.strictEqual(AgentlessConfigurationSource.firstCall.args[0].apiKey, undefined) + sinon.assert.notCalled(log.error) + }) + + it('requires a Datadog API key for the default Datadog Feature Flagging endpoint', () => { delete config.DD_API_KEY const source = configurationSource.create(config, sinon.spy()) @@ -195,7 +209,7 @@ describe('OpenFeature configuration source', () => { sinon.assert.calledOnceWithMatch( log.error, 'Unable to configure Feature Flagging configuration source', - sinon.match.instanceOf(Error) + sinon.match.has('message', 'DD_API_KEY is required for the default Datadog Feature Flagging endpoint') ) assert.strictEqual(source, undefined) sinon.assert.notCalled(AgentlessConfigurationSource) From c0877fc32de04e9204534557a599e2657b5a7737 Mon Sep 17 00:00:00 2001 From: Ilyas Shabi Date: Fri, 24 Jul 2026 09:08:28 +0200 Subject: [PATCH 7/9] fix(standalone): stamp _dd.apm.enabled on every span (#9506) --- integration-tests/appsec/standalone-asm.spec.js | 15 ++++++++++----- packages/dd-trace/src/span_processor.js | 2 +- packages/dd-trace/test/span_processor.spec.js | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/integration-tests/appsec/standalone-asm.spec.js b/integration-tests/appsec/standalone-asm.spec.js index cb5f57ffb7..e1fdcf5ac1 100644 --- a/integration-tests/appsec/standalone-asm.spec.js +++ b/integration-tests/appsec/standalone-asm.spec.js @@ -121,11 +121,16 @@ describe('Standalone ASM', () => { assert.ok(groups.indexOf(rootGroup) < groups.indexOf(outboundGroup)) assert.strictEqual(String(outboundSpan.parent_id), String(rootSpan.span_id)) - // Load-bearing: the delayed child chunk must carry the billing marker, - // even though its parent is a local (non-remote) span. - assert.strictEqual(rootSpan.metrics['_dd.apm.enabled'], 0) - assert.strictEqual(outboundGroup[0], outboundSpan) - assert.strictEqual(outboundSpan.metrics['_dd.apm.enabled'], 0) + // Load-bearing: every span in every chunk must carry the billing marker, + // including the delayed child whose parent is a local (non-remote) span. + for (const group of groups) { + for (const span of group) { + assert.strictEqual( + span.metrics['_dd.apm.enabled'], 0, + `span ${span.name}/${span.resource} missing _dd.apm.enabled:0` + ) + } + } }) it('should keep fifth req because RateLimiter allows 1 req/min', async () => { diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index b924220702..727631a108 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -62,7 +62,7 @@ class SpanProcessor { active.push(span) } else { const formattedSpan = spanFormat(span, isFirstSpanInChunk, this._processTags) - if (isFirstSpanInChunk && stampApmDisabled) { + if (stampApmDisabled) { formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0 } isFirstSpanInChunk = false diff --git a/packages/dd-trace/test/span_processor.spec.js b/packages/dd-trace/test/span_processor.spec.js index 6e15476db1..06433980cd 100644 --- a/packages/dd-trace/test/span_processor.spec.js +++ b/packages/dd-trace/test/span_processor.spec.js @@ -235,7 +235,7 @@ describe('SpanProcessor', () => { sinon.assert.calledWith(spanFormat.getCall(3), finishedSpan, false, processor._processTags) }) - it('should add APM disabled marker to first span in a chunk when APM tracing is disabled', () => { + it('should add APM disabled marker to every span in a chunk when APM tracing is disabled', () => { config.apmTracingEnabled = false config.flushMinSpans = 2 const processor = new SpanProcessor(exporter, prioritySampler, config) @@ -249,7 +249,7 @@ describe('SpanProcessor', () => { processor.process(finishedSpan) assert.strictEqual(firstFormatted.metrics[APM_TRACING_ENABLED_KEY], 0) - assert.ok(!Object.hasOwn(secondFormatted.metrics, APM_TRACING_ENABLED_KEY)) + assert.strictEqual(secondFormatted.metrics[APM_TRACING_ENABLED_KEY], 0) sinon.assert.calledWith(exporter.export, [firstFormatted, secondFormatted]) }) From 75f0ff166b1032c60b68c72dd400d8a9c5708a12 Mon Sep 17 00:00:00 2001 From: Pablo Erhard <104538390+pabloerhard@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:42:41 -0400 Subject: [PATCH 8/9] fix(webpack): opt out of typescript auto resolve (#9521) --- .../webpack/build-and-test-git-tags.js | 2 ++ .../webpack/build-and-test-minify.js | 2 ++ .../webpack/build-and-test-openfeature.js | 2 ++ .../webpack/build-and-test-skip-external.js | 2 ++ integration-tests/webpack/build.js | 2 ++ integration-tests/webpack/webpack-experiments.js | 16 ++++++++++++++++ 6 files changed, 26 insertions(+) create mode 100644 integration-tests/webpack/webpack-experiments.js diff --git a/integration-tests/webpack/build-and-test-git-tags.js b/integration-tests/webpack/build-and-test-git-tags.js index 08949ada3b..88fefe143b 100644 --- a/integration-tests/webpack/build-and-test-git-tags.js +++ b/integration-tests/webpack/build-and-test-git-tags.js @@ -9,6 +9,7 @@ const { spawnSync } = require('child_process') const assert = require('assert') const webpack = require('webpack') const DatadogWebpackPlugin = require('../../webpack') // dd-trace/webpack +const experiments = require('./webpack-experiments') const OUTFILE = path.join(__dirname, 'git-tags-out.js') @@ -17,6 +18,7 @@ const compiler = webpack({ entry: path.join(__dirname, 'basic-test.js'), target: 'node', externalsType: 'commonjs', + ...(experiments && { experiments }), output: { filename: 'git-tags-out.js', path: __dirname, diff --git a/integration-tests/webpack/build-and-test-minify.js b/integration-tests/webpack/build-and-test-minify.js index 6815d781b7..e664f518e3 100644 --- a/integration-tests/webpack/build-and-test-minify.js +++ b/integration-tests/webpack/build-and-test-minify.js @@ -8,6 +8,7 @@ const path = require('path') const assert = require('assert') const webpack = require('webpack') const DatadogWebpackPlugin = require('../../webpack') // dd-trace/webpack +const experiments = require('./webpack-experiments') const OUTFILE = path.join(__dirname, 'minify-out.js') @@ -17,6 +18,7 @@ try { // optimization.minimize is enabled by default in production mode entry: path.join(__dirname, 'basic-test.js'), target: 'node', + ...(experiments && { experiments }), output: { filename: 'minify-out.js', path: __dirname, diff --git a/integration-tests/webpack/build-and-test-openfeature.js b/integration-tests/webpack/build-and-test-openfeature.js index f1a74ced22..28842ca630 100644 --- a/integration-tests/webpack/build-and-test-openfeature.js +++ b/integration-tests/webpack/build-and-test-openfeature.js @@ -23,6 +23,7 @@ const assert = require('assert') const { execFileSync } = require('child_process') const webpack = require('webpack') const DatadogWebpackPlugin = require('../../webpack') // dd-trace/webpack +const experiments = require('./webpack-experiments') const ENTRY = path.join(__dirname, 'openfeature-app.js') const FLAGGING_PROVIDER = path.join('openfeature', 'flagging_provider') @@ -49,6 +50,7 @@ function build (outfile, plugins) { entry: ENTRY, target: 'node', externalsType: 'commonjs', + ...(experiments && { experiments }), output: { filename: path.basename(outfile), path: path.dirname(outfile), hashFunction: 'sha256' }, externals: EXTERNALS, plugins, diff --git a/integration-tests/webpack/build-and-test-skip-external.js b/integration-tests/webpack/build-and-test-skip-external.js index ff8e8307d7..be23bed01a 100644 --- a/integration-tests/webpack/build-and-test-skip-external.js +++ b/integration-tests/webpack/build-and-test-skip-external.js @@ -8,6 +8,7 @@ const path = require('path') const assert = require('assert') const webpack = require('webpack') const DatadogWebpackPlugin = require('../../webpack') // dd-trace/webpack +const experiments = require('./webpack-experiments') const OUTFILE = path.join(__dirname, 'skip-external-out.js') @@ -16,6 +17,7 @@ const compiler = webpack({ entry: path.join(__dirname, 'skip-external.js'), target: 'node', externalsType: 'commonjs', + ...(experiments && { experiments }), output: { filename: 'skip-external-out.js', path: __dirname, diff --git a/integration-tests/webpack/build.js b/integration-tests/webpack/build.js index 7e8d4277cd..4408aeb2b7 100644 --- a/integration-tests/webpack/build.js +++ b/integration-tests/webpack/build.js @@ -6,12 +6,14 @@ const path = require('path') const webpack = require('webpack') const DatadogWebpackPlugin = require('../../webpack') // dd-trace/webpack +const experiments = require('./webpack-experiments') const compiler = webpack({ mode: 'development', entry: path.join(__dirname, 'basic-test.js'), target: 'node', externalsType: 'commonjs', + ...(experiments && { experiments }), output: { filename: 'out.js', path: __dirname, diff --git a/integration-tests/webpack/webpack-experiments.js b/integration-tests/webpack/webpack-experiments.js new file mode 100644 index 0000000000..6483eeb2ba --- /dev/null +++ b/integration-tests/webpack/webpack-experiments.js @@ -0,0 +1,16 @@ +'use strict' + +const webpackPkg = require('webpack/package.json') + +// webpack 5.107.0 introduced `experiments.typescript` (opt-in), and 5.109.0 defaults it to +// "auto". On Node.js >= 22.6 (where `module.stripTypeScriptTypes` exists), "auto" turns the +// experiment on and sets `resolve.tsconfig = true`, making enhanced-resolve walk up to each +// resolved package's own tsconfig.json. Some published packages (e.g. `side-channel`) ship a +// tsconfig.json that `extends` a devDependency-only config package (`@ljharb/tsconfig`) that +// isn't installed, so resolution fails with a "Module not found" error unrelated to +// TypeScript. Explicitly disable the experiment where the option exists; older webpack +// versions don't recognize the key at all, so it must not be set for those. +const [major, minor] = webpackPkg.version.split('.').map(Number) +const supportsTypescriptExperiment = major > 5 || (major === 5 && minor >= 107) + +module.exports = supportsTypescriptExperiment ? { typescript: false } : undefined From 376fc9b3898eafb1a21568f16f3a8e3ded76f2ce Mon Sep 17 00:00:00 2001 From: sabrenner Date: Fri, 24 Jul 2026 16:29:39 +0000 Subject: [PATCH 9/9] v5.118.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 886c7d18c1..80df4d6843 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.117.0", + "version": "5.118.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts",