diff --git a/.github/chainguard/release-proposal.sts.yaml b/.github/chainguard/release-proposal.sts.yaml index f62eae93954..eb9c2e25651 100644 --- a/.github/chainguard/release-proposal.sts.yaml +++ b/.github/chainguard/release-proposal.sts.yaml @@ -5,7 +5,6 @@ subject: repo:DataDog/dd-trace-js:ref:refs/heads/master claim_pattern: event_name: (workflow_dispatch|schedule) ref: refs/heads/master - ref_protected: "true" job_workflow_ref: DataDog/dd-trace-js/.github/workflows/release-proposal.yml@refs/heads/master permissions: diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index 455759bbdd4..18f28026c13 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -250,7 +250,7 @@ jobs: runs-on: ubuntu-latest services: kafka: - image: apache/kafka-native:3.9.1 + image: apache/kafka:3.9.1 env: KAFKA_PROCESS_ROLES: broker,controller KAFKA_NODE_ID: "1" @@ -607,7 +607,7 @@ jobs: runs-on: ubuntu-latest services: kafka: - image: apache/kafka-native:3.9.1 + image: apache/kafka:3.9.1 env: KAFKA_PROCESS_ROLES: broker,controller KAFKA_NODE_ID: "1" diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index b650f753dbb..90f33890754 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -462,7 +462,7 @@ jobs: runs-on: ubuntu-latest services: kafka: - image: apache/kafka-native:3.9.1 + image: apache/kafka:3.9.1 env: KAFKA_PROCESS_ROLES: broker,controller KAFKA_NODE_ID: "1" diff --git a/.github/workflows/dependabot-automation.yml b/.github/workflows/dependabot-automation.yml index afc666ecf3d..4d51451b758 100644 --- a/.github/workflows/dependabot-automation.yml +++ b/.github/workflows/dependabot-automation.yml @@ -12,28 +12,10 @@ env: GROUPS: '["dev-minor-and-patch-dependencies", "gh-actions-packages", "test-versions"]' jobs: - dependabot: - if: github.event.pull_request.user.login == 'dependabot[bot]' - runs-on: ubuntu-latest - # Keep this job as a stable, always-green check on Dependabot PRs, even when the workflow is - # re-triggered by an automation commit (e.g., vendoring). Sensitive operations (OIDC token mint, - # approving, enabling auto-merge) are delegated to `dependabot-automation` below. - permissions: - contents: read - steps: - - name: Status - run: | - echo "Dependabot PR detected." - if [ "${{ github.actor }}" = "dependabot[bot]" ]; then - echo "Automation steps will run in the 'dependabot-automation' job." - else - echo "Skipping automation: workflow actor is '${{ github.actor }}'." - fi - dependabot-automation: # Only run automation on the initial Dependabot-triggered run. If an automation commit is pushed - # (e.g. vendor output), GitHub re-triggers this workflow with `github.actor == 'dd-octo-sts[bot]'`. - # We intentionally avoid minting tokens / approving / enabling auto-merge on that follow-up run. + # GitHub re-triggers this workflow with `github.actor == 'dd-octo-sts[bot]'`. We intentionally + # avoid minting tokens / approving / enabling auto-merge on that follow-up run. if: github.event.pull_request.user.login == 'dependabot[bot]' && github.actor == 'dependabot[bot]' runs-on: ubuntu-latest permissions: @@ -61,294 +43,3 @@ jobs: env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.octo-sts.outputs.token }} - - vendor-build: - if: github.event.pull_request.user.login == 'dependabot[bot]' - runs-on: ubuntu-latest - # Security: this job checks out and runs code from the PR (vendoring build), - # so it is intentionally restricted to read-only permissions and produces a - # patch artifact instead of pushing directly. - permissions: - contents: read - pull-requests: read - outputs: - has_changes: ${{ steps.diff.outputs.has_changes }} - is_vendor_group: ${{ steps.ctx.outputs.is_vendor_group }} - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # 2.5.0 - - name: Compute vendor context - id: ctx - run: | - set -euo pipefail - - echo "is_vendor_group=${{ steps.metadata.outputs.directory == '/vendor' }}" >> $GITHUB_OUTPUT - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - if: steps.ctx.outputs.is_vendor_group == 'true' - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 1 - persist-credentials: false - - name: Restore trusted Node setup actions - if: steps.ctx.outputs.is_vendor_group == 'true' - run: | - git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" - git checkout "${{ github.event.pull_request.base.sha }}" -- .github/actions/node - - name: Restore trusted vendoring scripts - if: steps.ctx.outputs.is_vendor_group == 'true' - run: | - git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" - git checkout "${{ github.event.pull_request.base.sha }}" -- vendor/rspack.js vendor/rspack.config.js - - uses: ./.github/actions/node/active-lts - if: steps.ctx.outputs.is_vendor_group == 'true' - - name: Install vendoring deps (no lifecycle scripts) - if: steps.ctx.outputs.is_vendor_group == 'true' - run: yarn --ignore-scripts --frozen-lockfile --non-interactive - working-directory: ./vendor - - name: Build vendored bundles (trusted script) - if: steps.ctx.outputs.is_vendor_group == 'true' - run: node ./rspack.js - working-directory: ./vendor - - name: Create patch (restricted paths only) - id: diff - run: | - set -euo pipefail - - if [ "${{ steps.ctx.outputs.is_vendor_group }}" != "true" ]; then - echo "has_changes=false" >> $GITHUB_OUTPUT - exit 0 - fi - - if git diff --quiet; then - echo "has_changes=false" >> $GITHUB_OUTPUT - exit 0 - fi - - allowed_prefix_1="vendor/dist/" - allowed_file_1="vendor/package.json" - allowed_file_2="vendor/yarn.lock" - - bad=0 - while IFS= read -r file; do - case "$file" in - "$allowed_file_1" | "$allowed_file_2" | "$allowed_prefix_1"*) - ;; - *) - echo "Unexpected changed path: $file" - bad=1 - ;; - esac - done < <(git diff --name-only) - - if [ "$bad" -ne 0 ]; then - echo "Refusing to proceed: unexpected paths changed during vendoring." - exit 1 - fi - - git diff --binary --no-color > "${RUNNER_TEMP}/vendor.patch" - echo "has_changes=true" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: steps.diff.outputs.has_changes == 'true' - with: - name: vendor-patch - path: ${{ runner.temp }}/vendor.patch - if-no-files-found: error - - vendor-push: - if: github.event.pull_request.user.login == 'dependabot[bot]' && needs.vendor-build.outputs.is_vendor_group == 'true' && needs.vendor-build.outputs.has_changes == 'true' - runs-on: ubuntu-latest - needs: vendor-build - # Security: this job never runs installs/builds. - # It only applies the vetted patch artifact and writes the update via the GitHub API. - permissions: - id-token: write - steps: - - uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3 - id: octo-sts - with: - scope: DataDog/dd-trace-js - policy: dependabot-automation - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # 2.5.0 - with: - github-token: "${{ steps.octo-sts.outputs.token }}" - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - token: ${{ steps.octo-sts.outputs.token }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} - persist-credentials: false - - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 - with: - name: vendor-patch - path: ${{ runner.temp }}/vendor-artifact - - name: Apply patch - run: git apply --whitespace=nowarn "${{ runner.temp }}/vendor-artifact/vendor.patch" - - name: Validate changed paths - run: | - set -euo pipefail - - allowed_prefix_1="vendor/dist/" - allowed_file_1="vendor/package.json" - allowed_file_2="vendor/yarn.lock" - - bad=0 - while IFS= read -r file; do - case "$file" in - "$allowed_file_1" | "$allowed_file_2" | "$allowed_prefix_1"*) - ;; - *) - echo "Unexpected changed path after applying patch: $file" - bad=1 - ;; - esac - done < <(git diff --name-only) - - if [ "$bad" -ne 0 ]; then - echo "Refusing to proceed: unexpected paths changed." - exit 1 - fi - - name: Create verified commit via GitHub API (server-side) - env: - TARGET_BRANCH: ${{ github.event.pull_request.head.ref }} - GH_TOKEN: ${{ steps.octo-sts.outputs.token }} - run: | - set -euo pipefail - - repo="${GITHUB_REPOSITORY}" - expected_head_oid="$(git rev-parse HEAD)" - - max_files=200 - max_total_bytes=$((10 * 1024 * 1024)) # 10 MiB - - mapfile -t changes < <(git diff --name-status) - change_count="${#changes[@]}" - if [ "$change_count" -eq 0 ]; then - echo "No changed files detected." - exit 1 - fi - if [ "$change_count" -gt "$max_files" ]; then - echo "Too many changed files ($change_count > $max_files)." - exit 1 - fi - - additions_file="${RUNNER_TEMP}/vendor-additions.ndjson" - deletions_file="${RUNNER_TEMP}/vendor-deletions.ndjson" - encoded_file="${RUNNER_TEMP}/vendor-file.base64" - : > "$additions_file" - : > "$deletions_file" - total_bytes=0 - for change in "${changes[@]}"; do - read -r status path path2 <<<"$change" - - if [[ "$status" == D ]]; then - jq -nc --arg path "$path" '{path: $path}' >> "$deletions_file" - continue - fi - - # Treat renames as delete+add to keep the server-side tree in sync. - if [[ "$status" == R* ]]; then - jq -nc --arg path "$path" '{path: $path}' >> "$deletions_file" - path="$path2" - fi - - test -f "$path" - file_bytes="$(stat -c '%s' "$path")" - total_bytes=$((total_bytes + file_bytes)) - if [ "$total_bytes" -gt "$max_total_bytes" ]; then - echo "Total changes too large (${total_bytes} bytes)." - exit 1 - fi - - base64 -w 0 "$path" > "$encoded_file" - jq -nc --arg path "$path" --rawfile contents "$encoded_file" \ - '{path: $path, contents: $contents}' >> "$additions_file" - done - - variables_file="${RUNNER_TEMP}/graphql-variables.json" - jq -n \ - --arg repo "$repo" \ - --arg branch "$TARGET_BRANCH" \ - --arg msg "update vendored dependencies with new versions" \ - --arg expected "$expected_head_oid" \ - --slurpfile additions "$additions_file" \ - --slurpfile deletions "$deletions_file" \ - '{ - input: { - branch: { repositoryNameWithOwner: $repo, branchName: $branch }, - message: { headline: $msg }, - expectedHeadOid: $expected, - fileChanges: { additions: $additions, deletions: $deletions } - } - }' > "$variables_file" - - query='mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }' - request_file="${RUNNER_TEMP}/graphql-request.json" - jq -n \ - --arg query "$query" \ - --slurpfile variables "$variables_file" \ - '{query: $query, variables: $variables[0]}' > "$request_file" - - gh api graphql --input "$request_file" -q '.data.createCommitOnBranch.commit.oid' >/dev/null - - # If branch protection is configured to dismiss stale approvals when new commits are pushed, - # the vendoring commit will invalidate the earlier approval. Re-approve and (re-)enable - # auto-merge after pushing so Dependabot PRs can still merge automatically. - - name: Approve a PR (after vendoring commit) - if: contains(fromJSON(env.GROUPS), steps.metadata.outputs.dependency-group) - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ steps.octo-sts.outputs.token }} - - name: Enable auto-merge for Dependabot PRs (after vendoring commit) - if: contains(fromJSON(env.GROUPS), steps.metadata.outputs.dependency-group) - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ steps.octo-sts.outputs.token }} - - vendor-validate: - # Run validation after the generated vendor patch has been pushed, to ensure the PR contains - # the committed `vendor/dist/*` outputs. This runs inside the same workflow as the push, so it - # doesn't rely on additional workflows being triggered by that push. - if: github.event.pull_request.user.login == 'dependabot[bot]' && needs.vendor-build.outputs.is_vendor_group == 'true' && needs.vendor-build.outputs.has_changes == 'true' - runs-on: ubuntu-latest - needs: - - vendor-build - - vendor-push - permissions: - contents: read - pull-requests: read - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} - fetch-depth: 1 - persist-credentials: false - - name: Restore trusted Node setup actions - run: | - git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" - git checkout "${{ github.event.pull_request.base.sha }}" -- .github/actions/node - - name: Restore trusted vendoring scripts - run: | - git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.sha }}" - git checkout "${{ github.event.pull_request.base.sha }}" -- vendor/rspack.js vendor/rspack.config.js - - uses: ./.github/actions/node/active-lts - # Running `yarn` also automatically runs Rspack as a postinstall script. - - run: yarn --frozen-lockfile - working-directory: vendor - - name: Ensure no untracked outputs - run: | - set -euo pipefail - - if [ -n "$(git status --porcelain)" ]; then - echo "Working tree is dirty after vendoring:" - git status --porcelain - exit 1 - fi - - name: Diff only expected paths - run: git diff --exit-code -- vendor/dist vendor/package.json vendor/yarn.lock diff --git a/.github/workflows/llmobs.yml b/.github/workflows/llmobs.yml index 1c3759915fc..0e7d3afe02b 100644 --- a/.github/workflows/llmobs.yml +++ b/.github/workflows/llmobs.yml @@ -240,3 +240,13 @@ jobs: if: "!cancelled()" with: dd_api_key: ${{ secrets.DD_API_KEY }} + + langgraph: + runs-on: ubuntu-latest + env: + PLUGINS: langgraph + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/plugins/test + with: + dd_api_key: ${{ secrets.DD_API_KEY }} diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index ffab044d58f..500c8046d24 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -43,7 +43,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/node/active-lts - - run: FILENAME=$(npm pack --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz + - uses: ./.github/actions/install + - run: FILENAME=$(npm pack --silent --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz - run: mkdir -p /tmp/app - run: npm i -g pnpm if: matrix.manager.name == 'pnpm' @@ -58,7 +59,7 @@ jobs: - uses: ./.github/actions/node/active-lts - uses: ./.github/actions/install - run: mkdir npm bun - - run: FILENAME=$(npm pack) && tar -zxf $FILENAME -C npm + - run: FILENAME=$(npm pack --silent) && tar -zxf $FILENAME -C npm - run: ./node_modules/.bin/bun pm pack --gzip-level 0 --filename bun.tgz && tar -zxf bun.tgz -C bun - run: diff -r npm bun @@ -478,12 +479,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: ./.github/actions/node - with: - version: ${{ matrix.version }} + - uses: ./.github/actions/node/active-lts - uses: ./.github/actions/install - run: bun add --ignore-scripts mocha@10 # Use older mocha to support old Node.js versions - run: bun add --ignore-scripts express@4 # Use older express to support old Node.js versions + - uses: ./.github/actions/node + with: + version: ${{ matrix.version }} - run: node node_modules/.bin/mocha --colors --timeout 30000 integration-tests/init.spec.js - uses: ./.github/actions/push_to_test_optimization if: "!cancelled()" diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 042e0cd2844..a1301f59af7 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -76,7 +76,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/node/latest - - run: FILENAME=$(npm pack --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz + - run: FILENAME=$(npm pack --silent --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz - run: rm -rf * - run: tar -zxf /tmp/dd-trace.tgz -C $(pwd) --strip-components=1 - run: yarn --prod --ignore-optional @@ -212,7 +212,7 @@ jobs: scope: DataDog/dd-trace-js policy: yarn-dedupe - - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: yarn-lock path: ${{ runner.temp }}/yarn-lock-artifact diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index 1b810d704ff..1e4044a3573 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -24,7 +24,7 @@ jobs: with: path: dd-trace-js - name: Pack dd-trace-js - run: mkdir -p ./binaries && echo /binaries/$(npm pack --pack-destination ./binaries ./dd-trace-js) > ./binaries/nodejs-load-from-npm + run: mkdir -p ./binaries && echo /binaries/$(npm pack --silent --pack-destination ./binaries ./dd-trace-js) > ./binaries/nodejs-load-from-npm - name: Upload artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: diff --git a/.github/workflows/update-3rdparty-licenses.yml b/.github/workflows/update-3rdparty-licenses.yml index 9ca523e1ee6..ae378e8f144 100644 --- a/.github/workflows/update-3rdparty-licenses.yml +++ b/.github/workflows/update-3rdparty-licenses.yml @@ -3,6 +3,8 @@ name: Update 3rd-party licenses on: pull_request: paths: + - ".github/vendored-dependencies.csv" + - "vendor/package-lock.json" - "yarn.lock" jobs: @@ -28,8 +30,8 @@ jobs: - name: Check out dd-license-attribution uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - repository: DataDog/dd-license-attribution - ref: 224a89cb69d3143e8aa4640405037cf9c233ddf5 + repository: watson/dd-license-attribution + ref: 8ea483b9f735bf8da632c89796789cc2a050a9a6 path: dd-license-attribution - name: Install dd-license-attribution @@ -62,7 +64,7 @@ jobs: --use-mirrors=mirrors.json \ --no-scancode-strategy \ --no-github-sbom-strategy \ - --yarn-subdir vendor \ + --lockfile-subdir vendor \ "${REPOSITORY_URL}" > LICENSE-3rdparty.csv - name: Append vendored dependencies from PR @@ -141,7 +143,7 @@ jobs: policy: update-3rdparty-licenses - name: Download updated LICENSE-3rdparty.csv - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: license-csv diff --git a/.gitignore b/.gitignore index a6d328eef86..5fa33334899 100644 --- a/.gitignore +++ b/.gitignore @@ -47,11 +47,10 @@ lib-cov # Coverage directory used by tools like istanbul coverage/ -coverage-node-*/ +*.lcov # nyc test coverage .nyc_output -.nyc_output-node-* # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt @@ -130,7 +129,7 @@ packages/dd-trace/test/plugins/versions/node_modules packages/dd-trace/test/plugins/versions/yarn.lock !packages/dd-trace/**/telemetry/logs packages/datadog-plugin-azure-functions/test/integration-test/fixtures/node_modules -!vendor/dist +!vendor/package-lock.json vendor.patch __azurite_db_queue__.json __azurite_db_queue_extent__.json diff --git a/docker-compose.yml b/docker-compose.yml index 23cb9b8a71e..898e28dc1a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,7 +160,7 @@ services: - LAMBDA_EXECUTOR=local kafka: platform: linux/arm64 - image: apache/kafka-native:3.9.1 + image: apache/kafka:3.9.1 ports: - "127.0.0.1:9092:9092" - "127.0.0.1:9093:9093" diff --git a/docs/API.md b/docs/API.md index 34a979d6fe2..4b9c6868b02 100644 --- a/docs/API.md +++ b/docs/API.md @@ -68,6 +68,7 @@ tracer.use('pg', {
+ @@ -147,6 +148,7 @@ tracer.use('pg', { * [knex](./interfaces/export_.plugins.knex.html) * [koa](./interfaces/export_.plugins.koa.html) * [langchain](./interfaces/export_.plugins.langchain.html) +* [langgraph](./interfaces/export_.plugins.langgraph.html) * [mariadb](./interfaces/export_.plugins.mariadb.html) * [memcached](./interfaces/export_.plugins.memcached.html) * [microgateway-core](./interfaces/export_.plugins.microgateway_core.html) diff --git a/docs/test.ts b/docs/test.ts index c34b4329792..3aa467780e5 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -377,6 +377,7 @@ tracer.use('koa'); tracer.use('koa', httpServerOptions); tracer.use('langchain'); tracer.use('mariadb', { service: () => `my-custom-mariadb` }) +tracer.use('langgraph'); tracer.use('memcached'); tracer.use('microgateway-core'); tracer.use('microgateway-core', httpServerOptions); diff --git a/index.d.ts b/index.d.ts index b046117a36a..060aaca11e0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -262,6 +262,7 @@ interface Plugins { "knex": tracer.plugins.knex; "koa": tracer.plugins.koa; "langchain": tracer.plugins.langchain; + "langgraph": tracer.plugins.langgraph; "mariadb": tracer.plugins.mariadb; "memcached": tracer.plugins.memcached; "microgateway-core": tracer.plugins.microgateway_core; @@ -2579,6 +2580,12 @@ declare namespace tracer { /** * This plugin automatically instruments the + * [langgraph](https://github.com/npmjs/package/langgraph) library. + */ + interface langgraph extends Instrumentation {} + + /** + * This plugin automatically instruments the * [ldapjs](https://github.com/ldapjs/node-ldapjs/) module. */ interface ldapjs extends Instrumentation {} diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index bae836641f6..c7be359c269 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -362,7 +362,8 @@ class FakeCiVisIntake extends FakeAgent { this.off('message', messageHandler) resolve() } catch (e) { - // we'll try again when a new payload arrives + // Assertion not yet satisfied — we'll try again when a new payload arrives. + // The timeout handler will re-run onPayload and reject with the actual error. } } } diff --git a/integration-tests/ci-visibility/features-test-management-parallel/disabled.feature b/integration-tests/ci-visibility/features-test-management-parallel/disabled.feature new file mode 100644 index 00000000000..fdeff5e11f5 --- /dev/null +++ b/integration-tests/ci-visibility/features-test-management-parallel/disabled.feature @@ -0,0 +1,4 @@ +Feature: Disabled Parallel + Scenario: Say disabled parallel + When the greeter says disabled parallel + Then I should have heard "disabled parallel" diff --git a/integration-tests/ci-visibility/features-test-management-parallel/passing.feature b/integration-tests/ci-visibility/features-test-management-parallel/passing.feature new file mode 100644 index 00000000000..5ad8b456df0 --- /dev/null +++ b/integration-tests/ci-visibility/features-test-management-parallel/passing.feature @@ -0,0 +1,4 @@ +Feature: Passing Parallel + Scenario: Say passing parallel + When the greeter says passing parallel + Then I should have heard "passing parallel" diff --git a/integration-tests/ci-visibility/features-test-management-parallel/quarantine.feature b/integration-tests/ci-visibility/features-test-management-parallel/quarantine.feature new file mode 100644 index 00000000000..016019ac66f --- /dev/null +++ b/integration-tests/ci-visibility/features-test-management-parallel/quarantine.feature @@ -0,0 +1,4 @@ +Feature: Quarantine Parallel + Scenario: Say quarantine parallel + When the greeter says quarantine parallel + Then I should have heard "fail" diff --git a/integration-tests/ci-visibility/features-test-management-parallel/support/steps.js b/integration-tests/ci-visibility/features-test-management-parallel/support/steps.js new file mode 100644 index 00000000000..2e42144e348 --- /dev/null +++ b/integration-tests/ci-visibility/features-test-management-parallel/support/steps.js @@ -0,0 +1,26 @@ +'use strict' + +const assert = require('assert') +const { When, Then } = require('@cucumber/cucumber') + +Then('I should have heard {string}', function (expectedResponse) { + assert.equal(this.whatIHeard, expectedResponse) +}) + +When('the greeter says disabled parallel', function () { + // eslint-disable-next-line no-console + console.log('I am running disabled parallel') + // expected to fail if not disabled + this.whatIHeard = 'disabld parallel' +}) + +When('the greeter says passing parallel', function () { + this.whatIHeard = 'passing parallel' +}) + +When('the greeter says quarantine parallel', function () { + // eslint-disable-next-line no-console + console.log('I am running quarantine parallel') + // Will always fail the Then step — quarantined tests should not affect exit code + this.whatIHeard = 'quarantine parallel' +}) diff --git a/integration-tests/ci-visibility/test-management/test-attempt-to-fix-parallel-1.js b/integration-tests/ci-visibility/test-management/test-attempt-to-fix-parallel-1.js new file mode 100644 index 00000000000..aa67382dd4e --- /dev/null +++ b/integration-tests/ci-visibility/test-management/test-attempt-to-fix-parallel-1.js @@ -0,0 +1,9 @@ +'use strict' + +const assert = require('assert') + +describe('attempt to fix parallel tests 1', () => { + it('can attempt to fix a test', () => { + assert.strictEqual(1 + 2, 4) + }) +}) diff --git a/integration-tests/ci-visibility/test-management/test-attempt-to-fix-parallel-2.js b/integration-tests/ci-visibility/test-management/test-attempt-to-fix-parallel-2.js new file mode 100644 index 00000000000..af3e88de361 --- /dev/null +++ b/integration-tests/ci-visibility/test-management/test-attempt-to-fix-parallel-2.js @@ -0,0 +1,9 @@ +'use strict' + +const assert = require('assert') + +describe('attempt to fix parallel tests 2', () => { + it('can attempt to fix a test', () => { + assert.strictEqual(1 + 2, 4) + }) +}) diff --git a/integration-tests/ci-visibility/test-optimization-wrong-init.spec.js b/integration-tests/ci-visibility/test-optimization-wrong-init.spec.js index 7f35ca6f103..5b781fffe04 100644 --- a/integration-tests/ci-visibility/test-optimization-wrong-init.spec.js +++ b/integration-tests/ci-visibility/test-optimization-wrong-init.spec.js @@ -44,8 +44,8 @@ testFrameworks.forEach(({ testFramework, command, expectedOutput, extraTestConte describe(`test optimization wrong init for ${testFramework}`, () => { let cwd, receiver, childProcess, processOutput - // cucumber does not support Node.js@18 anymore - if (NODE_MAJOR <= 18 && testFramework === 'cucumber') return + // cucumber and vitest@4.x do not support Node.js@18 + if (NODE_MAJOR <= 18 && (testFramework === 'cucumber' || testFramework === 'vitest')) return const testFrameworks = ['jest', 'mocha', 'vitest'] diff --git a/integration-tests/ci-visibility/vitest-tests/test-attempt-to-fix.mjs b/integration-tests/ci-visibility/vitest-tests/test-attempt-to-fix.mjs index 4fe4ba6cacc..1092519a8d4 100644 --- a/integration-tests/ci-visibility/vitest-tests/test-attempt-to-fix.mjs +++ b/integration-tests/ci-visibility/vitest-tests/test-attempt-to-fix.mjs @@ -15,6 +15,14 @@ describe('attempt to fix tests', () => { } else { expect(1 + 2).to.equal(3) } + } else if (process.env.SHOULD_FAIL_FIRST_ONLY) { + // First attempt fails, all retries pass. Exit code must still be 1 + // for plain ATF tests (not quarantined/disabled). + if (numAttempt++ === 0) { + expect(1 + 2).to.equal(4) + } else { + expect(1 + 2).to.equal(3) + } } else { expect(1 + 2).to.equal(4) } diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index 2e5f8f9358f..6058d512c99 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -1088,6 +1088,61 @@ describe(`cucumber@${version} commonJS`, () => { }).catch(done) }) }) + + onlyLatestIt('can skip suites in parallel mode', async () => { + receiver.setSuitesToSkip([{ + type: 'suite', + attributes: { + suite: `${featuresPath}farewell.feature`, + }, + }]) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + assert.strictEqual(testSession.meta[CUCUMBER_IS_PARALLEL], 'true') + assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assert.strictEqual(testSession.meta[TEST_ITR_TESTS_SKIPPED], 'true') + assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_TYPE], 'suite') + assert.strictEqual(testSession.metrics[TEST_ITR_SKIPPING_COUNT], 1) + + const suites = events.filter(event => event.type === 'test_suite_end').map(event => event.content) + assert.strictEqual(suites.length, 2) + + const skippedSuite = suites.find(s => + s.resource === `test_suite.${featuresPath}farewell.feature` + ) + assert.strictEqual(skippedSuite.meta[TEST_STATUS], 'skip') + assert.strictEqual(skippedSuite.meta[TEST_SKIPPED_BY_ITR], 'true') + + // greetings.feature ran (not skipped) + const runningSuite = suites.find(s => + s.resource === `test_suite.${featuresPath}greetings.feature` + ) + assert.ok(runningSuite) + assert.ok(!(TEST_SKIPPED_BY_ITR in runningSuite.meta)) + + // Only tests from the non-skipped suite ran + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.ok(tests.length > 0) + tests.forEach(test => { + assert.ok(!test.meta[TEST_SUITE].includes('farewell')) + }) + }) + + childProcess = exec( + parallelModeCommand, + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + } + ) + await Promise.all([ + eventsPromise, + once(childProcess, 'exit'), + ]) + }) }) context('early flake detection', () => { @@ -2833,6 +2888,119 @@ describe(`cucumber@${version} commonJS`, () => { ]) assert.match(testOutput, /Test management tests could not be fetched/) }) + + onlyLatestIt('can disable tests in parallel mode', async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTests({ + cucumber: { + suites: { + 'ci-visibility/features-test-management-parallel/disabled.feature': { + tests: { + 'Say disabled parallel': { + properties: { disabled: true }, + }, + }, + }, + }, + }, + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + assert.strictEqual(testSession.meta[CUCUMBER_IS_PARALLEL], 'true') + assert.strictEqual(testSession.meta[TEST_MANAGEMENT_ENABLED], 'true') + assert.strictEqual(testSession.meta[TEST_STATUS], 'pass') + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.strictEqual(tests.length, 2) + + const disabledTest = tests.find(t => t.meta[TEST_NAME] === 'Say disabled parallel') + assert.strictEqual(disabledTest.meta[TEST_STATUS], 'skip') + assert.strictEqual(disabledTest.meta[TEST_MANAGEMENT_IS_DISABLED], 'true') + + const passingTest = tests.find(t => t.meta[TEST_NAME] === 'Say passing parallel') + assert.strictEqual(passingTest.meta[TEST_STATUS], 'pass') + }) + + let exitCode + childProcess = exec( + './node_modules/.bin/cucumber-js' + + ' ci-visibility/features-test-management-parallel/disabled.feature' + + ' ci-visibility/features-test-management-parallel/passing.feature' + + ' --parallel 2', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + } + ) + + childProcess.on('exit', (code) => { exitCode = code }) + + await Promise.all([ + eventsPromise, + once(childProcess, 'exit'), + ]) + + assert.strictEqual(exitCode, 0) + }) + + onlyLatestIt('can quarantine tests in parallel mode', async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTests({ + cucumber: { + suites: { + 'ci-visibility/features-test-management-parallel/quarantine.feature': { + tests: { + 'Say quarantine parallel': { + properties: { quarantined: true }, + }, + }, + }, + }, + }, + }) + + let exitCode + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + assert.strictEqual(testSession.meta[CUCUMBER_IS_PARALLEL], 'true') + assert.strictEqual(testSession.meta[TEST_MANAGEMENT_ENABLED], 'true') + assert.strictEqual(testSession.meta[TEST_STATUS], 'pass') + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.strictEqual(tests.length, 2) + + const quarantinedTest = tests.find(t => t.meta[TEST_NAME] === 'Say quarantine parallel') + assert.strictEqual(quarantinedTest.meta[TEST_STATUS], 'fail') + assert.strictEqual(quarantinedTest.meta[TEST_MANAGEMENT_IS_QUARANTINED], 'true') + + const passingTest = tests.find(t => t.meta[TEST_NAME] === 'Say passing parallel') + assert.strictEqual(passingTest.meta[TEST_STATUS], 'pass') + }) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-test-management-parallel/quarantine.feature' + + ' ci-visibility/features-test-management-parallel/passing.feature --parallel 2', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + } + ) + + childProcess.on('exit', (code) => { exitCode = code }) + + await Promise.all([ + eventsPromise, + once(childProcess, 'exit'), + ]) + + // Quarantined test fails but exit code should be 0 + assert.strictEqual(exitCode, 0) + }) }) context('libraries capabilities', () => { @@ -2852,11 +3020,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.ok(metadataDicts.length > 0) metadataDicts.forEach(metadata => { - if (runMode === 'parallel') { - assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], undefined) - } else { - assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], '1') - } + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], '1') assert.strictEqual(metadata.test[DD_CAPABILITIES_EARLY_FLAKE_DETECTION], '1') assert.strictEqual(metadata.test[DD_CAPABILITIES_AUTO_TEST_RETRIES], '1') assert.strictEqual(metadata.test[DD_CAPABILITIES_IMPACTED_TESTS], '1') diff --git a/integration-tests/cypress/cypress.spec.js b/integration-tests/cypress/cypress.spec.js index c9cf3a9a7aa..2de35e848e2 100644 --- a/integration-tests/cypress/cypress.spec.js +++ b/integration-tests/cypress/cypress.spec.js @@ -369,7 +369,7 @@ moduleTypes.forEach(({ testTestEvent.content.meta[TEST_SOURCE_FILE].endsWith('spec-source-line.cy.ts'), `TEST_SOURCE_FILE should point to TypeScript source, got: ${testTestEvent.content.meta[TEST_SOURCE_FILE]}` ) - }, 120000) + }, 60000) // Run Cypress with the pre-compiled JS spec (compiled from spec-source-line.cy.ts). // Cypress bundles the compiled JS via its own preprocessor; the plugin resolves @@ -459,7 +459,7 @@ moduleTypes.forEach(({ fallbackEvent.content.meta[TEST_SOURCE_FILE].endsWith('spec-source-line-fallback.cy.ts'), `TEST_SOURCE_FILE should point to TypeScript source, got: ${fallbackEvent.content.meta[TEST_SOURCE_FILE]}` ) - }, 120000) + }, 60000) childProcess = exec(testCommand, { cwd, @@ -507,7 +507,7 @@ moduleTypes.forEach(({ noMatchEvent.content.meta[TEST_SOURCE_FILE].endsWith('spec-source-line-no-match.cy.ts'), `TEST_SOURCE_FILE should point to TypeScript source, got: ${noMatchEvent.content.meta[TEST_SOURCE_FILE]}` ) - }, 120000) + }, 60000) childProcess = exec(testCommand, { cwd, @@ -540,7 +540,7 @@ moduleTypes.forEach(({ assert.ok(jsInvocationDetailsEvent, 'plain-js invocationDetails test event should exist') assert.strictEqual( jsInvocationDetailsEvent.content.metrics[TEST_SOURCE_START], - 246, + 244, 'should keep invocationDetails line directly for plain JS specs without source maps' ) assert.ok( @@ -549,7 +549,7 @@ moduleTypes.forEach(({ jsInvocationDetailsEvent.content.meta[TEST_SOURCE_FILE] }` ) - }, 120000) + }, 60000) childProcess = exec(testCommand, { cwd, @@ -1850,7 +1850,7 @@ moduleTypes.forEach(({ const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') assert.strictEqual(newTests.length, 0) - }, 120000) + }, 60000) const specToRun = 'cypress/e2e/spec.cy.js' @@ -2975,11 +2975,9 @@ moduleTypes.forEach(({ testAssertionsPromise, ]) - if (shouldAlwaysPass) { + if (shouldAlwaysPass || isQuarantined || isDisabled) { assert.strictEqual(exitCode, 0) } else { - // TODO: we need to figure out how to trick cypress into returning exit code 0 - // even if there are failed tests assert.strictEqual(exitCode, 1) } } @@ -3018,9 +3016,6 @@ moduleTypes.forEach(({ * TODO: * The spec says that quarantined tests that are not attempted to fix should be run and their result ignored. * Cypress will skip the test instead. - * - * When a test is quarantined and attempted to fix, the spec is to run the test and ignore its result. - * Cypress will run the test, but it won't ignore its result. */ it('can mark tests as quarantined and tests are not skipped', async () => { receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } }) @@ -3290,7 +3285,7 @@ moduleTypes.forEach(({ const tests = events.filter(event => event.type === 'test').map(event => event.content) // it is not retried assert.strictEqual(tests.length, 1) - }, 120000) + }, 60000) const { NODE_OPTIONS, diff --git a/integration-tests/debugger/diagnostics.spec.js b/integration-tests/debugger/diagnostics.spec.js index 649a88a0ffc..7718807c2a1 100644 --- a/integration-tests/debugger/diagnostics.spec.js +++ b/integration-tests/debugger/diagnostics.spec.js @@ -8,6 +8,10 @@ const { pollInterval, setup } = require('./utils') describe('Dynamic Instrumentation', function () { const t = setup({ testApp: 'target-app/basic.js', dependencies: ['fastify'] }) + before(function () { + require('../../packages/dd-trace/src/process-tags').initialize() + }) + describe('diagnostics messages', function () { it('should send expected diagnostics messages if probe is received and triggered', function (done) { let receivedAckUpdate = false diff --git a/integration-tests/esbuild/package.json b/integration-tests/esbuild/package.json index 88e09a91f7a..a208ab2d8aa 100644 --- a/integration-tests/esbuild/package.json +++ b/integration-tests/esbuild/package.json @@ -27,6 +27,6 @@ "express": "4.22.1", "knex": "3.1.0", "koa": "3.1.2", - "openai": "6.27.0" + "openai": "6.29.0" } } diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index 0042b1fa7b1..141d68cb40e 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -387,7 +387,7 @@ function execHelper (command, options) { * @param {NodeJS.ProcessEnv} env - The environment to use for the pack command */ function packTarball (tarballPath, env) { - execHelper(`${BUN} pm pack --quiet --gzip-level 0 --filename ${tarballPath}`, { env }) + execHelper(`${BUN} pm pack --ignore-scripts --quiet --gzip-level 0 --filename ${tarballPath}`, { env }) log('Tarball packed successfully:', tarballPath) } diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index 260267ca7b1..d42b7ae989f 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -2100,6 +2100,60 @@ describe(`mocha@${MOCHA_VERSION}`, function () { }).catch(done) }) }) + + onlyLatestIt('can skip suites in parallel mode', async () => { + receiver.setSuitesToSkip([{ + type: 'suite', + attributes: { + suite: 'ci-visibility/test/ci-visibility-test.js', + }, + }]) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + assert.strictEqual(testSession.meta[MOCHA_IS_PARALLEL], 'true') + assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assert.strictEqual(testSession.meta[TEST_ITR_TESTS_SKIPPED], 'true') + assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_TYPE], 'suite') + assert.strictEqual(testSession.metrics[TEST_ITR_SKIPPING_COUNT], 1) + + const suites = events.filter(event => event.type === 'test_suite_end').map(event => event.content) + assert.strictEqual(suites.length, 2) + + const skippedSuite = suites.find(s => + s.resource === 'test_suite.ci-visibility/test/ci-visibility-test.js' + ) + assert.strictEqual(skippedSuite.meta[TEST_STATUS], 'skip') + assert.strictEqual(skippedSuite.meta[TEST_SKIPPED_BY_ITR], 'true') + + const runningSuite = suites.find(s => + s.resource === 'test_suite.ci-visibility/test/ci-visibility-test-2.js' + ) + assert.strictEqual(runningSuite.meta[TEST_STATUS], 'pass') + + // Only 1 test ran (from the non-skipped suite) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + assert.strictEqual(tests.length, 1) + assert.strictEqual(tests[0].meta[TEST_STATUS], 'pass') + }) + + childProcess = exec( + 'node node_modules/mocha/bin/mocha --parallel --jobs 2' + + ' ./ci-visibility/test/ci-visibility-test.js' + + ' ./ci-visibility/test/ci-visibility-test-2.js', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + } + ) + + await Promise.all([ + eventsPromise, + once(childProcess, 'exit'), + ]) + }) }) context('error tags', () => { @@ -4012,6 +4066,86 @@ describe(`mocha@${MOCHA_VERSION}`, function () { runAttemptToFixTest(done, { isAttemptToFix: true, isDisabled: true }) }) + + onlyLatestIt('can attempt to fix in parallel mode', async () => { + const NUM_RETRIES = 3 + receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: NUM_RETRIES } }) + receiver.setTestManagementTests({ + mocha: { + suites: { + 'ci-visibility/test-management/test-attempt-to-fix-parallel-1.js': { + tests: { + 'attempt to fix parallel tests 1 can attempt to fix a test': { + properties: { attempt_to_fix: true }, + }, + }, + }, + 'ci-visibility/test-management/test-attempt-to-fix-parallel-2.js': { + tests: { + 'attempt to fix parallel tests 2 can attempt to fix a test': { + properties: { attempt_to_fix: true }, + }, + }, + }, + }, + }, + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + + const sessionEvent = events.find(event => event.type === 'test_session_end').content + assert.strictEqual(sessionEvent.meta[MOCHA_IS_PARALLEL], 'true') + assert.strictEqual(sessionEvent.meta[TEST_MANAGEMENT_ENABLED], 'true') + + // Each file: 1 initial attempt + NUM_RETRIES retries = (NUM_RETRIES + 1) per file, 2 files + assert.strictEqual(tests.length, (NUM_RETRIES + 1) * 2) + + // All attempts fail (tests always throw) + tests.forEach(test => { + assert.strictEqual(test.meta[TEST_STATUS], 'fail') + assert.strictEqual(test.meta[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX], 'true') + }) + + // Last attempt of each test should have failed-all-retries + const testsBySuite = {} + for (const test of tests) { + const suite = test.meta[TEST_SUITE] + if (!testsBySuite[suite]) testsBySuite[suite] = [] + testsBySuite[suite].push(test) + } + for (const suiteTests of Object.values(testsBySuite)) { + const lastAttempt = suiteTests[suiteTests.length - 1] + assert.strictEqual(lastAttempt.meta[TEST_HAS_FAILED_ALL_RETRIES], 'true') + assert.strictEqual(lastAttempt.meta[TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED], 'false') + } + + // Verify separate worker processes + const firstTestPerSuite = Object.values(testsBySuite).map(t => t[0]) + assert.strictEqual(firstTestPerSuite.length, 2) + const runtimeIds = firstTestPerSuite.map(t => t.meta['runtime-id']) + assert.ok(runtimeIds[0]) + assert.ok(runtimeIds[1]) + assert.notStrictEqual(runtimeIds[0], runtimeIds[1]) + }) + + childProcess = exec( + 'node node_modules/mocha/bin/mocha --parallel --jobs 2' + + ' ./ci-visibility/test-management/test-attempt-to-fix-parallel-1.js' + + ' ./ci-visibility/test-management/test-attempt-to-fix-parallel-2.js', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + } + ) + + await Promise.all([ + eventsPromise, + once(childProcess, 'exit'), + ]) + }) }) context('disabled', () => { @@ -4397,19 +4531,14 @@ describe(`mocha@${MOCHA_VERSION}`, function () { }) context('libraries capabilities', () => { - const getTestAssertions = (isParallel) => + const getTestAssertions = () => receiver.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('citestcycle'), (payloads) => { const metadataDicts = payloads.flatMap(({ payload }) => payload.metadata) assert.ok(metadataDicts.length > 0) metadataDicts.forEach(metadata => { - if (isParallel) { - assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], undefined) - assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], undefined) - } else { - assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], '1') - assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], '5') - } + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], '5') assert.strictEqual(metadata.test[DD_CAPABILITIES_EARLY_FLAKE_DETECTION], '1') assert.strictEqual(metadata.test[DD_CAPABILITIES_AUTO_TEST_RETRIES], '1') assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE], '1') @@ -4420,8 +4549,8 @@ describe(`mocha@${MOCHA_VERSION}`, function () { }) }) - const runTest = (done, isParallel, extraEnvVars = {}) => { - const testAssertionsPromise = getTestAssertions(isParallel) + const runTest = (done, extraEnvVars = {}) => { + const testAssertionsPromise = getTestAssertions() childProcess = exec( runTestsCommand, @@ -4440,13 +4569,11 @@ describe(`mocha@${MOCHA_VERSION}`, function () { } it('adds capabilities to tests', (done) => { - runTest(done, false) + runTest(done) }) onlyLatestIt('adds capabilities to tests (parallel)', (done) => { - runTest(done, true, { - RUN_IN_PARALLEL: '1', - }) + runTest(done, { RUN_IN_PARALLEL: '1' }) }) }) diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index 58d019474e5..d0370a026d7 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -685,7 +685,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }, 120000) + }, 60000) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', diff --git a/integration-tests/vitest.config.mjs b/integration-tests/vitest.config.mjs index da2c6eb96f3..cbb1799d8e4 100644 --- a/integration-tests/vitest.config.mjs +++ b/integration-tests/vitest.config.mjs @@ -6,6 +6,7 @@ const config = { process.env.TEST_DIR || 'ci-visibility/vitest-tests/test-visibility*', ], pool: process.env.POOL_CONFIG || 'forks', + reporters: ['default'], }, } diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index 15eccda8772..08a181d01ba 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -66,7 +66,8 @@ const { NODE_MAJOR } = require('../../version') const NUM_RETRIES_EFD = 3 -const versions = ['1.6.0', 'latest'] +// vitest@4.x requires Node.js >= 20 +const versions = NODE_MAJOR <= 18 ? ['1.6.0', '3'] : ['1.6.0', 'latest'] const linePctMatchRegex = /Lines\s+:\s+([\d.]+)%/ @@ -1716,6 +1717,7 @@ versions.forEach((version) => { shouldAlwaysPass, isQuarantining, shouldFailSometimes, + shouldFailFirstOnly, isDisabling, extraEnvVars = {}, } = {}) => { @@ -1738,6 +1740,7 @@ versions.forEach((version) => { ...extraEnvVars, ...(shouldAlwaysPass ? { SHOULD_ALWAYS_PASS: '1' } : {}), ...(shouldFailSometimes ? { SHOULD_FAIL_SOMETIMES: '1' } : {}), + ...(shouldFailFirstOnly ? { SHOULD_FAIL_FIRST_ONLY: '1' } : {}), }, } ) @@ -1777,6 +1780,12 @@ versions.forEach((version) => { runAttemptToFixTest(done, { isAttemptingToFix: true, shouldFailSometimes: true }) }) + it('does not suppress exit code for plain ATF tests even when last retry passes', (done) => { + receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } }) + + runAttemptToFixTest(done, { isAttemptingToFix: true, shouldFailFirstOnly: true }) + }) + it('does not attempt to fix tests if test management is not enabled', (done) => { receiver.setSettings({ test_management: { enabled: false, attempt_to_fix_retries: 3 } }) diff --git a/nyc.config.js b/nyc.config.js index 1852ee3d12e..3ff611feb5b 100644 --- a/nyc.config.js +++ b/nyc.config.js @@ -38,8 +38,8 @@ module.exports = { '**/vendor/**', ], // Avoid collisions when a single CI job runs coverage sequentially across multiple Node.js versions. - tempDir: `.nyc_output-node-${process.version}${label}`, - reportDir: `coverage-node-${process.version}${label}`, + tempDir: `.nyc_output/node-${process.version}${label}`, + reportDir: `coverage/node-${process.version}${label}`, // Not tracking all coverage has the downside to potentially miss some code // paths and files that we do not use anymore. Doing so is just going to // report lots of files in tests that are empty and that is more confusing. diff --git a/package.json b/package.json index b0d8feb182d..90e3e723616 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "dd-trace", - "version": "5.90.0", + "version": "5.91.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", "scripts": { "env": "bash ./plugin-env", + "prepare": "cd vendor && npm ci --include=dev", "preinstall": "node scripts/preinstall.js", "bench": "node benchmark/index.js", "bench:e2e:test-optimization": "node benchmark/e2e-test-optimization/benchmark-run.js", @@ -146,13 +147,13 @@ "@datadog/wasm-js-rewriter": "5.0.1", "@opentelemetry/api": ">=1.0.0 <1.10.0", "@opentelemetry/api-logs": "<1.0.0", - "oxc-parser": "^0.116.0" + "oxc-parser": "^0.118.0" }, "devDependencies": { "@actions/core": "^3.0.0", "@actions/github": "^9.0.0", "@babel/helpers": "^7.28.6", - "@eslint/eslintrc": "^3.3.1", + "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^9.39.2", "@msgpack/msgpack": "^3.1.3", "@openfeature/core": "^1.8.1", @@ -165,11 +166,11 @@ "benchmark": "^2.1.4", "body-parser": "^2.2.2", "bun": "1.3.10", - "codeowners-audit": "^2.7.1", + "codeowners-audit": "^2.9.0", "eslint": "^9.39.2", - "eslint-plugin-cypress": "^6.1.0", + "eslint-plugin-cypress": "^6.2.0", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsdoc": "^62.5.0", + "eslint-plugin-jsdoc": "^62.8.0", "eslint-plugin-mocha": "^11.2.0", "eslint-plugin-n": "^17.23.2", "eslint-plugin-promise": "^7.2.1", diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 3c2b8def25e..36054ac579b 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -166,9 +166,9 @@ function getErrorFromCucumberResult (cucumberResult) { return error } -function getChannelPromise (channelToPublishTo, isParallel = false, frameworkVersion = null) { +function getChannelPromise (channelToPublishTo, frameworkVersion = null) { return new Promise(resolve => { - channelToPublishTo.publish({ onDone: resolve, isParallel, frameworkVersion }) + channelToPublishTo.publish({ onDone: resolve, frameworkVersion }) }) } @@ -505,7 +505,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin } let errorSkippableRequest - const configurationResponse = await getChannelPromise(libraryConfigurationCh, isParallel, frameworkVersion) + const configurationResponse = await getChannelPromise(libraryConfigurationCh, frameworkVersion) isEarlyFlakeDetectionEnabled = configurationResponse.libraryConfig?.isEarlyFlakeDetectionEnabled earlyFlakeDetectionNumRetries = configurationResponse.libraryConfig?.earlyFlakeDetectionNumRetries @@ -681,6 +681,7 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa let isQuarantined = false let isModified = false + const originalDryRun = this.options.dryRun if (isTestManagementTestsEnabled) { const testProperties = getTestProperties(testSuitePath, pickle.name) isAttemptToFix = testProperties.attemptToFix @@ -719,6 +720,9 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa // TODO: for >=11 we could use `runTestCaseResult` instead of accumulating results in `lastStatusByPickleId` let runTestCaseResult = await runTestCaseFunction.apply(this, arguments) + // Restore dryRun so it doesn't affect subsequent tests in the same worker + this.options.dryRun = originalDryRun + const testStatuses = lastStatusByPickleId.get(pickle.id) const lastTestStatus = testStatuses.at(-1) @@ -1053,6 +1057,12 @@ addHook({ this.options.worldParameters._ddIsFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled this.options.worldParameters._ddNumTestRetries = numTestRetries + if (isTestManagementTestsEnabled) { + this.options.worldParameters._ddIsTestManagementTestsEnabled = true + this.options.worldParameters._ddTestManagementTests = testManagementTests + this.options.worldParameters._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries + } + return startWorker.apply(this, arguments) }) return adapterPackage @@ -1090,6 +1100,11 @@ addHook({ } isFlakyTestRetriesEnabled = !!this.options.worldParameters._ddIsFlakyTestRetriesEnabled numTestRetries = this.options.worldParameters._ddNumTestRetries ?? 0 + isTestManagementTestsEnabled = !!this.options.worldParameters._ddIsTestManagementTestsEnabled + if (isTestManagementTestsEnabled) { + testManagementTests = this.options.worldParameters._ddTestManagementTests + testManagementAttemptToFixRetries = this.options.worldParameters._ddTestManagementAttemptToFixRetries + } } ) return workerPackage diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index ae4b8a134b3..3d83b13e406 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -4,6 +4,7 @@ module.exports = { '@anthropic-ai/sdk': { esmFirst: true, fn: () => require('../anthropic') }, '@apollo/server': () => require('../apollo-server'), '@apollo/gateway': () => require('../apollo'), + '@langchain/langgraph': { esmFirst: true, fn: () => require('../langgraph') }, 'apollo-server-core': () => require('../apollo-server-core'), '@aws-sdk/smithy-client': () => require('../aws-sdk'), '@azure/event-hubs': () => require('../azure-event-hubs'), diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js index aa384785bd8..28b13f15191 100644 --- a/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +++ b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js @@ -4,4 +4,5 @@ module.exports = [ ...require('./ai'), ...require('./bullmq'), ...require('./langchain'), + ...require('./langgraph'), ] diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js new file mode 100644 index 00000000000..9d46ec9b748 --- /dev/null +++ b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js @@ -0,0 +1,30 @@ +'use strict' + +module.exports = [ + { + module: { + name: '@langchain/langgraph', + versionRange: '>=1.1.2', + filePath: 'dist/pregel/index.js', + }, + functionQuery: { + methodName: 'stream', + className: 'Pregel', + kind: 'AsyncIterator', + }, + channelName: 'Pregel_stream', + }, + { + module: { + name: '@langchain/langgraph', + versionRange: '>=1.1.2', + filePath: 'dist/pregel/index.cjs', + }, + functionQuery: { + methodName: 'stream', + className: 'Pregel', + kind: 'AsyncIterator', + }, + channelName: 'Pregel_stream', + }, +] diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 72113641e9d..8661a98e3a7 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -702,11 +702,15 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { const mightHitBreakpoint = this.isDiEnabled && numTestExecutions >= 2 const ctx = testContexts.get(event.test) + if (!ctx) { + log.warn('"ci:jest:test_done": no context found for test "%s"', testName) + return + } const finalStatus = this.getFinalStatus(testName, status, - !!ctx?.isNew, - !!ctx?.isModified, + !!ctx.isNew, + !!ctx.isModified, isEfdRetry, isAttemptToFix, numTestExecutions) @@ -761,6 +765,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { efdDeterminedRetries.clear() efdSlowAbortedTests.clear() efdNewTestCandidates.clear() + retriedTestsToNumAttempts.clear() + attemptToFixRetriedTestsStatuses.clear() + testsToBeRetried.clear() } if (event.name === 'test_skip' || event.name === 'test_todo') { const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName()) diff --git a/packages/datadog-instrumentations/src/langgraph.js b/packages/datadog-instrumentations/src/langgraph.js new file mode 100644 index 00000000000..e98273ab842 --- /dev/null +++ b/packages/datadog-instrumentations/src/langgraph.js @@ -0,0 +1,7 @@ +'use strict' + +const { addHook, getHooks } = require('./helpers/instrument') + +for (const hook of getHooks('@langchain/langgraph')) { + addHook(hook, exports => exports) +} diff --git a/packages/datadog-instrumentations/src/mocha/main.js b/packages/datadog-instrumentations/src/mocha/main.js index c6ba716def1..a83c784bf0b 100644 --- a/packages/datadog-instrumentations/src/mocha/main.js +++ b/packages/datadog-instrumentations/src/mocha/main.js @@ -101,12 +101,12 @@ function getFilteredSuites (originalSuites) { }, { suitesToRun: [], skippedSuites: new Set() }) } -function getOnStartHandler (isParallel, frameworkVersion) { +function getOnStartHandler (frameworkVersion) { return function () { const processArgv = process.argv.slice(2).join(' ') const command = `mocha ${processArgv}` testSessionStartCh.publish({ command, frameworkVersion }) - if (!isParallel && skippedSuites.length) { + if (skippedSuites.length) { itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion }) } } @@ -315,8 +315,7 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini config.isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled config.testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries config.isImpactedTestsEnabled = libraryConfig.isImpactedTestsEnabled - // ITR is not supported in parallel mode yet - config.isSuitesSkippingEnabled = !isParallel && libraryConfig.isSuitesSkippingEnabled + config.isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled config.isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled config.flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount @@ -452,7 +451,7 @@ addHook({ const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite) - this.once('start', getOnStartHandler(false, frameworkVersion)) + this.once('start', getOnStartHandler(frameworkVersion)) this.once('end', getOnEndHandler(false)) @@ -623,9 +622,16 @@ addHook({ return run.apply(this, arguments) } - this.once('start', getOnStartHandler(true, frameworkVersion)) + this.once('start', getOnStartHandler(frameworkVersion)) this.once('end', getOnEndHandler(true)) + // Populate unskippable suites before config is fetched (matches serial mode at Mocha.prototype.run) + for (const filePath of files) { + if (isMarkedAsUnskippable({ path: filePath })) { + unskippableSuites.push(filePath) + } + } + getExecutionConfiguration(this, true, frameworkVersion, () => { if (config.isKnownTestsEnabled) { const testSuites = files.map(file => getTestSuitePath(file, process.cwd())) @@ -640,7 +646,25 @@ addHook({ config.isEarlyFlakeDetectionFaulty = true } } - run.apply(this, arguments) + if (config.isSuitesSkippingEnabled && suitesToSkip.length) { + const filteredFiles = [] + const skippedFiles = [] + for (const file of files) { + const testPath = getTestSuitePath(file, process.cwd()) + const shouldSkip = suitesToSkip.includes(testPath) + const isUnskippable = unskippableSuites.includes(file) + if (shouldSkip && !isUnskippable) { + skippedFiles.push(testPath) + } else { + filteredFiles.push(file) + } + } + isSuitesSkipped = skippedFiles.length > 0 + skippedSuites = skippedFiles + run.apply(this, [cb, { files: filteredFiles }]) + } else { + run.apply(this, arguments) + } }) return this @@ -694,8 +718,7 @@ addHook({ if (config.isTestManagementTestsEnabled) { const testSuiteTestManagementTests = config.testManagementTests?.mocha?.suites?.[testPath] || {} newWorkerArgs._ddIsTestManagementTestsEnabled = true - // TODO: attempt to fix does not work in parallel mode yet - // newWorkerArgs._ddTestManagementAttemptToFixRetries = config.testManagementAttemptToFixRetries + newWorkerArgs._ddTestManagementAttemptToFixRetries = config.testManagementAttemptToFixRetries newWorkerArgs._ddTestManagementTests = { mocha: { suites: { diff --git a/packages/datadog-instrumentations/src/mocha/utils.js b/packages/datadog-instrumentations/src/mocha/utils.js index 6ba5a287d73..54644660bc9 100644 --- a/packages/datadog-instrumentations/src/mocha/utils.js +++ b/packages/datadog-instrumentations/src/mocha/utils.js @@ -140,7 +140,6 @@ function runnableWrapper (RunnablePackage, libraryConfig) { if (!testFinishCh.hasSubscribers) { return run.apply(this, arguments) } - // Flaky test retries does not work in parallel mode if (libraryConfig?.isFlakyTestRetriesEnabled) { this.retries(libraryConfig?.flakyTestRetriesCount) } diff --git a/packages/datadog-instrumentations/src/mocha/worker.js b/packages/datadog-instrumentations/src/mocha/worker.js index 224d5e32deb..b7de0006172 100644 --- a/packages/datadog-instrumentations/src/mocha/worker.js +++ b/packages/datadog-instrumentations/src/mocha/worker.js @@ -43,10 +43,10 @@ addHook({ } if (this.options._ddIsTestManagementTestsEnabled) { config.isTestManagementTestsEnabled = true - // TODO: attempt to fix does not work in parallel mode yet - // config.testManagementAttemptToFixRetries = this.options._ddTestManagementAttemptToFixRetries + config.testManagementAttemptToFixRetries = this.options._ddTestManagementAttemptToFixRetries config.testManagementTests = this.options._ddTestManagementTests delete this.options._ddIsTestManagementTestsEnabled + delete this.options._ddTestManagementAttemptToFixRetries delete this.options._ddTestManagementTests } if (this.options._ddIsFlakyTestRetriesEnabled) { diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index eda8e330b72..61aabbed358 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -146,8 +146,23 @@ function isReporterPackageNewest (vitestPackage) { return vitestPackage.h?.name === 'BaseSequencer' } -function isBaseSequencer (vitestPackage) { - return vitestPackage.b?.name === 'BaseSequencer' +/** + * Finds an export by its `.name` property in a minified vitest chunk. + * Minified export keys change across versions, so we search by function/class name. + * @param {object} pkg - The module exports object + * @param {string} name - The `.name` value to look for + * @returns {{ key: string, value: Function } | undefined} + */ +function findExportByName (pkg, name) { + for (const [key, value] of Object.entries(pkg)) { + if (value?.name === name) { + return { key, value } + } + } +} + +function getBaseSequencerExport (vitestPackage) { + return findExportByName(vitestPackage, 'BaseSequencer') } function getChannelPromise (channelToPublishTo, frameworkVersion) { @@ -157,19 +172,19 @@ function getChannelPromise (channelToPublishTo, frameworkVersion) { } function isCliApiPackage (vitestPackage) { - return vitestPackage.s?.name === 'startVitest' + return !!findExportByName(vitestPackage, 'startVitest') } -function isTestPackage (testPackage) { - return testPackage.V?.name === 'VitestTestRunner' +function getTestRunnerExport (testPackage) { + return findExportByName(testPackage, 'VitestTestRunner') || findExportByName(testPackage, 'TestRunner') } -function hasForksPoolWorker (vitestPackage) { - return vitestPackage.f?.name === 'ForksPoolWorker' +function getForksPoolWorkerExport (vitestPackage) { + return findExportByName(vitestPackage, 'ForksPoolWorker') } -function hasThreadsPoolWorker (vitestPackage) { - return vitestPackage.T?.name === 'ThreadsPoolWorker' +function getThreadsPoolWorkerExport (vitestPackage) { + return findExportByName(vitestPackage, 'ThreadsPoolWorker') } function getSessionStatus (state) { @@ -447,7 +462,11 @@ function getCliOrStartVitestWrapper (frameworkVersion) { } function getCreateCliWrapper (vitestPackage, frameworkVersion) { - shimmer.wrap(vitestPackage, 'c', getCliOrStartVitestWrapper(frameworkVersion)) + const createCliExport = findExportByName(vitestPackage, 'createCLI') + if (!createCliExport) { + return vitestPackage + } + shimmer.wrap(vitestPackage, createCliExport.key, getCliOrStartVitestWrapper(frameworkVersion)) return vitestPackage } @@ -534,27 +553,30 @@ function getStartVitestWrapper (cliApiPackage, frameworkVersion) { if (!isCliApiPackage(cliApiPackage)) { return cliApiPackage } - shimmer.wrap(cliApiPackage, 's', getCliOrStartVitestWrapper(frameworkVersion)) + const startVitestExport = findExportByName(cliApiPackage, 'startVitest') + shimmer.wrap(cliApiPackage, startVitestExport.key, getCliOrStartVitestWrapper(frameworkVersion)) - if (hasForksPoolWorker(cliApiPackage)) { + const forksPoolWorker = getForksPoolWorkerExport(cliApiPackage) + if (forksPoolWorker) { // function is async - shimmer.wrap(cliApiPackage.f.prototype, 'start', start => function () { + shimmer.wrap(forksPoolWorker.value.prototype, 'start', start => function () { vitestPool = 'child_process' this.env.DD_VITEST_WORKER = '1' return start.apply(this, arguments) }) - shimmer.wrap(cliApiPackage.f.prototype, 'on', getWrappedOn) + shimmer.wrap(forksPoolWorker.value.prototype, 'on', getWrappedOn) } - if (hasThreadsPoolWorker(cliApiPackage)) { + const threadsPoolWorker = getThreadsPoolWorkerExport(cliApiPackage) + if (threadsPoolWorker) { // function is async - shimmer.wrap(cliApiPackage.T.prototype, 'start', start => function () { + shimmer.wrap(threadsPoolWorker.value.prototype, 'start', start => function () { vitestPool = 'worker_threads' this.env.DD_VITEST_WORKER = '1' return start.apply(this, arguments) }) - shimmer.wrap(cliApiPackage.T.prototype, 'on', getWrappedOn) + shimmer.wrap(threadsPoolWorker.value.prototype, 'on', getWrappedOn) } return cliApiPackage } @@ -747,7 +769,10 @@ function wrapVitestTestRunner (VitestTestRunner) { } const lastExecutionStatus = task.result.state - const shouldFlipStatus = isEarlyFlakeDetectionEnabled || attemptToFixTasks.has(task) + const isAtf = attemptToFixTasks.has(task) + const isQuarantinedOrDisabledAtf = isAtf && (quarantinedTasks.has(task) || disabledTasks.has(task)) + const shouldTrackStatuses = isEarlyFlakeDetectionEnabled || isAtf + const shouldFlipStatus = isEarlyFlakeDetectionEnabled || isQuarantinedOrDisabledAtf const statuses = taskToStatuses.get(task) // These clauses handle task.repeats, whether EFD is enabled or not @@ -765,8 +790,10 @@ function wrapVitestTestRunner (VitestTestRunner) { } else { testPassCh.publish({ task, ...ctx.currentStore }) } - if (shouldFlipStatus) { + if (shouldTrackStatuses) { statuses.push(lastExecutionStatus) + } + if (shouldFlipStatus) { // If we don't "reset" the result.state to "pass", once a repetition fails, // vitest will always consider the test as failed, so we can't read the actual status // This means that we change vitest's behavior: @@ -776,7 +803,7 @@ function wrapVitestTestRunner (VitestTestRunner) { } } } else if (numRepetition === task.repeats) { - if (shouldFlipStatus) { + if (shouldTrackStatuses) { statuses.push(lastExecutionStatus) } @@ -864,11 +891,12 @@ addHook({ versions: ['>=4.0.0'], filePattern: 'dist/chunks/test.*', }, (testPackage) => { - if (!isTestPackage(testPackage)) { + const testRunner = getTestRunnerExport(testPackage) + if (!testRunner) { return testPackage } - wrapVitestTestRunner(testPackage.V) + wrapVitestTestRunner(testRunner.value) return testPackage }) @@ -937,8 +965,9 @@ addHook({ versions: ['>=3.0.9'], filePattern: 'dist/chunks/coverage.*', }, (coveragePackage) => { - if (isBaseSequencer(coveragePackage)) { - shimmer.wrap(coveragePackage.b.prototype, 'sort', getSortWrapper) + const baseSequencer = getBaseSequencerExport(coveragePackage) + if (baseSequencer) { + shimmer.wrap(baseSequencer.value.prototype, 'sort', getSortWrapper) } return coveragePackage }) diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index a29db608fa5..1889ede16f0 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -607,7 +607,7 @@ class CypressPlugin { [TEST_SESSION_NAME]: testSessionName, } } - const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, false, this.frameworkVersion) + const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, this.frameworkVersion) metadataTags.test = { ...metadataTags.test, ...libraryCapabilitiesTags, diff --git a/packages/datadog-plugin-cypress/src/support.js b/packages/datadog-plugin-cypress/src/support.js index fddfd5ad1fa..5f66012c222 100644 --- a/packages/datadog-plugin-cypress/src/support.js +++ b/packages/datadog-plugin-cypress/src/support.js @@ -61,18 +61,16 @@ Cypress.on('fail', (err, runnable) => { } const testName = runnable.fullTitle() - const { isQuarantined, isAttemptToFix } = getTestProperties(testName) + const { isQuarantined, isDisabled } = getTestProperties(testName) - // For pure quarantined tests (not attemptToFix), suppress the failure - // This makes the test "pass" from Cypress's perspective while we still track the error - if (isQuarantined && !isAttemptToFix) { - // Store the error so we can report it to Datadog in afterEach + // Suppress failures for quarantined or disabled tests so they don't affect the exit code. + // This applies regardless of attempt-to-fix status: per spec, quarantined/disabled test + // results are always ignored. + if (isQuarantined || isDisabled) { quarantinedTestErrors.set(testName, err) - // Don't re-throw - this prevents Cypress from marking the test as failed return } - // For all other tests (including attemptToFix), let the error propagate normally throw err }) diff --git a/packages/datadog-plugin-langgraph/src/index.js b/packages/datadog-plugin-langgraph/src/index.js new file mode 100644 index 00000000000..4e927200d53 --- /dev/null +++ b/packages/datadog-plugin-langgraph/src/index.js @@ -0,0 +1,24 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const langgraphLLMObsPlugins = require('../../dd-trace/src/llmobs/plugins/langgraph') +const streamPlugin = require('./stream') + +const plugins = {} + +// CRITICAL: LLMObs plugins MUST come first +for (const Plugin of langgraphLLMObsPlugins) { + plugins[Plugin.id] = Plugin +} + +// Tracing plugins second +for (const Plugin of streamPlugin) { + plugins[Plugin.id] = Plugin +} + +class LanggraphPlugin extends CompositePlugin { + static id = 'langgraph' + static plugins = plugins +} + +module.exports = LanggraphPlugin diff --git a/packages/datadog-plugin-langgraph/src/stream.js b/packages/datadog-plugin-langgraph/src/stream.js new file mode 100644 index 00000000000..6bb5a71cb07 --- /dev/null +++ b/packages/datadog-plugin-langgraph/src/stream.js @@ -0,0 +1,41 @@ +'use strict' + +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const { spanHasError } = require('../../dd-trace/src/llmobs/util') + +// We are only tracing Pregel.stream because Pregel.invoke calls stream internally resulting in +// a graph with spans that look redundant. +class PregelStreamPlugin extends TracingPlugin { + static id = 'langgraph_pregel_stream' + static prefix = 'tracing:orchestrion:@langchain/langgraph:Pregel_stream' + + bindStart (ctx) { + this.startSpan('LangGraph', { + service: this.config.service, + kind: 'internal', + component: 'langgraph', + }, ctx) + return ctx.currentStore + } +} +class NextStreamPlugin extends TracingPlugin { + static id = 'langgraph_stream_next' + static prefix = 'tracing:orchestrion:@langchain/langgraph:Pregel_stream_next' + + bindStart (ctx) { + return ctx.currentStore + } + + asyncEnd (ctx) { + const span = ctx.currentStore?.span + if (!span) return + if (ctx.result.done === true || spanHasError(span)) { + span.finish() + } + } +} + +module.exports = [ + PregelStreamPlugin, + NextStreamPlugin, +] diff --git a/packages/datadog-plugin-langgraph/test/index.spec.js b/packages/datadog-plugin-langgraph/test/index.spec.js new file mode 100644 index 00000000000..15955bdc567 --- /dev/null +++ b/packages/datadog-plugin-langgraph/test/index.spec.js @@ -0,0 +1,69 @@ +'use strict' + +const assert = require('node:assert/strict') +const { createIntegrationTestSuite } = require('../../dd-trace/test/setup/helpers/plugin-test-helpers') +const TestSetup = require('./test-setup') + +const testSetup = new TestSetup() + +createIntegrationTestSuite('langgraph', '@langchain/langgraph', { + category: 'llm', +}, (meta) => { + const { agent } = meta + + before(async () => { + await testSetup.setup(meta.mod) + }) + + after(async () => { + await testSetup.teardown() + }) + + beforeEach(async () => { + await agent.load('langgraph') + }) + + afterEach(async () => { + await agent.close({ ritmReset: false }) + }) + + describe('Pregel.stream() - stream', () => { + it('should generate span with correct tags (happy path)', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const allSpans = traces.flat() + const streamSpan = allSpans.find(span => span.name === 'LangGraph') + + assert.ok(streamSpan) + + assert.equal(streamSpan.name, 'LangGraph') + assert.equal(streamSpan.meta['span.kind'], 'internal') + assert.equal(streamSpan.meta.component, 'langgraph') + }) + + await testSetup.pregelStream() + + return traceAssertion + }) + + it('should generate span with error tags (error path)', async () => { + const traceAssertion = agent.assertSomeTraces((traces) => { + const allSpans = traces.flat() + const streamSpan = allSpans.find(span => span.name === 'LangGraph' && span.error === 1) + + assert.ok(streamSpan) + + assert.equal(streamSpan.name, 'LangGraph') + assert.equal(streamSpan.error, 1) + assert.equal(streamSpan.meta['span.kind'], 'internal') + assert.equal(streamSpan.meta.component, 'langgraph') + assert.ok(Object.hasOwn(streamSpan.meta, 'error.type')) + assert.ok(Object.hasOwn(streamSpan.meta, 'error.message')) + assert.ok(Object.hasOwn(streamSpan.meta, 'error.stack')) + }) + + await testSetup.pregelStreamError().catch(() => {}) + + return traceAssertion + }) + }) +}) diff --git a/packages/datadog-plugin-langgraph/test/test-setup.js b/packages/datadog-plugin-langgraph/test/test-setup.js new file mode 100644 index 00000000000..403ecf1adf3 --- /dev/null +++ b/packages/datadog-plugin-langgraph/test/test-setup.js @@ -0,0 +1,132 @@ +'use strict' + +/** + * Sample application for `@langchain/langgraph` instrumentation testing. + * Tests the Pregel class methods: invoke and stream. + */ +// Import necessary symbols from @langchain/langgraph +/** + * Sample application class for testing langgraph instrumentation. + * Creates a simple graph workflow to test Pregel.invoke() and Pregel.stream(). + */ + +class LanggraphTestSetup { + async setup (module) { + this.app = null + this.module = module + // Destructure required symbols from the langgraph module + const { Annotation, StateGraph, START, END } = module + // Define state annotation with messages array + const StateAnnotation = Annotation.Root({ + messages: Annotation({ + default: () => [], + reducer: (prev, next) => [...prev, ...next], + }), + step: Annotation({ + default: () => 0, + reducer: (prev, next) => next, + }), + }) + + // Create a new StateGraph + const graph = new StateGraph(StateAnnotation) + + // Add processing nodes + graph.addNode('preprocess', (state) => { + return { + messages: ['preprocessed'], + step: 1, + } + }) + + graph.addNode('process', (state) => { + return { + messages: ['processed'], + step: 2, + } + }) + + graph.addNode('postprocess', (state) => { + return { + messages: ['completed'], + step: 3, + } + }) + + // Define edges + graph.addEdge(START, 'preprocess') + graph.addEdge('preprocess', 'process') + graph.addEdge('process', 'postprocess') + graph.addEdge('postprocess', END) + + // Compile the graph to get a Pregel instance + this.app = graph.compile() + } + + async teardown () { + this.app = null + } + + // --- Operations --- + async pregelInvoke () { + const input = { messages: ['hello world'] } + const result = await this.app.invoke(input) + + return result + } + + async pregelInvokeError () { + // Create a separate error graph that throws + const { Annotation, StateGraph, START, END } = this.module + const StateAnnotation = Annotation.Root({ + messages: Annotation({ + default: () => [], + reducer: (prev, next) => [...prev, ...next], + }), + }) + + const errorGraph = new StateGraph(StateAnnotation) + errorGraph.addNode('error', (state) => { + throw new Error('Intentional test error') + }) + errorGraph.addEdge(START, 'error') + errorGraph.addEdge('error', END) + + const errorApp = errorGraph.compile() + await errorApp.invoke({ messages: ['test'] }) + } + + async pregelStream () { + const input = { messages: ['streaming test'] } + const stream = await this.app.stream(input) + + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + return chunks + } + + async pregelStreamError () { + // Use the happy path graph but manually trigger error via iterator.throw() + const input = { messages: ['streaming test'] } + const stream = await this.app.stream(input) + + // Get the iterator and manually throw an error + const iterator = stream[Symbol.asyncIterator]() + + // Consume one chunk first to start the stream + await iterator.next() + + // Now manually throw an error using the iterator's throw method + // This will trigger the instrumentation's error handling (line 82-91 in internal.js) + if (iterator.throw) { + await iterator.throw(new Error('Intentional test error')).catch(() => {}) + } else { + throw new Error('Intentional test error') + } + } +} + +module.exports = LanggraphTestSetup diff --git a/packages/dd-trace/src/config/defaults.js b/packages/dd-trace/src/config/defaults.js index 8e8133636f7..1d14e84f7e9 100644 --- a/packages/dd-trace/src/config/defaults.js +++ b/packages/dd-trace/src/config/defaults.js @@ -106,6 +106,7 @@ const defaultsWithoutSupportedConfigurationEntry = { isGCPFunction: false, instrumentationSource: 'manual', isServiceUserProvided: false, + isServiceNameInferred: true, lookup: undefined, plugins: true, } diff --git a/packages/dd-trace/src/config/index.js b/packages/dd-trace/src/config/index.js index f5fe3703418..02f0cd3e083 100644 --- a/packages/dd-trace/src/config/index.js +++ b/packages/dd-trace/src/config/index.js @@ -722,8 +722,10 @@ class Config { // Priority: // DD_SERVICE > tags.service > OTEL_SERVICE_NAME > NX_TASK_TARGET_PROJECT (if DD_ENABLE_NX_SERVICE_NAME) > default let serviceName = DD_SERVICE || tags.service || OTEL_SERVICE_NAME + let isServiceNameInferred if (!serviceName && NX_TASK_TARGET_PROJECT) { if (isTrue(DD_ENABLE_NX_SERVICE_NAME)) { + isServiceNameInferred = true serviceName = NX_TASK_TARGET_PROJECT } else if (DD_MAJOR < 6) { // Warn about v6 behavior change for Nx projects @@ -734,6 +736,7 @@ class Config { } } setString(target, 'service', serviceName) + if (serviceName) setBoolean(target, 'isServiceNameInferred', isServiceNameInferred ?? false) if (DD_SERVICE_MAPPING) { target.serviceMapping = Object.fromEntries( DD_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) @@ -1004,7 +1007,11 @@ class Config { setUnit(opts, 'sampleRate', options.sampleRate ?? options.ingestion.sampleRate) opts['sampler.rateLimit'] = maybeInt(options.rateLimit ?? options.ingestion.rateLimit) setSamplingRule(opts, 'sampler.rules', options.samplingRules) - setString(opts, 'service', options.service || tags.service) + const optService = options.service || tags.service + setString(opts, 'service', optService) + if (optService) { + setBoolean(opts, 'isServiceNameInferred', false) + } opts.serviceMapping = options.serviceMapping setString(opts, 'site', options.site) if (options.spanAttributeSchema) { diff --git a/packages/dd-trace/src/config/supported-configurations.json b/packages/dd-trace/src/config/supported-configurations.json index dffd439c88a..c3fa3368afd 100644 --- a/packages/dd-trace/src/config/supported-configurations.json +++ b/packages/dd-trace/src/config/supported-configurations.json @@ -2995,6 +2995,13 @@ "default": "true" } ], + "DD_TRACE_LANGGRAPH_ENABLED": [ + { + "implementation": "C", + "type": "boolean", + "default": "true" + } + ], "DD_TRACE_LDAPJS_ENABLED": [ { "implementation": "A", diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index 7a865b00782..a4fe59187c7 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -23,6 +23,7 @@ module.exports = { SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second', DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent', DECISION_MAKER_KEY: '_dd.p.dm', + SAMPLING_KNUTH_RATE: '_dd.p.ksr', PROCESS_ID: 'process_id', ERROR_TYPE: 'error.type', ERROR_MESSAGE: 'error.message', diff --git a/packages/dd-trace/src/crashtracking/crashtracker.js b/packages/dd-trace/src/crashtracking/crashtracker.js index 5f8da0bafbf..1fd2a822fb6 100644 --- a/packages/dd-trace/src/crashtracking/crashtracker.js +++ b/packages/dd-trace/src/crashtracking/crashtracker.js @@ -74,7 +74,7 @@ class Crashtracker { timeout_ms: 3000, }, timeout: { secs: 5, nanos: 0 }, - demangle_names: false, + demangle_names: true, signals: [], resolve_frames: resolveMode, } diff --git a/packages/dd-trace/src/debugger/devtools_client/config.js b/packages/dd-trace/src/debugger/devtools_client/config.js index 43fd4c8040c..acf7344cd57 100644 --- a/packages/dd-trace/src/debugger/devtools_client/config.js +++ b/packages/dd-trace/src/debugger/devtools_client/config.js @@ -2,8 +2,11 @@ const { workerData: { config: parentConfig, parentThreadId, configPort } } = require('node:worker_threads') const { getAgentUrl } = require('../../agent/url') +const processTags = require('../../process-tags') const log = require('./log') +processTags.initialize() + const config = module.exports = { ...parentConfig, parentThreadId, diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index 12d1c86e998..b9f1491febc 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -26,6 +26,7 @@ class DogStatsDClient { constructor (options = {}) { if (options.metricsProxyUrl) { this._httpOptions = { + method: 'POST', url: options.metricsProxyUrl.toString(), path: '/dogstatsd/v2/proxy', } diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js index 139fa4dec2e..48af285d76f 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js @@ -15,6 +15,17 @@ class LangChainLLMObsChainHandler extends LangChainLLMObsHandler { // chain spans will always be workflows this._tagger.tagTextIO(span, input, output) } + + getName ({ span, instance }) { + const firstCallable = instance?.first + + if (firstCallable?.constructor?.name === 'ChannelWrite') return + + const firstCallableIsLangGraph = firstCallable?.lc_namespace?.includes('langgraph') + const firstCallableName = firstCallable?.name + + return firstCallableIsLangGraph ? firstCallableName : super.getName({ span }) + } } module.exports = LangChainLLMObsChainHandler diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/index.js b/packages/dd-trace/src/llmobs/plugins/langchain/index.js index d1c08cb3fde..d9c484f353e 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/index.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/index.js @@ -54,6 +54,8 @@ class BaseLangChainLLMObsPlugin extends LLMObsPlugin { const handler = this._handlers[ctx.type] const name = handler?.getName({ span, instance }) + if (name == null) return + return { modelProvider, modelName, diff --git a/packages/dd-trace/src/llmobs/plugins/langgraph/index.js b/packages/dd-trace/src/llmobs/plugins/langgraph/index.js new file mode 100644 index 00000000000..b9be88179a2 --- /dev/null +++ b/packages/dd-trace/src/llmobs/plugins/langgraph/index.js @@ -0,0 +1,114 @@ +'use strict' + +const LLMObsPlugin = require('../base') +const { spanHasError } = require('../../util') + +const streamDataMap = new WeakMap() + +function formatIO (data) { + if (data == null) return '' + + if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { + return data + } + + if (data.constructor?.name === 'Object') { + const formatted = {} + for (const [key, value] of Object.entries(data)) { + formatted[key] = formatIO(value) + } + return formatted + } + + if (Array.isArray(data)) { + return data.map(item => formatIO(item)) + } + + try { + return JSON.stringify(data) + } catch { + return String(data) + } +} + +class PregelStreamLLMObsPlugin extends LLMObsPlugin { + static id = 'llmobs_langgraph_pregel_stream' + static integration = 'langgraph' + static prefix = 'tracing:orchestrion:@langchain/langgraph:Pregel_stream' + + getLLMObsSpanRegisterOptions (ctx) { + const name = ctx.self.name || 'LangGraph' + + const enabled = this._tracerConfig.llmobs.enabled + if (!enabled) return + + const span = ctx.currentStore?.span + if (!span) return + streamDataMap.set(span, { + streamInputs: ctx.arguments?.[0], + chunks: [], + }) + + return { + kind: 'workflow', + name, + } + } + + asyncEnd () {} +} + +class NextStreamLLMObsPlugin extends LLMObsPlugin { + static id = 'llmobs_langgraph_next_stream' + static prefix = 'tracing:orchestrion:@langchain/langgraph:Pregel_stream_next' + + start () {} // no-op: span was already registered by PregelStreamLLMObsPlugin + + end () {} // no-op: context restore is handled by PregelStreamLLMObsPlugin + + error (ctx) { + const span = ctx.currentStore?.span + if (!span) return + + this.#tagAndCleanup(span, true) + } + + setLLMObsTags (ctx) { + const span = ctx.currentStore?.span + if (!span) return + + // Accumulate chunks until done + if (ctx.result?.value && !ctx.result.done) { + const streamData = streamDataMap.get(span) + if (streamData) { + streamData.chunks.push(ctx.result.value) + } + return + } + + // Tag on last chunk + if (ctx.result?.done) { + const hasError = ctx.error || spanHasError(span) + this.#tagAndCleanup(span, hasError) + } + } + + #tagAndCleanup (span, hasError) { + const streamData = streamDataMap.get(span) + if (!streamData) return + + const { streamInputs: inputs, chunks } = streamData + const input = inputs == null ? undefined : formatIO(inputs) + const lastChunk = chunks.length > 0 ? chunks[chunks.length - 1] : undefined + const output = !hasError && lastChunk != null ? formatIO(lastChunk) : undefined + + this._tagger.tagTextIO(span, input, output) + + streamDataMap.delete(span) + } +} + +module.exports = [ + PregelStreamLLMObsPlugin, + NextStreamLLMObsPlugin, +] diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index 0843c41ec0f..9008186c107 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -125,7 +125,7 @@ module.exports = class CiPlugin extends Plugin { this._pendingRequestErrorTags = [] this.addSub(`ci:${this.constructor.id}:library-configuration`, (ctx) => { - const { onDone, isParallel, frameworkVersion } = ctx + const { onDone, frameworkVersion } = ctx ctx.currentStore = storage('legacy').getStore() if (!this.tracer._exporter || !this.tracer._exporter.getLibraryConfiguration) { @@ -143,7 +143,7 @@ module.exports = class CiPlugin extends Plugin { ? getSessionRequestErrorTags(this.testSessionSpan) : Object.fromEntries(this._pendingRequestErrorTags.map(({ tag, value }) => [tag, value])) - const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, isParallel, frameworkVersion) + const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, frameworkVersion) const metadataTags = { test: { ...libraryCapabilitiesTags, diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 99ceb6e5103..44d84e67324 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -31,6 +31,7 @@ const plugins = { get '@redis/client' () { return require('../../../datadog-plugin-redis/src') }, 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 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/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 23d940b1c41..3c5f1a2f2c8 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -150,7 +150,6 @@ const DD_CAPABILITIES_FAILED_TEST_REPLAY = '_dd.library_capabilities.failed_test const DD_CI_LIBRARY_CONFIGURATION_ERROR = '_dd.ci.library_configuration_error' const UNSUPPORTED_TIA_FRAMEWORKS = new Set(['playwright', 'vitest']) -const UNSUPPORTED_TIA_FRAMEWORKS_PARALLEL_MODE = new Set(['cucumber', 'mocha']) const MINIMUM_FRAMEWORK_VERSION_FOR_EFD = { playwright: '>=1.38.0', } @@ -170,7 +169,6 @@ const MINIMUM_FRAMEWORK_VERSION_FOR_FAILED_TEST_REPLAY = { playwright: '>=1.38.0', } -const UNSUPPORTED_ATTEMPT_TO_FIX_FRAMEWORKS_PARALLEL_MODE = new Set(['mocha']) const NOT_SUPPORTED_GRANULARITY_IMPACTED_TESTS_FRAMEWORKS = new Set(['mocha', 'playwright', 'vitest']) const TEST_LEVEL_EVENT_TYPES = [ @@ -987,9 +985,8 @@ function getFormattedError (error, repositoryRoot) { return newError } -function isTiaSupported (testFramework, isParallel) { - return !(UNSUPPORTED_TIA_FRAMEWORKS.has(testFramework) || - (isParallel && UNSUPPORTED_TIA_FRAMEWORKS_PARALLEL_MODE.has(testFramework))) +function isTiaSupported (testFramework) { + return !UNSUPPORTED_TIA_FRAMEWORKS.has(testFramework) } function isEarlyFlakeDetectionSupported (testFramework, frameworkVersion) { @@ -1016,12 +1013,12 @@ function isDisableSupported (testFramework, frameworkVersion) { : true } -function isAttemptToFixSupported (testFramework, isParallel, frameworkVersion) { +function isAttemptToFixSupported (testFramework, frameworkVersion) { if (testFramework === 'playwright') { return satisfies(frameworkVersion, MINIMUM_FRAMEWORK_VERSION_FOR_ATTEMPT_TO_FIX[testFramework]) } - return !(isParallel && UNSUPPORTED_ATTEMPT_TO_FIX_FRAMEWORKS_PARALLEL_MODE.has(testFramework)) + return true } function isFailedTestReplaySupported (testFramework, frameworkVersion) { @@ -1030,9 +1027,9 @@ function isFailedTestReplaySupported (testFramework, frameworkVersion) { : true } -function getLibraryCapabilitiesTags (testFramework, isParallel, frameworkVersion) { +function getLibraryCapabilitiesTags (testFramework, frameworkVersion) { return { - [DD_CAPABILITIES_TEST_IMPACT_ANALYSIS]: isTiaSupported(testFramework, isParallel) + [DD_CAPABILITIES_TEST_IMPACT_ANALYSIS]: isTiaSupported(testFramework) ? '1' : undefined, [DD_CAPABILITIES_EARLY_FLAKE_DETECTION]: isEarlyFlakeDetectionSupported(testFramework, frameworkVersion) @@ -1049,7 +1046,7 @@ function getLibraryCapabilitiesTags (testFramework, isParallel, frameworkVersion ? '1' : undefined, [DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX]: - isAttemptToFixSupported(testFramework, isParallel, frameworkVersion) + isAttemptToFixSupported(testFramework, frameworkVersion) ? '5' : undefined, [DD_CAPABILITIES_FAILED_TEST_REPLAY]: isFailedTestReplaySupported(testFramework, frameworkVersion) diff --git a/packages/dd-trace/src/priority_sampler.js b/packages/dd-trace/src/priority_sampler.js index 36761ce3d0e..4e586bc6a93 100644 --- a/packages/dd-trace/src/priority_sampler.js +++ b/packages/dd-trace/src/priority_sampler.js @@ -31,10 +31,21 @@ const { SAMPLING_LIMIT_DECISION, SAMPLING_AGENT_DECISION, DECISION_MAKER_KEY, + SAMPLING_KNUTH_RATE, } = require('./constants') const DEFAULT_KEY = 'service:,env:' +/** + * Formats a sampling rate as a string with up to 6 significant digits and no trailing zeros. + * + * @param {number} rate + * @returns {string} + */ +function formatKnuthRate (rate) { + return Number(rate.toPrecision(6)).toString() +} + const defaultSampler = new Sampler(AUTO_KEEP) /** @@ -254,6 +265,7 @@ class PrioritySampler { */ #getPriorityByRule (context, rule) { context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate + context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rule.sampleRate) context._sampling.mechanism = SAMPLING_MECHANISM_RULE if (rule.provenance === 'customer') context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER if (rule.provenance === 'dynamic') context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC @@ -290,9 +302,15 @@ class PrioritySampler { // TODO: Change underscored properties to private ones. const sampler = this._samplers[key] || this._samplers[DEFAULT_KEY] - context._trace[SAMPLING_AGENT_DECISION] = sampler.rate() + const rate = sampler.rate() + context._trace[SAMPLING_AGENT_DECISION] = rate - context._sampling.mechanism = sampler === defaultSampler ? SAMPLING_MECHANISM_DEFAULT : SAMPLING_MECHANISM_AGENT + if (sampler === defaultSampler) { + context._sampling.mechanism = SAMPLING_MECHANISM_DEFAULT + } else { + context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rate) + context._sampling.mechanism = SAMPLING_MECHANISM_AGENT + } return sampler.isSampled(context) ? AUTO_KEEP : AUTO_REJECT } diff --git a/packages/dd-trace/src/process-tags/index.js b/packages/dd-trace/src/process-tags/index.js index e8549bd2fc3..6fe87b848cb 100644 --- a/packages/dd-trace/src/process-tags/index.js +++ b/packages/dd-trace/src/process-tags/index.js @@ -12,8 +12,19 @@ const ENTRYPOINT_PATH = require.main?.filename || '' // entrypoint.basedir = baz // package.json.name =