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..370a87db33 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,35 @@ 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 + + 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/.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 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/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/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/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 diff --git a/package.json b/package.json index 307fe575aa..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", @@ -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/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-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/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/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/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/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 6bdc55c9cd..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] || @@ -500,6 +516,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/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/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/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/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/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/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/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/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/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 0fc69388fc..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,12 +643,20 @@ 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 }, }) }) }) + 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) @@ -709,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: '' }, @@ -757,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', @@ -829,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', @@ -901,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' }, ], @@ -1056,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' }, @@ -1123,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' }, @@ -1157,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', }) @@ -1173,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') 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) 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/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/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/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/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() + } }) }) } 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]) }) 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 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==