diff --git a/.codecov.yml b/.codecov.yml index 125fedadc9f..096d02447bd 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -4,4 +4,12 @@ coverage: precision: 2 status: project: off - patch: off + patch: + default: + target: 95% + if_not_found: success + if_ci_failed: success + # PRs against `vN.x` release branches are typically backports / cherry-picks + # where the patch coverage is decided in the source PR; only enforce on master. + branches: + - master diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 724c69db32e..b9a7220a998 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -32,6 +32,7 @@ /packages/dd-trace/src/lambda/ @DataDog/serverless-aws @DataDog/apm-serverless /packages/dd-trace/test/lambda/ @DataDog/serverless-aws @DataDog/apm-serverless /packages/dd-trace/test/azure_metadata.spec.js @DataDog/apm-serverless +/packages/dd-trace/test/serverless.spec.js @DataDog/apm-serverless /packages/dd-trace/test/plugins/util/inferred_proxy.spec.js @DataDog/serverless-aws @DataDog/apm-serverless # IDM @@ -43,6 +44,7 @@ /benchmark/sirun/plugin-net/ @DataDog/apm-idm-js /benchmark/sirun/plugin-q/ @DataDog/apm-idm-js +/integration-tests/electron/ @DataDog/apm-idm-js /integration-tests/esbuild/ @DataDog/apm-idm-js /integration-tests/webpack/ @DataDog/apm-idm-js /integration-tests/pino.spec.js @DataDog/apm-idm-js @@ -53,6 +55,7 @@ /packages/datadog-webpack @DataDog/apm-idm-js /packages/datadog-plugin-*/ @DataDog/apm-idm-js /packages/datadog-instrumentations/ @DataDog/apm-idm-js +/packages/dd-trace/src/exporters/electron/ @DataDog/apm-idm-js /packages/dd-trace/src/plugins/ @DataDog/apm-idm-js /packages/dd-trace/test/plugins/ @DataDog/apm-idm-js /packages/dd-trace/src/service-naming/ @DataDog/apm-idm-js @@ -121,6 +124,7 @@ /integration-tests/vitest/ @DataDog/ci-app-libraries /integration-tests/vitest.config.mjs @DataDog/ci-app-libraries /integration-tests/ci-visibility-intake.js @DataDog/ci-app-libraries +/integration-tests/ci-visibility-intake.spec.js @DataDog/ci-app-libraries /integration-tests/CODEOWNERS @DataDog/ci-app-libraries /integration-tests/config-jest-multiproject.js @DataDog/ci-app-libraries /integration-tests/config-jest.js @DataDog/ci-app-libraries @@ -236,7 +240,13 @@ /.github/actions/push_to_test_optimization/ @DataDog/ci-app-libraries /.github/actions/upload-node-reports/action.yml @Datadog/lang-platform-js /.github/chainguard @DataDog/sdlc-security +/.github/codeql_config.yml @DataDog/sdlc-security +/.github/workflows/codeql-analysis.yml @DataDog/sdlc-security /.github/workflows/mirror-image.yml @Datadog/lang-platform-js +/.github/workflows/all-green.yml @Datadog/lang-platform-js +/.github/workflows/custom-node-version-dispatch.yml @Datadog/lang-platform-js +/.github/workflows/project.yml @Datadog/lang-platform-js +/.github/workflows/stale.yml @Datadog/lang-platform-js /scripts/check-no-mcr-images.js @Datadog/lang-platform-js /.github/workflows/apm-capabilities.yml @DataDog/apm-sdk-capabilities-js @@ -245,6 +255,7 @@ /.github/workflows/appsec.yml @DataDog/asm-js /.github/workflows/debugger.yml @DataDog/debugger-nodejs /.github/workflows/instrumentation.yml @DataDog/apm-idm-js +/.github/workflows/electron.yml @DataDog/apm-idm-js /.github/workflows/serverless.yml @DataDog/serverless-aws @DataDog/apm-serverless /.github/workflows/llmobs.yml @DataDog/ml-observability /.github/workflows/profiling.yml @DataDog/profiling-js diff --git a/.github/actions/node/action.yml b/.github/actions/node/action.yml index 5ee09aac834..0fc3690169f 100644 --- a/.github/actions/node/action.yml +++ b/.github/actions/node/action.yml @@ -23,12 +23,13 @@ runs: run: echo "version=$NODE_VERSION" >> "$GITHUB_OUTPUT" # Cache a tiny file containing the exact Node.js version resolved by a previous run. - # Key rotates every 20 minutes (epoch / 1200) so patches are picked up regularly. - # On cache hit, install the cached patch and skip the manifest lookup. + # Key rotates every 60 minutes (epoch / 3600), capping setup-node manifest API + # calls at one per (os, arch, major) per hour. On cache hit, install the cached + # patch directly and skip the manifest lookup. - name: Compute cache key id: cache-key shell: bash - run: echo "block=$(( $(date -u +%s) / 1200 ))" >> "$GITHUB_OUTPUT" + run: echo "block=$(( $(date -u +%s) / 3600 ))" >> "$GITHUB_OUTPUT" - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 id: node-version-cache with: @@ -50,7 +51,7 @@ runs: check-latest: ${{ steps.cached.outputs.version == '' }} registry-url: ${{ inputs.registry-url || 'https://registry.npmjs.org' }} - # Persist the resolved version so subsequent runs within this 20-minute window can reuse it. + # Persist the resolved version so subsequent runs within this 60-minute window can reuse it. - name: Save resolved version if: steps.node-version-cache.outputs.cache-hit != 'true' shell: bash diff --git a/.github/chainguard/all-green.sts.yaml b/.github/chainguard/all-green.sts.yaml new file mode 100644 index 00000000000..bf6ebb729d9 --- /dev/null +++ b/.github/chainguard/all-green.sts.yaml @@ -0,0 +1,11 @@ +issuer: https://token.actions.githubusercontent.com + +subject_pattern: 'repo:DataDog/dd-trace-js:(ref:refs/heads/master|pull_request)' + +claim_pattern: + event_name: (push|pull_request|schedule) + job_workflow_ref: DataDog/dd-trace-js/\.github/workflows/all-green\.yml@.* + +permissions: + actions: write + contents: read diff --git a/.github/chainguard/codeql-analysis.sts.yaml b/.github/chainguard/codeql-analysis.sts.yaml new file mode 100644 index 00000000000..371a3d5e5c4 --- /dev/null +++ b/.github/chainguard/codeql-analysis.sts.yaml @@ -0,0 +1,12 @@ +issuer: https://token.actions.githubusercontent.com + +subject_pattern: 'repo:DataDog/dd-trace-js:(ref:refs/heads/(master|mq-working-branch-master-.*)|pull_request)' + +claim_pattern: + event_name: (push|pull_request) + job_workflow_ref: DataDog/dd-trace-js/\.github/workflows/codeql-analysis\.yml@.* + +permissions: + contents: read + actions: read + security_events: write diff --git a/.github/chainguard/custom-node-version-dispatch.sts.yaml b/.github/chainguard/custom-node-version-dispatch.sts.yaml new file mode 100644 index 00000000000..4524399af7d --- /dev/null +++ b/.github/chainguard/custom-node-version-dispatch.sts.yaml @@ -0,0 +1,11 @@ +issuer: https://token.actions.githubusercontent.com + +subject: repo:DataDog/dd-trace-js:ref:refs/heads/master + +claim_pattern: + event_name: workflow_dispatch + ref: refs/heads/master + job_workflow_ref: DataDog/dd-trace-js/\.github/workflows/custom-node-version-dispatch\.yml@refs/heads/master + +permissions: + actions: write diff --git a/.github/chainguard/package-size-report.sts.yaml b/.github/chainguard/package-size-report.sts.yaml new file mode 100644 index 00000000000..9f81f8f5ea1 --- /dev/null +++ b/.github/chainguard/package-size-report.sts.yaml @@ -0,0 +1,10 @@ +issuer: https://token.actions.githubusercontent.com + +subject_pattern: 'repo:DataDog/dd-trace-js:(ref:refs/heads/(master|mq-working-branch-master-.*)|pull_request)' + +claim_pattern: + event_name: (push|pull_request) + job_workflow_ref: DataDog/dd-trace-js/\.github/workflows/project\.yml@.* + +permissions: + pull_requests: write diff --git a/.github/chainguard/stale.sts.yaml b/.github/chainguard/stale.sts.yaml new file mode 100644 index 00000000000..9d3d38b5e45 --- /dev/null +++ b/.github/chainguard/stale.sts.yaml @@ -0,0 +1,11 @@ +issuer: https://token.actions.githubusercontent.com + +subject: repo:DataDog/dd-trace-js:ref:refs/heads/master + +claim_pattern: + event_name: schedule + ref: refs/heads/master + job_workflow_ref: DataDog/dd-trace-js/\.github/workflows/stale\.yml@refs/heads/master + +permissions: + pull_requests: write diff --git a/.github/workflows/all-green.yml b/.github/workflows/all-green.yml index 53cc5dbaa56..a69db26140f 100644 --- a/.github/workflows/all-green.yml +++ b/.github/workflows/all-green.yml @@ -15,9 +15,13 @@ jobs: all-green: runs-on: ubuntu-latest permissions: - actions: write - contents: read + id-token: write steps: + - uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 + id: octo-sts + with: + scope: DataDog/dd-trace-js + policy: all-green - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: sparse-checkout-cone-mode: false @@ -32,6 +36,6 @@ jobs: env: DELAY: ${{ github.run_attempt == 1 && '5' || '0' }} # 5 minutes on first attempt, no delay on reruns GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ steps.octo-sts.outputs.token }} POLLING_INTERVAL: 1 RETRIES: 30 diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index feb8e411cd1..30c01d6610d 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -334,8 +334,6 @@ jobs: matrix: node-version: [eol] range: - # - '^2.6.12' skipping due to bug with couchbase integration that is blocking CI. - # TODO: diagnose and fix failures. Link to bug issue: https://github.com/DataDog/dd-trace-js/issues/6400 - "^3.0.7" - ">=4.0.0 <4.2.0" include: @@ -442,6 +440,31 @@ jobs: with: dd_api_key: ${{ steps.dd-sts.outputs.api_key }} + electron: + runs-on: ubuntu-latest + env: + PLUGINS: electron + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/latest + - uses: ./.github/actions/install + # Ubuntu 24.04 tightened AppArmor defaults and now blocks unprivileged user + # namespaces, which Electron's Chromium sandbox requires to run. Setting + # this to 0 re-enables them for the duration of the job. + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - run: | + sudo apt-get update + # Xvfb provides a virtual X display so Electron (a GUI app) can start + # on the headless CI runner without a physical screen. + sudo apt-get install -y xvfb + # Launch the virtual display on screen :99 with a 24-bit 1024×768 + # framebuffer. Output is discarded; the & backgrounds it so the step + # does not block. + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + # Point Electron at the virtual display started above. + - run: DISPLAY=:99 yarn test:plugins:ci + express: runs-on: ubuntu-latest env: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 535bb8610a3..ba4c5f14dcf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,14 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: @@ -18,13 +7,23 @@ on: # The branches below must be a subset of the branches above branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/master' && github.run_id || github.ref }} + cancel-in-progress: true + jobs: + # dd-octo-sts is used for checkout and init to reduce GITHUB_TOKEN rate-limit + # consumption. The official analyze action uploads SARIF with github.token + # (security-events: write) so GitHub derives the correct analysis_key from the + # workflow context — raw API uploads always get "(default)" which the PR + # Security tab does not recognise. wait-for-processing is disabled on the + # action to avoid spending github.token budget on status polls; processing + # completion is checked in a separate step using the dd-octo-sts token. analyze: name: Analyze runs-on: ubuntu-latest permissions: - actions: read - contents: read + id-token: write security-events: write strategy: @@ -33,22 +32,57 @@ jobs: language: ["javascript"] steps: + - uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 + id: octo-sts + with: + scope: DataDog/dd-trace-js + policy: codeql-analysis + - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.octo-sts.outputs.token }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + id: init-codeql + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} config-file: .github/codeql_config.yml + token: ${{ steps.octo-sts.outputs.token }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - - name: Autobuild - uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + id: analyze + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + with: + token: ${{ github.token }} + wait-for-processing: false + + - name: Wait for SARIF processing + env: + TOKEN: ${{ steps.octo-sts.outputs.token }} + SARIF_ID: ${{ steps.analyze.outputs.sarif-id }} + run: | + # Poll every 10s (30x = 5 min total), matching the official action's + # timeout but halving the API calls vs its default 5s interval. + for i in $(seq 1 30); do + RESULT=$(curl -sf \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/code-scanning/sarifs/$SARIF_ID") + STATUS=$(echo "$RESULT" | jq -r '.processing_status') + echo "Processing status: $STATUS" + if [ "$STATUS" = "complete" ]; then exit 0; fi + if [ "$STATUS" = "failed" ]; then + echo "$RESULT" | jq -r '.errors // empty' + exit 1 + fi + sleep 10 + done + echo "SARIF processing did not complete within 5 minutes" + exit 1 diff --git a/.github/workflows/custom-node-version-dispatch.yml b/.github/workflows/custom-node-version-dispatch.yml index 267735a4205..8d6268a9ae7 100644 --- a/.github/workflows/custom-node-version-dispatch.yml +++ b/.github/workflows/custom-node-version-dispatch.yml @@ -44,8 +44,7 @@ on: type: boolean permissions: - actions: write - contents: read + id-token: write env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} @@ -65,8 +64,13 @@ jobs: - profiling runs-on: ubuntu-latest steps: + - uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 + id: octo-sts + with: + scope: DataDog/dd-trace-js + policy: custom-node-version-dispatch - name: Trigger ${{matrix.workflow }} workflow if: ${{ inputs[matrix.workflow] }} run: gh workflow run ${{matrix.workflow}}.yml --repo ${{ github.repository }} --ref ${{ env.BRANCH_NAME }} -f latest-version=${{ inputs.node-version }} env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.octo-sts.outputs.token }} diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml new file mode 100644 index 00000000000..001dae52c1a --- /dev/null +++ b/.github/workflows/electron.yml @@ -0,0 +1,39 @@ +name: Electron + +on: + pull_request: + push: + branches: [master] + schedule: + - cron: 0 4 * * * + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +env: + MOCHA_OPTIONS: ${{ github.ref == 'refs/heads/master' && '--retries 1' || '' }} + +jobs: + macos: + runs-on: macos-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: ./.github/actions/node/latest + - uses: ./.github/actions/install + - run: yarn test:integration:electron + + ubuntu: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: ./.github/actions/node/latest + - uses: ./.github/actions/install + # Electron needs a display even for headless (show: false) windows. + # xvfb-run provides a virtual framebuffer so the test can run without a physical display. + - run: xvfb-run --auto-servernum yarn test:integration:electron diff --git a/.github/workflows/llmobs.yml b/.github/workflows/llmobs.yml index 12f6d2aa5a7..5092e54541b 100644 --- a/.github/workflows/llmobs.yml +++ b/.github/workflows/llmobs.yml @@ -54,6 +54,10 @@ jobs: dd_api_key: ${{ steps.dd-sts.outputs.api_key }} openai: + strategy: + fail-fast: false + matrix: + version: [oldest, latest] runs-on: ubuntu-latest permissions: id-token: write @@ -64,23 +68,21 @@ jobs: - uses: ./.github/actions/dd-sts-api-key id: dd-sts - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/oldest-maintenance-lts + - uses: ./.github/actions/node + with: + version: ${{ matrix.version }} - uses: ./.github/actions/install - run: yarn test:plugins:ci - - run: yarn test:llmobs:plugins:ci - shell: bash - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - run: yarn test:llmobs:plugins:ci shell: bash - uses: ./.github/actions/coverage with: - flags: llmobs-${{ github.job }} + flags: llmobs-openai-${{ matrix.version }} dd_api_key: ${{ steps.dd-sts.outputs.api_key }} - if: always() uses: ./.github/actions/testagent/logs with: - suffix: llmobs-${{ github.job }} + suffix: llmobs-${{ github.job }}-${{ matrix.version }} - uses: ./.github/actions/push_to_test_optimization if: "!cancelled()" with: diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 975506c8d41..0c71450eab9 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -80,8 +80,13 @@ jobs: package-size-report: runs-on: ubuntu-latest permissions: - pull-requests: write + id-token: write steps: + - uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 + id: octo-sts + with: + scope: DataDog/dd-trace-js + policy: package-size-report - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: ./.github/actions/node/latest - run: FILENAME=$(npm pack --silent --pack-destination /tmp) && mv /tmp/$FILENAME /tmp/dd-trace.tgz @@ -92,7 +97,7 @@ jobs: - name: Compute module size tree and report uses: qard/heaviest-objects-in-the-universe@1e02edbdda803a45537a808ede97866db47756d3 # Unreleased with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.octo-sts.outputs.token }} static-analysis: runs-on: ubuntu-latest diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6a63a43c84e..61dd4f873b7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -4,14 +4,20 @@ on: - cron: "0 4 * * *" permissions: - pull-requests: write + id-token: write jobs: stale: runs-on: ubuntu-latest steps: + - uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4 + id: octo-sts + with: + scope: DataDog/dd-trace-js + policy: stale - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: + repo-token: ${{ steps.octo-sts.outputs.token }} days-before-issue-stale: -1 # disabled for issues days-before-pr-stale: 90 # 3 months diff --git a/.github/workflows/test-optimization.yml b/.github/workflows/test-optimization.yml index f7fbf9db446..ea0ec946550 100644 --- a/.github/workflows/test-optimization.yml +++ b/.github/workflows/test-optimization.yml @@ -12,7 +12,8 @@ concurrency: cancel-in-progress: true env: - MOCHA_OPTIONS: ${{ github.ref == 'refs/heads/master' && '--retries 1' || '' }} + # TODO: remove this once we have stable tests + MOCHA_OPTIONS: --retries 2 jobs: benchmarks-e2e: @@ -90,7 +91,7 @@ jobs: permissions: id-token: write container: - image: ghcr.io/rochdev/playwright-tools@sha256:f36c96b0a8f6899692798f77e01df78bd920e272d66f7f0bf153896a9135aa91 # 1.54.1-4 + image: ghcr.io/rochdev/playwright-tools@sha256:65b8161e23ede2354d04c8a242032d9ee8ca275359eac1764c027f073d38217c # 1.54.1-5 env: DD_SERVICE: dd-trace-js-integration-tests DD_CIVISIBILITY_AGENTLESS_ENABLED: 1 diff --git a/.gitignore b/.gitignore index 97c1dbc8d36..a16e3f9249f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ Temporary Items # Logs logs *.log +node-*-junit.xml npm-debug.log* yarn-debug.log* yarn-error.log* @@ -135,6 +136,7 @@ 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/package-lock.json +!benchmark/sirun/startup/everything-fixture/package-lock.json vendor.patch __azurite_db_queue__.json __azurite_db_queue_extent__.json diff --git a/.gitlab/benchmarks.yml b/.gitlab/benchmarks.yml index df75404b2d7..9e4ec5bb122 100644 --- a/.gitlab/benchmarks.yml +++ b/.gitlab/benchmarks.yml @@ -1,6 +1,7 @@ variables: MICROBENCHMARKS_CI_IMAGE: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/benchmarking-platform:dd-trace-js SLS_CI_IMAGE: registry.ddbuild.io/ci/serverless-tools:1 + SLS_CI_BRANCH: main # Benchmark's env variables. Modify to tweak benchmark parameters. UNCONFIDENCE_THRESHOLD: "5.0" @@ -71,18 +72,6 @@ benchmark: # one, change the other — otherwise variants get silently skipped. parallel: matrix: - - MAJOR_VERSION: 18 - GROUP: 1 - - MAJOR_VERSION: 18 - GROUP: 2 - - MAJOR_VERSION: 18 - GROUP: 3 - - MAJOR_VERSION: 18 - GROUP: 4 - - MAJOR_VERSION: 18 - GROUP: 5 - - MAJOR_VERSION: 18 - GROUP: 6 - MAJOR_VERSION: 20 GROUP: 1 - MAJOR_VERSION: 20 @@ -152,16 +141,10 @@ benchmark-serverless-trigger: rules: - if: $CI_COMMIT_BRANCH == 'master' interruptible: false - - # don't run on merges to release branches ("vN.x" where N is any integer) - - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^v\d+\.x$/' - when: never - # don't run on pushes to release branches - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH =~ /^v\d+\.x$/' - when: never - interruptible: true trigger: project: DataDog/serverless-tools + branch: $SLS_CI_BRANCH strategy: depend allow_failure: true variables: @@ -174,3 +157,6 @@ benchmark-serverless-trigger: UPSTREAM_PROJECT_NAME: $CI_PROJECT_NAME UPSTREAM_GITLAB_USER_LOGIN: $GITLAB_USER_LOGIN UPSTREAM_GITLAB_USER_EMAIL: $GITLAB_USER_EMAIL + # only available on Merge Requests + UPSTREAM_MERGE_TARGET_BRANCH: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME + DD_TAGS: "SLS_CI_BRANCH:$SLS_CI_BRANCH" diff --git a/.gitlab/one-pipeline.locked.yml b/.gitlab/one-pipeline.locked.yml index ec12afd12e8..c863cb6b6b7 100644 --- a/.gitlab/one-pipeline.locked.yml +++ b/.gitlab/one-pipeline.locked.yml @@ -1,4 +1,4 @@ # DO NOT EDIT THIS FILE MANUALLY # This file is auto-generated by automation. include: - - remote: https://gitlab-templates.ddbuild.io/libdatadog/one-pipeline/ca/ca5cee1b4e12e6f5a3d789807b9063eec3995d24d0dd4e644b232ed4e5cc01eb/one-pipeline.yml + - remote: https://gitlab-templates.ddbuild.io/libdatadog/one-pipeline/ca/08b1c626970e6f9f6d0c1084b5f9ce92921646c3b349ee1a4af9bd3fc505f7b3/one-pipeline.yml diff --git a/MIGRATING.md b/MIGRATING.md index 39160fce6c0..766af9b774b 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -4,6 +4,83 @@ This guide describes the steps to upgrade dd-trace from a major version to the next. If you are having any issues related to migrating, please feel free to open an issue or contact our [support](https://www.datadoghq.com/support/) team. +## 5.0 to 6.0 (unreleased) + +### IAST security controls is env-only + +`iast.securityControlsConfiguration` (and the legacy alias +`experimental.iast.securityControlsConfiguration`) is no longer accepted as a +programmatic option. Set `DD_IAST_SECURITY_CONTROLS_CONFIGURATION` instead. + +### Plugin `whitelist` and `blacklist` options removed from types + +The deprecated `whitelist` / `blacklist` plugin options on the `http`, `ioredis`, +`iovalkey`, and `redis` plugin interfaces are no longer part of the v6 TypeScript +surface. Use `allowlist` / `blocklist` instead — both have been the canonical +names for several majors. + +### `DD_TRACE_STARTUP_LOGS` defaults to `true` + +Startup configuration is logged to the console by default. Set +`DD_TRACE_STARTUP_LOGS=false` to silence it. + +### `experimental.iast` configuration removed + +The `experimental.iast.*` programmatic aliases have been removed. Use the +canonical top-level `iast.*` fields and `DD_IAST_*` environment variables +instead. + +### AppSec extended-data-collection programmatic config removed from types + +`appsec.extendedHeadersCollection.{enabled,redaction,maxHeaders}` and +`appsec.rasp.bodyCollection` are no longer part of the v6 TypeScript surface. +Configure these features through the Datadog UI and Remote Configuration +instead — the runtime keeps consuming the values pushed by RC. + +The matching `DD_APPSEC_COLLECT_ALL_HEADERS`, +`DD_APPSEC_HEADER_COLLECTION_REDACTION_ENABLED`, +`DD_APPSEC_MAX_COLLECTED_HEADERS`, and `DD_APPSEC_RASP_COLLECT_REQUEST_BODY` +environment variables are deprecated in v6 and will follow in a future major. + +### `experimental.b3` removed + +The `experimental.b3` programmatic flag and `DD_TRACE_EXPERIMENTAL_B3_ENABLED` +env var are gone. Configure b3 propagation via `DD_TRACE_PROPAGATION_STYLE` +directly (see the renamed-style note below). + +### Profiling experimental aliases removed + +`DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED`, +`DD_PROFILING_EXPERIMENTAL_CPU_ENABLED`, +`DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED`, and +`DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED` are gone. Use the canonical names +without the `_EXPERIMENTAL_` segment. + +### `DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED` removed + +Use `DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED` instead. + +### `"b3 single header"` propagation style renamed to `"b3"` + +The historical `'b3'` value used to mean multi-header; per the OTel `b3` +propagator spec, `'b3'` now means single-header. Multi-header propagation is +the existing `'b3multi'` value. The legacy `'b3 single header'` spelling is +still accepted on `DD_TRACE_PROPAGATION_STYLE` and the programmatic option as +a quiet alias for `'b3'`; prefer the canonical `'b3'` going forward. + +### `experimental.appsec` configuration removed + +The `experimental.appsec.*` programmatic aliases (and +`experimental.appsec.standalone.enabled`) have been removed. Use the canonical +top-level `appsec.*` fields, and `apmTracingEnabled` (or +`DD_APM_TRACING_ENABLED`) to control standalone ASM mode. + +### `ingestion` option removed + +The `ingestion: { sampleRate, rateLimit }` wrapper has been removed. Set +`sampleRate` and `rateLimit` directly on the top-level `TracerOptions` object, +or use `DD_TRACE_SAMPLE_RATE` / `DD_TRACE_RATE_LIMIT`. + ## 4.0 to 5.0 ### Node 16 is no longer supported @@ -14,9 +91,9 @@ our support policy. ### Update `trace` TypeScript declaration -The TypeScript declaration for `trace` has been updated to enforce -that calls to `tracer.trace(name, fn)` must receive a function which takes at least -the span object. Previously the span was technically optional when it should not have +The TypeScript declaration for `trace` has been updated to enforce +that calls to `tracer.trace(name, fn)` must receive a function which takes at least +the span object. Previously the span was technically optional when it should not have been as the span must be handled. ## 3.0 to 4.0 @@ -139,21 +216,21 @@ point, there should be no impact as no application is expected to be using them. #### Cypress -`dd-trace/cypress/plugin` and `dd-trace/cypress/support` are removed, so you won't +`dd-trace/cypress/plugin` and `dd-trace/cypress/support` are removed, so you won't be able to use them for your `cypress` instrumentation. Use `dd-trace/ci/cypress/plugin` -and `dd-trace/ci/cypress/support` instead for your plugin and support configuration -respectively. +and `dd-trace/ci/cypress/support` instead for your plugin and support configuration +respectively. #### Jest The use of `'dd-trace/ci/jest/env'` in [`testEnvironment`](https://jestjs.io/docs/configuration#testenvironment-string) -is no longer supported. +is no longer supported. To instrument `jest` tests now, add `'-r dd-trace/ci/init'` to the `NODE_OPTIONS` environment variable passed to the process running the tests, for example, `NODE_OPTIONS='-r dd-trace/ci/init' yarn test`. #### Mocha -The use of `--require dd-trace/ci/init` as a `mocha` flag is no longer supported. +The use of `--require dd-trace/ci/init` as a `mocha` flag is no longer supported. To instrument `mocha` tests now, add `'-r dd-trace/ci/init'` to the `NODE_OPTIONS` environment variable passed to the process running the tests, for example, `NODE_OPTIONS='-r dd-trace/ci/init' yarn test`. diff --git a/benchmark/sirun/appsec-iast/insecure-bank.js b/benchmark/sirun/appsec-iast/insecure-bank.js index 0bfef252aa7..2eab494d2ff 100644 --- a/benchmark/sirun/appsec-iast/insecure-bank.js +++ b/benchmark/sirun/appsec-iast/insecure-bank.js @@ -1,10 +1,11 @@ 'use strict' const http = require('http') -const app = require('/opt/insecure-bank-js/app') // eslint-disable-line import/no-absolute-path +const loadInsecureBank = require('../load-insecure-bank') const { port } = require('./common') +const app = loadInsecureBank() app.set('port', port) const server = http.createServer(app) diff --git a/benchmark/sirun/appsec/insecure-bank.js b/benchmark/sirun/appsec/insecure-bank.js index 0bfef252aa7..2eab494d2ff 100644 --- a/benchmark/sirun/appsec/insecure-bank.js +++ b/benchmark/sirun/appsec/insecure-bank.js @@ -1,10 +1,11 @@ 'use strict' const http = require('http') -const app = require('/opt/insecure-bank-js/app') // eslint-disable-line import/no-absolute-path +const loadInsecureBank = require('../load-insecure-bank') const { port } = require('./common') +const app = loadInsecureBank() app.set('port', port) const server = http.createServer(app) diff --git a/benchmark/sirun/debugger/start-devtools-client.js b/benchmark/sirun/debugger/start-devtools-client.js index 75fd0e235e1..6611adba26f 100644 --- a/benchmark/sirun/debugger/start-devtools-client.js +++ b/benchmark/sirun/debugger/start-devtools-client.js @@ -1,6 +1,9 @@ 'use strict' -const assert = require('node:assert') +const assert = require('node:assert/strict') + +// Entry point normally primes this; bench imports src directly. +globalThis[Symbol.for('dd-trace')] ??= { beforeExitHandlers: new Set() } const getConfig = require('../../../packages/dd-trace/src/config') const { start } = require('../../../packages/dd-trace/src/debugger') @@ -33,3 +36,5 @@ const rc = { } start(config, rc) + +assert.ok(globalThis[Symbol.for('dd-trace')].utilTypes, 'debugger.start did not populate utilTypes') diff --git a/benchmark/sirun/exporting-pipeline/index.js b/benchmark/sirun/exporting-pipeline/index.js index f3395667f00..f3ecdbfd0d7 100644 --- a/benchmark/sirun/exporting-pipeline/index.js +++ b/benchmark/sirun/exporting-pipeline/index.js @@ -2,6 +2,11 @@ // TODO: Update setup script to not leave agent process running in background. +const assert = require('node:assert/strict') + +// Entry point normally primes this; bench imports src directly. +globalThis[Symbol.for('dd-trace')] ??= { beforeExitHandlers: new Set() } + const hostname = require('os').hostname() const SpanProcessor = require('../../../packages/dd-trace/src/span_processor') const Exporter = require('../../../packages/dd-trace/src/exporters/agent/index') @@ -59,7 +64,12 @@ for (let i = 0, parent = null; i < 30; i++) { parent = createSpan(parent) } -let iterations = 0 +const writerCountBefore = exporter._writer?._encoder?.count?.() ?? 0 +sp.process(finished[0]) +const writerCountAfter = exporter._writer?._encoder?.count?.() ?? 0 +assert.ok(writerCountAfter > writerCountBefore, 'span processor did not advance encoder count') + +let iterations = 1 function processSpans () { sp.process(finished[0]) trace.finished = finished diff --git a/benchmark/sirun/load-insecure-bank.js b/benchmark/sirun/load-insecure-bank.js new file mode 100644 index 00000000000..74a54bc6198 --- /dev/null +++ b/benchmark/sirun/load-insecure-bank.js @@ -0,0 +1,38 @@ +'use strict' + +const url = require('url') + +/** + * @returns {import('http').RequestListener} + */ +function loadInsecureBank () { + // eslint-disable-next-line n/no-deprecated-api + const parse = url.parse + /** + * @param {string} value + * @param {boolean} [parseQueryString] + * @param {boolean} [slashesDenoteHost] + */ + // eslint-disable-next-line n/no-deprecated-api + url.parse = function parseSqliteMemory (value, parseQueryString, slashesDenoteHost) { + if (value === 'sqlite::memory:') { + return { + protocol: 'sqlite:', + hostname: '', + pathname: '/:memory:', + query: {}, + } + } + + return parse(value, parseQueryString, slashesDenoteHost) + } + + try { + return require('/opt/insecure-bank-js/app') // eslint-disable-line import/no-absolute-path + } finally { + // eslint-disable-next-line n/no-deprecated-api + url.parse = parse + } +} + +module.exports = loadInsecureBank diff --git a/benchmark/sirun/log/index.js b/benchmark/sirun/log/index.js index 4e51600c4fa..ff1016e3d23 100644 --- a/benchmark/sirun/log/index.js +++ b/benchmark/sirun/log/index.js @@ -1,23 +1,35 @@ 'use strict' +const assert = require('node:assert/strict') + +const { debugChannel, errorChannel } = require('../../../packages/dd-trace/src/log/channels') const log = require('../../../packages/dd-trace/src/log') -const { - DD_TRACE_DEBUG = 'true', - WITH_LEVEL = 'debug', -} = process.env +const { WITH_LEVEL = 'debug' } = process.env -require('../../..').init({ - debug: DD_TRACE_DEBUG, +// Override the default console-backed logger to isolate dispatch + filtering cost. +log.configure({ + logger: { + debug () {}, + info () {}, + warn () {}, + error () {}, + }, }) -log.use({ - debug () {}, - info () {}, - warn () {}, - error () {}, -}) +const debugLevel = process.env.DD_TRACE_LOG_LEVEL ?? 'debug' +const debugEnabled = process.env.DD_TRACE_DEBUG === 'true' +assert.equal( + debugChannel.hasSubscribers, + debugEnabled && (debugLevel === 'trace' || debugLevel === 'debug'), + `debugChannel.hasSubscribers mismatch (DD_TRACE_DEBUG=${process.env.DD_TRACE_DEBUG}, level=${debugLevel})` +) +assert.equal( + errorChannel.hasSubscribers, + debugEnabled, + `errorChannel.hasSubscribers mismatch (DD_TRACE_DEBUG=${process.env.DD_TRACE_DEBUG})` +) -for (let i = 0; i < 10000000; i++) { +for (let i = 0; i < 1_000_000; i++) { log[WITH_LEVEL](() => 'message') } diff --git a/benchmark/sirun/log/meta.json b/benchmark/sirun/log/meta.json index 80ff8ea1c99..696b4f88ce8 100644 --- a/benchmark/sirun/log/meta.json +++ b/benchmark/sirun/log/meta.json @@ -7,8 +7,8 @@ "instructions": true, "variants": { "without-log": { "env": { "DD_TRACE_DEBUG": "false" } }, - "skip-log": { "env": { "WITH_LEVEL": "debug", "DD_TRACE_LOG_LEVEL": "error" } }, - "with-debug": { "env": { "WITH_LEVEL": "debug", "DD_TRACE_LOG_LEVEL": "debug" } }, - "with-error": { "env": { "WITH_LEVEL": "error", "DD_TRACE_LOG_LEVEL": "error" } } + "skip-log": { "env": { "DD_TRACE_DEBUG": "true", "WITH_LEVEL": "debug", "DD_TRACE_LOG_LEVEL": "error" } }, + "with-debug": { "env": { "DD_TRACE_DEBUG": "true", "WITH_LEVEL": "debug", "DD_TRACE_LOG_LEVEL": "debug" } }, + "with-error": { "env": { "DD_TRACE_DEBUG": "true", "WITH_LEVEL": "error", "DD_TRACE_LOG_LEVEL": "error" } } } } diff --git a/benchmark/sirun/profiler/index.js b/benchmark/sirun/profiler/index.js index d43c62b05dc..41660a0939d 100644 --- a/benchmark/sirun/profiler/index.js +++ b/benchmark/sirun/profiler/index.js @@ -1,24 +1,19 @@ 'use strict' -const { - profiler, -} = require('../../../packages/dd-trace/src/profiling') +const assert = require('node:assert/strict') -const { PROFILER } = process.env +const tracer = require('../../..') -const profilers = [] +const { PROFILER } = process.env -if (PROFILER === 'wall' || PROFILER === 'all') { - profilers.push('wall') +if (PROFILER !== 'wall' && PROFILER !== 'all') { + process.env.DD_PROFILING_WALLTIME_ENABLED = 'false' } -if (PROFILER === 'space' || PROFILER === 'all') { - profilers.push('space') +if (PROFILER !== 'space' && PROFILER !== 'all') { + process.env.DD_PROFILING_HEAP_ENABLED = 'false' } +process.env.DD_PROFILING_HEAP_SAMPLING_INTERVAL = '0' -const exporters = ['none'] +tracer.init({ profiling: 'true' }) -profiler.start(/** @type {import('../../../packages/dd-trace/src/config/config-base')} */ ({ - DD_PROFILING_PROFILERS: profilers, - DD_PROFILING_EXPORTERS: exporters, - DD_PROFILING_HEAP_SAMPLING_INTERVAL: 0, -})) +assert.equal(tracer._profilerStarted, true, 'profiler.start did not return true') diff --git a/benchmark/sirun/runall.sh b/benchmark/sirun/runall.sh index 6790eea1572..40d7fd47f7f 100755 --- a/benchmark/sirun/runall.sh +++ b/benchmark/sirun/runall.sh @@ -5,10 +5,15 @@ set -e DIRS=($(ls -d */ | sed 's:/$::')) # Array of subdirectories CWD=$(pwd) +# Background subshells can't share a bash variable, so failed variants +# write their dir/variant name here and the parent counts lines after `wait`. +FAILURES_FILE=$(mktemp) + function cleanup { for D in "${DIRS[@]}"; do rm -f "${CWD}/${D}/meta-temp.json" done + rm -f "$FAILURES_FILE" } trap cleanup EXIT @@ -32,6 +37,11 @@ fi && PLUGINS="bluebird|q|graphql|express" yarn services ) +( + cd "${CWD}/startup/everything-fixture" && + npm ci --no-audit --no-fund || (sleep 60 && npm ci --no-audit --no-fund) +) + # run each test in parallel for a given version of Node.js # once all of the tests have complete move on to the next version @@ -90,7 +100,16 @@ for D in "${DIRS[@]}"; do export SIRUN_VARIANT=$V - (time node ../run-one-variant.js >> ../results.ndjson && echo "${D}/${V} finished." || echo "${D}/${V} FAILED on core ${CPU_AFFINITY}" >&2) & + ( + if time node ../run-one-variant.js >> ../results.ndjson; then + echo "${D}/${V} finished." + else + echo "${D}/${V} FAILED on core ${CPU_AFFINITY}" >&2 + # Append-only writes to a single tempfile from parallel subshells are + # atomic on Linux below PIPE_BUF (4 KiB); each line here is ~30 bytes. + echo "${D}/${V}" >> "$FAILURES_FILE" + fi + ) & ((CPU_AFFINITY=CPU_AFFINITY+1)) fi @@ -110,3 +129,11 @@ if [ "$DEBUG_RESULTS" == "true" ]; then fi echo "all tests for ${VERSION} have now completed." + +FAILED_COUNT=$(wc -l < "$FAILURES_FILE" | tr -d ' ') +if [[ "${FAILED_COUNT}" -gt 0 ]]; then + echo "" >&2 + echo "${FAILED_COUNT} variant(s) failed:" >&2 + sed 's/^/ - /' "$FAILURES_FILE" >&2 + exit 1 +fi diff --git a/benchmark/sirun/startup/README.md b/benchmark/sirun/startup/README.md index 69c311d778c..8fe7d115ce9 100644 --- a/benchmark/sirun/startup/README.md +++ b/benchmark/sirun/startup/README.md @@ -1,7 +1,52 @@ -This is a simple startup test. It tests with an without the tracer, and with -and without requiring every dependency and devDependency in the package.json, -for a total of four variants. +# startup -While it's unrealistic to load all the tracer's devDependencies, the intention -is to simulate loading a lot of dependencies for an application, and have them -either be intercepted by our loader hooks, or not. +Measures tracer startup overhead with and without a realistic dependency +load surface. The signal is `with-tracer-everything` − `control-everything`: +how much the tracer's loader hooks add when there's a representative +dependency graph to wrap. See `meta.json` for the variant set. + +## The fixture + +`everything-fixture/` is a self-contained sub-project with its own +`package.json`, `package-lock.json`, and `node_modules`. The bench loads +it through a single `require('./everything-fixture')`. Stability +properties: + +- dd-trace's own dependency tree (top-level deps, vendoring, hoisting) + does not affect it. +- A fixture dependency cutting a breaking release is pinned by the + lockfile. +- The package list lives in exactly one place (the manifest), so the + `require()` list cannot drift away from the install set. +- A missing module fails `npm ci` loudly before the benchmark runs. + +The package set is curated for a realistic production Node.js +application — web frameworks, HTTP clients, database drivers, message +queues, loggers — biased toward modules that dd-trace instruments. + +## Updating the fixture + +```sh +cd benchmark/sirun/startup/everything-fixture +# edit package.json +npm install +git add package.json package-lock.json +``` + +Do not commit `node_modules/`; `runall.sh` re-installs via `npm ci`. + +The bench runs across the Node majors listed in +`.gitlab/benchmarks.yml` (currently 20–26), so every pinned version +must: + +1. Have an `engines.node` range that includes the matrix floor, + including all transitive dependencies after `npm install` resolves. +2. Be loadable through CJS `require()`. ESM-only releases (recent `got`, + `uuid`) break the fixture's `require(name)` walk; pin a CJS-era + version (e.g. `got@11`, `uuid@9`) instead. + +Some top-level packages still satisfy (1) and (2) themselves but pull +in transitive deps that bumped past the matrix floor. When that +happens, add an `overrides` entry pinning the offending transitive dep +to the last release that supports the floor; do not downgrade the +top-level package unless it has no other compatible release. diff --git a/benchmark/sirun/startup/everything-fixture/index.js b/benchmark/sirun/startup/everything-fixture/index.js new file mode 100644 index 00000000000..ab21c1dbb0a --- /dev/null +++ b/benchmark/sirun/startup/everything-fixture/index.js @@ -0,0 +1,7 @@ +'use strict' + +const { dependencies } = require('./package.json') + +for (const name of Object.keys(dependencies)) { + require(name) +} diff --git a/benchmark/sirun/startup/everything-fixture/package-lock.json b/benchmark/sirun/startup/everything-fixture/package-lock.json new file mode 100644 index 00000000000..51a127dac5f --- /dev/null +++ b/benchmark/sirun/startup/everything-fixture/package-lock.json @@ -0,0 +1,5462 @@ +{ + "name": "dd-trace-startup-everything-fixture", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dd-trace-startup-everything-fixture", + "version": "0.0.0", + "dependencies": { + "@aws-sdk/client-s3": "3.967.0", + "ajv": "8.20.0", + "amqplib": "1.0.6", + "axios": "1.16.0", + "body-parser": "2.2.2", + "bunyan": "1.8.15", + "cassandra-driver": "4.8.0", + "dayjs": "1.11.20", + "express": "5.2.1", + "fastify": "5.8.5", + "got": "11.8.6", + "graphql": "16.14.0", + "ioredis": "5.10.1", + "joi": "17.13.3", + "jsonwebtoken": "9.0.3", + "kafkajs": "2.2.4", + "knex": "3.2.10", + "koa": "3.2.0", + "lodash": "4.18.1", + "mongodb": "6.21.0", + "mongoose": "8.23.1", + "mysql2": "3.22.3", + "pg": "8.20.0", + "pino": "10.3.1", + "redis": "5.12.1", + "uuid": "9.0.1", + "winston": "3.19.0", + "ws": "8.20.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.967.0.tgz", + "integrity": "sha512-7vDlsBqd9y0dJDjCy84WMN+1r60El97IKMGlegU+l9K2+t8+Wf8bYj/J2xfm+6Ayemje6P4nkKS9tubxBLqg+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.967.0", + "@aws-sdk/credential-provider-node": "3.967.0", + "@aws-sdk/middleware-bucket-endpoint": "3.966.0", + "@aws-sdk/middleware-expect-continue": "3.965.0", + "@aws-sdk/middleware-flexible-checksums": "3.967.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-location-constraint": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-sdk-s3": "3.967.0", + "@aws-sdk/middleware-ssec": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.967.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/signature-v4-multi-region": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.967.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.2", + "@smithy/eventstream-serde-browser": "^4.2.7", + "@smithy/eventstream-serde-config-resolver": "^4.3.7", + "@smithy/eventstream-serde-node": "^4.2.7", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-blob-browser": "^4.2.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/hash-stream-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/md5-js": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.3", + "@smithy/middleware-retry": "^4.4.19", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.18", + "@smithy/util-defaults-mode-node": "^4.2.21", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.967.0.tgz", + "integrity": "sha512-7RgUwHcRMJtWme6kCHGUVT+Rn9GmNH+FHm34N9UgMXzUqQlzFMweE7T5E9O8nv3wIp7xFNB20ADaCw9Xdnox1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.967.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.967.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.967.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.2", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.3", + "@smithy/middleware-retry": "^4.4.19", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.18", + "@smithy/util-defaults-mode-node": "^4.2.21", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.967.0.tgz", + "integrity": "sha512-sJmuP7GrVmlbO6DpXkuf9Mbn6jGNNvy6PLawvaxVF150c8bpNk3w39rerRls6q1dot1dBFV2D29hBXMY1agNMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws-sdk/xml-builder": "3.965.0", + "@smithy/core": "^3.20.2", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.965.0.tgz", + "integrity": "sha512-9FbIyJ/Zz1AdEIrb0+Pn7wRi+F/0Y566ooepg0hDyHUzRV3ZXKjOlu3wJH3YwTz2UkdwQmldfUos2yDJps7RyA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.967.0.tgz", + "integrity": "sha512-+XWw0+f/txeMbEVRtTFZhgSw1ymH1ffaVKkdMBSnw48rfSohJElKmitCqdihagRTZpzh7m8qI6tIQ5t3OUqugw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.967.0.tgz", + "integrity": "sha512-0/GIAEv5pY5htg6IBMuYccBgzz3oS2DqHjHi396ziTrwlhbrCNX96AbNhQhzAx3LBZUk13sPfeapjyQ7G57Ekg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.967.0.tgz", + "integrity": "sha512-U8dMpaM6Qf6+2Qvp1uG6OcWv1RlrZW7tQkpmzEVWH8HZTGrVHIXXju64NMtIOr7yOnNwd0CKcytuD1QG+phCwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/credential-provider-env": "3.967.0", + "@aws-sdk/credential-provider-http": "3.967.0", + "@aws-sdk/credential-provider-login": "3.967.0", + "@aws-sdk/credential-provider-process": "3.967.0", + "@aws-sdk/credential-provider-sso": "3.967.0", + "@aws-sdk/credential-provider-web-identity": "3.967.0", + "@aws-sdk/nested-clients": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.967.0.tgz", + "integrity": "sha512-kbvZsZL6CBlfnb71zuJdJmBUFZN5utNrcziZr/DZ2olEOkA9vlmizE8i9BUIbmS7ptjgvRnmcY1A966yfhiblw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/nested-clients": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.967.0.tgz", + "integrity": "sha512-WuNbHs9rfKKSVok4+OBrZf0AHfzDgFYYMxN2G/q6ZfUmY4QmiPyxV5HkNFh1rqDxS9VV6kAZPo0EBmry10idSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.967.0", + "@aws-sdk/credential-provider-http": "3.967.0", + "@aws-sdk/credential-provider-ini": "3.967.0", + "@aws-sdk/credential-provider-process": "3.967.0", + "@aws-sdk/credential-provider-sso": "3.967.0", + "@aws-sdk/credential-provider-web-identity": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.967.0.tgz", + "integrity": "sha512-sNCY5JDV0whsfsZ6c2+6eUwH33H7UhKbqvCPbEYlIIa8wkGjCtCyFI3zZIJHVcMKJJ3117vSUFHEkNA7g+8rtw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.967.0.tgz", + "integrity": "sha512-0K6kITKNytFjk1UYabYUsTThgU6TQkyW6Wmt8S5zd1A/up7NSQGpp58Rpg9GIf4amQDQwb+p9FGG7emmV8FEeA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.967.0", + "@aws-sdk/core": "3.967.0", + "@aws-sdk/token-providers": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.967.0.tgz", + "integrity": "sha512-Vkr7S2ec7q/v8i/MzkHcBEdqqfWz3lyb8FDjb+NjslEwdxC3f6XwADRZzWwV1pChfx6SbsvJXKfkcF/pKAelhA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/nested-clients": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.966.0.tgz", + "integrity": "sha512-KMPZ7gtFXErd9pMpXJMBwFlxxlGIaIQrUBfj3ea7rlrNtoVHnSI4qsoldLq5l9/Ho64KoCiICH4+qXjze8JTDQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-arn-parser": "3.966.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.965.0.tgz", + "integrity": "sha512-UBxVytsmhEmFwkBnt+aV0eAJ7uc+ouNokCqMBrQ7Oc5A77qhlcHfOgXIKz2SxqsiYTsDq+a0lWFM/XpyRWraqA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.967.0.tgz", + "integrity": "sha512-RuOan0fknnAep2pTSjmJ+Heomowxg3M3s+pcs0JEW/SYnvdwYhFOTcFg2VBvGv3V1kwXxXHMlC57zoGn6pNcqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.967.0", + "@aws-sdk/crc64-nvme": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.965.0.tgz", + "integrity": "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.965.0.tgz", + "integrity": "sha512-07T1rwAarQs33mVg5U28AsSdLB5JUXu9yBTBmspFGajKVsEahIyntf53j9mAXF1N2KR0bNdP0J4A0kst4t43UQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.965.0.tgz", + "integrity": "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.965.0.tgz", + "integrity": "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.967.0.tgz", + "integrity": "sha512-Kkd6xGwTqbg7Spq1SI3ZX6PPYKdGLxdRGlXGNE3lnEPzNueQZQJKLZFpOY2aVdcAT+ytAY96N5szeeeAsFdUaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-arn-parser": "3.966.0", + "@smithy/core": "^3.20.2", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.965.0.tgz", + "integrity": "sha512-dke++CTw26y+a2D1DdVuZ4+2TkgItdx6TeuE0zOl4lsqXGvTBUG4eaIZalt7ZOAW5ys2pbDOk1bPuh4opoD3pQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.967.0.tgz", + "integrity": "sha512-2qzJzZj5u+cZiG7kz3XJPaTH4ssUY/aet1kwJsUTFKrWeHUf7mZZkDFfkXP5cOffgiOyR5ZkrmJoLKAde9hshg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@smithy/core": "^3.20.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.967.0.tgz", + "integrity": "sha512-PYa7V8w0gaNux6Sz/Z7zrHmPloEE+EKpRxQIOG/D0askTr5Yd4oO2KGgcInf65uHK3f0Z9U4CTUGHZvQvABypA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.967.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.967.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.967.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.2", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.3", + "@smithy/middleware-retry": "^4.4.19", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.4", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.18", + "@smithy/util-defaults-mode-node": "^4.2.21", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.965.0.tgz", + "integrity": "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.967.0.tgz", + "integrity": "sha512-LfpCEqe/BliiwBtNImz/Txx6MQZkDqjP2bbk+Q4Km6mYhFU9pyPlKo3AYEHfGWn92Smt1nS3S8SzIK0nL6J2Fg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.967.0.tgz", + "integrity": "sha512-Qnd/nJ0CgeUa7zQczgmdQm0vYUF7pD1G0C+dR1T7huHQHRIsgCWIsCV9wNKzOFluqtcr6YAeuTwvY0+l8XWxnA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.967.0", + "@aws-sdk/nested-clients": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.965.0.tgz", + "integrity": "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.966.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.966.0.tgz", + "integrity": "sha512-WcCLdKBK2nHhtOPE8du5XjOXaOToxGF3Ge8rgK2jaRpjkzjS0/mO+Jp2H4+25hOne3sP2twBu5BrvD9KoXQ5LQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.965.0.tgz", + "integrity": "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-endpoints": "^3.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.965.0.tgz", + "integrity": "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.967.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.967.0.tgz", + "integrity": "sha512-yUz6pCGxyG4+QaDg0dkdIBphjQp8A9rrbZa/+U3RJgRrW47hy64clFQUROzj5Poy1Ur8ICVXEUpBsSqRuYEU2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.967.0", + "@aws-sdk/types": "3.965.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.965.0.tgz", + "integrity": "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz", + "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@redis/bloom": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", + "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/client": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", + "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/json": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", + "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/search": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", + "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/time-series": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", + "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", + "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", + "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/amqplib": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-1.0.6.tgz", + "integrity": "sha512-HGls46nTCt5xInwow2gPcm9mX512CIA7VJWn/GS9A2ymt0PVcAzkd9GsK2tQuUTdKhTu87ZKGl5UMLdcBKNa4A==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, + "node_modules/bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "engines": [ + "node >=0.10.0" + ], + "license": "MIT", + "bin": { + "bunyan": "bin/bunyan" + }, + "optionalDependencies": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cassandra-driver": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.8.0.tgz", + "integrity": "sha512-HritfMGq9V7SuESeSodHvArs0mLuMk7uh+7hQK2lqdvXrvm50aWxb4RPxkK3mPDdsgHjJ427xNRFITMH2ei+Sw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "adm-zip": "~0.5.10", + "long": "~5.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cassandra-driver/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/cassandra-driver/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "hasInstallScript": true, + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "nan": "^2.14.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", + "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastify": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", + "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graphql": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/knex": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.2.10.tgz", + "integrity": "sha512-oypTHfrc9i72iyxaUQBKHOxhcr0xM65MPf6FpN02nimsftXwzXprIkLjfXdubvhbu4PMWLp023q8o8CYvHSuZw==", + "license": "MIT", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.18.1", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "pg-query-stream": "^4.14.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/koa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.2.0.tgz", + "integrity": "sha512-TrM4/tnNY7uJ1aW55sIIa+dqBvc4V14WRIAlGcWat9wV5pRS9Wr5Zk2ZTjQP1jtfIHDoHiSbPuV08P0fUZo2pg==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.8", + "content-disposition": "~1.0.1", + "content-type": "^1.0.5", + "cookies": "~0.9.1", + "delegates": "^1.0.0", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.5.0", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/koa/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/koa/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.5.tgz", + "integrity": "sha512-e0r9YBBgNCq1D1o5Dp8FMH0N5hsFtXDBiVa0qoJPHpakvZkmDKPRoGffZJII/XsHvj9An9blm+cRJ01yQqU+Dw==", + "license": "Apache-2.0" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.1.tgz", + "integrity": "sha512-gHSPD8qEwRmiXapK17hEnFWZdcFENMegHTcw5XIIg2+7R8eXQvdwSiMpD/A2oG8tKzFLLHyRXd8/eaDPAVwZgQ==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/mysql2": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.3.tgz", + "integrity": "sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "license": "MIT", + "optional": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "license": "MIT", + "optional": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redis": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", + "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.12.1", + "@redis/client": "5.12.1", + "@redis/json": "5.12.1", + "@redis/search": "5.12.1", + "@redis/time-series": "5.12.1" + }, + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^6.0.1" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "license": "MIT", + "optional": true + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.1.0.tgz", + "integrity": "sha512-Bw6h2iBDt16v6iHLChBIoVYU8CBo9GPsW8TG7h1hRVhqKhIkH6N8qkxNSmiOZTKsCLPbtWG4ViWLkU6KeKXpig==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/benchmark/sirun/startup/everything-fixture/package.json b/benchmark/sirun/startup/everything-fixture/package.json new file mode 100644 index 00000000000..41179ebf674 --- /dev/null +++ b/benchmark/sirun/startup/everything-fixture/package.json @@ -0,0 +1,40 @@ +{ + "name": "dd-trace-startup-everything-fixture", + "version": "0.0.0", + "private": true, + "description": "Dependency fixture for the startup/everything sirun benchmark. See ../README.md.", + "main": "index.js", + "dependencies": { + "@aws-sdk/client-s3": "3.967.0", + "ajv": "8.20.0", + "amqplib": "1.0.6", + "axios": "1.16.0", + "body-parser": "2.2.2", + "bunyan": "1.8.15", + "cassandra-driver": "4.8.0", + "dayjs": "1.11.20", + "express": "5.2.1", + "fastify": "5.8.5", + "got": "11.8.6", + "graphql": "16.14.0", + "ioredis": "5.10.1", + "joi": "17.13.3", + "jsonwebtoken": "9.0.3", + "kafkajs": "2.2.4", + "knex": "3.2.10", + "koa": "3.2.0", + "lodash": "4.18.1", + "mongodb": "6.21.0", + "mongoose": "8.23.1", + "mysql2": "3.22.3", + "pg": "8.20.0", + "pino": "10.3.1", + "redis": "5.12.1", + "uuid": "9.0.1", + "winston": "3.19.0", + "ws": "8.20.0" + }, + "overrides": { + "fast-xml-parser": "5.7.2" + } +} diff --git a/benchmark/sirun/startup/startup-test.js b/benchmark/sirun/startup/startup-test.js index 1a86577e03a..8685f57c58a 100644 --- a/benchmark/sirun/startup/startup-test.js +++ b/benchmark/sirun/startup/startup-test.js @@ -1,79 +1,16 @@ 'use strict' +const assert = require('node:assert/strict') + if (Number(process.env.USE_TRACER)) { require('../../..').init() } if (Number(process.env.EVERYTHING)) { - // TODO: Add a preparation step that installs these dependencies. That way we - // are independent from what is currently installed in case a dependency is - // removed. - const packages = [ - '@babel/helpers', - '@datadog/libdatadog', - '@datadog/native-appsec', - '@datadog/native-iast-taint-tracking', - '@datadog/native-metrics', - '@datadog/pprof', - '@datadog/sketches-js', - '@datadog/wasm-js-rewriter', - '@eslint/eslintrc', - '@eslint/js', - '@isaacs/ttlcache', - '@msgpack/msgpack', - '@opentelemetry/api', - '@opentelemetry/core', - '@stylistic/eslint-plugin', - 'axios', - 'benchmark', - 'body-parser', - 'crypto-randomuuid', - 'dc-polyfill', - 'eslint-plugin-cypress', - 'eslint-plugin-import', - 'eslint-plugin-mocha', - 'eslint-plugin-n', - 'eslint-plugin-promise', - 'eslint-plugin-unicorn', - 'eslint', - 'express', - 'glob', - 'globals', - 'graphql', - 'import-in-the-middle', - 'istanbul-lib-coverage', - 'jest-docblock', - 'jsonpath-plus', - 'jszip', - 'limiter', - 'lodash.sortby', - 'lru-cache', - 'mocha', - 'module-details-from-path', - 'multer', - 'mutexify', - 'nock', - 'nyc', - 'octokit', - 'opentracing', - 'pprof-format', - 'protobufjs', - 'proxyquire', - 'retry', - 'rfdc', - 'semifies', - 'semver', - 'shell-quote', - 'sinon', - 'source-map', - 'tiktoken', - 'tlhunter-sorted-set', - 'ttl-set', - 'workerpool', - 'yaml', - 'yarn-deduplicate', - ] - for (const pkg of packages) { - require(pkg) - } + require('./everything-fixture') + + assert.ok( + require.cache[require.resolve('./everything-fixture/node_modules/express')], + 'everything-fixture did not load (express not in require cache)' + ) } diff --git a/docs/API.md b/docs/API.md index 0cb5bf4f4c0..888a113216a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -46,6 +46,7 @@ tracer.use('pg', {
+
@@ -127,6 +128,7 @@ tracer.use('pg', { * [cypress](./interfaces/export_.plugins.cypress.html) * [dns](./interfaces/export_.plugins.dns.html) * [elasticsearch](./interfaces/export_.plugins.elasticsearch.html) +* [electron](./interfaces/export_.plugins.electron.html) * [express](./interfaces/export_.plugins.express.html) * [fastify](./interfaces/export_.plugins.fastify.html) * [fetch](./interfaces/export_.plugins.fetch.html) diff --git a/docs/test.ts b/docs/test.ts index 95027a098be..7577888051a 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -46,21 +46,16 @@ tracer.init({ version: '1.0.0', url: 'http://localhost', runtimeMetrics: true, - ingestion: { - sampleRate: 0.5, - rateLimit: 500 - }, experimental: { - iast: true, - b3: true, exporter: 'log' }, + iast: true, hostname: 'agent', logger: { - error (message: string | Error) {}, - warn (message: string) {}, - info (message: string) {}, - debug (message: string) {} + error(message: string | Error) { }, + warn(message: string) { }, + info(message: string) { }, + debug(message: string) { } }, plugins: false, port: 7777, @@ -70,7 +65,7 @@ tracer.init({ }, flushInterval: 1000, flushMinSpans: 500, - lookup: () => {}, + lookup: () => { }, sampleRate: 0.1, rateLimit: 1000, samplingRules: [ @@ -124,18 +119,12 @@ tracer.init({ endpointCollectionMessageLimit: 300 }, rasp: { - enabled: true, - bodyCollection: true + enabled: true }, stackTrace: { enabled: true, maxStackTraces: 5, maxDepth: 42 - }, - extendedHeadersCollection: { - enabled: true, - redaction: false, - maxHeaders: 42 } }, iast: { @@ -156,42 +145,35 @@ tracer.init({ }); tracer.init({ - experimental: { - iast: { - enabled: true, - requestSampling: 50, - maxConcurrentRequests: 4, - maxContextOperations: 30, - dbRowsToTaint: 6, - deduplicationEnabled: true, - redactionEnabled: true, - redactionNamePattern: 'password', - redactionValuePattern: 'bearer', - telemetryVerbosity: 'OFF' - }, - appsec: { - standalone: { - enabled: true - } - } + iast: { + enabled: true, + requestSampling: 50, + maxConcurrentRequests: 4, + maxContextOperations: 30, + dbRowsToTaint: 6, + deduplicationEnabled: true, + redactionEnabled: true, + redactionNamePattern: 'password', + redactionValuePattern: 'bearer', + telemetryVerbosity: 'OFF' } }) tracer.dogstatsd.increment('foo') tracer.dogstatsd.increment('foo', 2) -tracer.dogstatsd.increment('foo', 2, {a: 'b'}) +tracer.dogstatsd.increment('foo', 2, { a: 'b' }) tracer.dogstatsd.increment('foo', 2, ['a:b']) tracer.dogstatsd.decrement('foo') tracer.dogstatsd.decrement('foo', 2) -tracer.dogstatsd.decrement('foo', 2, {a: 'b'}) +tracer.dogstatsd.decrement('foo', 2, { a: 'b' }) tracer.dogstatsd.decrement('foo', 2, ['a:b']) tracer.dogstatsd.distribution('foo') tracer.dogstatsd.distribution('foo', 2) -tracer.dogstatsd.distribution('foo', 2, {a: 'b'}) +tracer.dogstatsd.distribution('foo', 2, { a: 'b' }) tracer.dogstatsd.distribution('foo', 2, ['a:b']) tracer.dogstatsd.gauge('foo') tracer.dogstatsd.gauge('foo', 2) -tracer.dogstatsd.gauge('foo', 2, {a: 'b'}) +tracer.dogstatsd.gauge('foo', 2, { a: 'b' }) tracer.dogstatsd.gauge('foo', 2, ['a:b']) tracer.dogstatsd.flush() @@ -207,7 +189,7 @@ const httpOptions = { const httpServerOptions: plugins.HttpServer = { ...httpOptions, hooks: { - request: (span?: Span, req?, res?) => {} + request: (span?: Span, req?, res?) => { } } }; @@ -244,16 +226,16 @@ const graphqlOptions: plugins.graphql = { collapse: false, signature: false, hooks: { - execute: (span?: Span, args?, res?) => {}, - validate: (span?: Span, document?, errors?) => {}, - parse: (span?: Span, source?, document?) => {} + execute: (span?: Span, args?, res?) => { }, + validate: (span?: Span, document?, errors?) => { }, + parse: (span?: Span, source?, document?) => { } } }; const elasticsearchOptions: plugins.elasticsearch = { service: 'test', hooks: { - query: (span?: Span, params?) => {}, + query: (span?: Span, params?) => { }, }, }; @@ -261,7 +243,7 @@ const awsSdkOptions: plugins.aws_sdk = { service: 'test', batchPropagationEnabled: false, hooks: { - request: (span?: Span, response?) => {}, + request: (span?: Span, response?) => { }, }, s3: false, sqs: { @@ -279,8 +261,8 @@ const redisOptions: plugins.redis = { const sharedbOptions: plugins.sharedb = { service: 'test', hooks: { - receive: (span?: Span, request?) => {}, - reply: (span?: Span, request?, reply?) => {}, + receive: (span?: Span, request?) => { }, + reply: (span?: Span, request?, reply?) => { }, }, }; @@ -295,7 +277,7 @@ const moleculerOptions: plugins.moleculer = { const openSearchOptions: plugins.opensearch = { service: 'test', hooks: { - query: (span?: Span, params?) => {}, + query: (span?: Span, params?) => { }, }, }; @@ -322,6 +304,8 @@ tracer.use('cucumber', { service: 'cucumber-service' }); tracer.use('dns'); tracer.use('elasticsearch'); tracer.use('elasticsearch', elasticsearchOptions); +tracer.use('electron'); +tracer.use('electron', { net: false, ipc: false }); tracer.use('express'); tracer.use('express', httpServerOptions); tracer.use('fastify'); @@ -439,17 +423,17 @@ span = tracer.startSpan('test', { span = tracer.startSpan('test', { childOf: null }) span = tracer.startSpan('test', { integrationName: 'testIntegration' }) -tracer.trace('test', () => {}) -tracer.trace('test', { tags: { foo: 'bar' } }, () => {}) -tracer.trace('test', { service: 'foo', resource: 'bar', type: 'baz' }, () => {}) -tracer.trace('test', { measured: true }, () => {}) -tracer.trace('test', (span: Span) => {}) -tracer.trace('test', (span: Span, fn: () => void) => {}) -tracer.trace('test', (span: Span, fn: (err: Error) => void) => {}) +tracer.trace('test', () => { }) +tracer.trace('test', { tags: { foo: 'bar' } }, () => { }) +tracer.trace('test', { service: 'foo', resource: 'bar', type: 'baz' }, () => { }) +tracer.trace('test', { measured: true }, () => { }) +tracer.trace('test', (span: Span) => { }) +tracer.trace('test', (span: Span, fn: () => void) => { }) +tracer.trace('test', (span: Span, fn: (err: Error) => void) => { }) promise = tracer.trace('test', () => Promise.resolve()) -tracer.wrap('test', () => {}) +tracer.wrap('test', () => { }) tracer.wrap('test', (foo: string) => 'test') promise = tracer.wrap('test', () => Promise.resolve())() @@ -469,10 +453,10 @@ const scope = tracer.scope() span = scope.active()!; const activateStringType: string = scope.activate(span, () => 'test'); -const activateVoidType: void = scope.activate(span, () => {}); +const activateVoidType: void = scope.activate(span, () => { }); const bindFunctionStringType: (arg1: string, arg2: number) => string = scope.bind((arg1: string, arg2: number): string => 'test'); -const bindFunctionVoidType: (arg1: string, arg2: number) => void = scope.bind((arg1: string, arg2: number): void => {}); +const bindFunctionVoidType: (arg1: string, arg2: number) => void = scope.bind((arg1: string, arg2: number): void => { }); const bindFunctionVoidTypeWithSpan: (arg1: string, arg2: number) => void = scope.bind((arg1: string, arg2: number): string => 'test', span); tracer.wrap('x', () => { @@ -530,7 +514,7 @@ tracer.profiling.setCustomLabelKeys(['customer', 'region']) tracer.profiling.setCustomLabelKeys(new Set(['customer', 'region'])) const labelResult: number = tracer.profiling.runWithLabels({ customer: 'acme', region: 'us-east' }, () => 42) tracer.profiling.runWithLabels({ tier: 'premium' }, () => { - tracer.profiling.runWithLabels({ region: 'eu-west' }, () => {}) + tracer.profiling.runWithLabels({ region: 'eu-west' }, () => { }) }) // OTel TracerProvider registers and provides a tracer @@ -646,8 +630,8 @@ llmobs.registerProcessor((llmobsSpan) => { llmobs.deregisterProcessor() // trace block of code -llmobs.trace({ name: 'name', kind: 'llm' }, () => {}) -llmobs.trace({ kind: 'llm', name: 'myLLM', modelName: 'myModel', modelProvider: 'myProvider' }, () => {}) +llmobs.trace({ name: 'name', kind: 'llm' }, () => { }) +llmobs.trace({ kind: 'llm', name: 'myLLM', modelName: 'myModel', modelProvider: 'myProvider' }, () => { }) llmobs.trace({ name: 'name', kind: 'llm' }, (span, cb) => { llmobs.annotate(span, {}) span.setTag('foo', 'bar') @@ -655,8 +639,8 @@ llmobs.trace({ name: 'name', kind: 'llm' }, (span, cb) => { }) // wrap a function -llmobs.wrap({ kind: 'llm' }, function myLLM () {})() -llmobs.wrap({ kind: 'llm', name: 'myLLM', modelName: 'myModel', modelProvider: 'myProvider' }, function myFunction () {})() +llmobs.wrap({ kind: 'llm' }, function myLLM() { })() +llmobs.wrap({ kind: 'llm', name: 'myLLM', modelName: 'myModel', modelProvider: 'myProvider' }, function myFunction() { })() // export a span llmobs.enable({ mlApp: 'myApp', agentlessEnabled: false }) diff --git a/docs/yarn.lock b/docs/yarn.lock index 1796b171e92..563bdf3fc2c 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -145,6 +145,6 @@ uc.micro@^2.0.0, uc.micro@^2.1.0: integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== yaml@^2.8.3: - version "2.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" - integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== + version "2.9.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4" + integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA== diff --git a/eslint-rules/eslint-config-names-sync.mjs b/eslint-rules/eslint-config-names-sync.mjs index ed716f68893..cafc034fc10 100644 --- a/eslint-rules/eslint-config-names-sync.mjs +++ b/eslint-rules/eslint-config-names-sync.mjs @@ -1,12 +1,31 @@ import fs from 'node:fs' +import { createRequire } from 'node:module' import path from 'node:path' import ts from 'typescript' +const require = createRequire(import.meta.url) +const { DD_MAJOR } = require('../version.js') +const applyMajorOverrides = require('../packages/dd-trace/src/config/major-overrides.js') + const IGNORED_CONFIGURATION_NAMES = new Set([ + // v6 drops `experimental.b3` from `index.d.ts`; v5 still consumes the env var. + 'experimental.b3', 'tracePropagationStyle', 'tracing', ]) +// Configuration name prefixes that are intentionally only present in +// `supported-configurations.json` for v5 backports and stripped at runtime in +// v6. Keep these out of the `index.d.ts` ↔ JSON parity check. +const IGNORED_CONFIGURATION_NAME_PREFIXES = [ + 'experimental.appsec.', + 'experimental.iast.', + 'ingestion.', +] +const IGNORED_CONFIGURATION_LEAVES = new Set([ + 'experimental.appsec', + 'experimental.iast', +]) const UNSUPPORTED_CONFIGURATION_ROOTS = new Set([ 'isCiVisibility', 'logger', @@ -69,6 +88,16 @@ function createInspectionResult (overrides) { } } +/** + * @param {string} name + * @returns {boolean} + */ +function isIgnoredConfigurationName (name) { + return IGNORED_CONFIGURATION_NAMES.has(name) || + IGNORED_CONFIGURATION_LEAVES.has(name) || + IGNORED_CONFIGURATION_NAME_PREFIXES.some((prefix) => name.startsWith(prefix)) +} + /** * @param {string} filePath * @returns {SupportedConfigurationInfo} @@ -76,6 +105,7 @@ function createInspectionResult (overrides) { function getSupportedConfigurationInfo (filePath) { const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) const supportedConfigurations = parsed?.supportedConfigurations + applyMajorOverrides(supportedConfigurations, DD_MAJOR) const names = new Set() const primaryEnvTargets = new Map() @@ -113,9 +143,18 @@ function getSupportedConfigurationInfo (filePath) { } for (const name of entry.configurationNames ?? []) { - if (typeof name === 'string' && !IGNORED_CONFIGURATION_NAMES.has(name)) { + if (typeof name !== 'string') { + continue + } + + targets.add(name) + + if (isIgnoredConfigurationName(name)) { + continue + } + + if (!entry.deprecated) { names.add(name) - targets.add(name) } } } @@ -470,8 +509,10 @@ function getIndexDtsConfigurationNames (filePath, supportedConfigurationInfo) { inspectMembers(tracerOptions.node.members, tracerOptions.namespaceKey, '') - for (const ignoredConfigurationName of IGNORED_CONFIGURATION_NAMES) { - names.delete(ignoredConfigurationName) + for (const name of names) { + if (isIgnoredConfigurationName(name)) { + names.delete(name) + } } return names @@ -513,8 +554,10 @@ export default { schema: [{ type: 'object', properties: { - indexDtsPath: { - type: 'string', + indexDtsPaths: { + type: 'array', + items: { type: 'string' }, + minItems: 1, }, supportedConfigurationsPath: { type: 'string', @@ -533,7 +576,8 @@ export default { }, create (context) { const options = context.options[0] || {} - const indexDtsPath = path.resolve(context.cwd, options.indexDtsPath || 'index.d.ts') + const indexDtsPaths = (options.indexDtsPaths ?? ['index.d.ts']) + .map(p => path.resolve(context.cwd, p)) const supportedConfigurationsPath = path.resolve( context.cwd, options.supportedConfigurationsPath || 'packages/dd-trace/src/config/supported-configurations.json' @@ -544,9 +588,22 @@ export default { let indexDtsNames let supportedConfigurationInfo + let primaryIndexDtsNames try { supportedConfigurationInfo = getSupportedConfigurationInfo(supportedConfigurationsPath) - indexDtsNames = getIndexDtsConfigurationNames(indexDtsPath, supportedConfigurationInfo) + + // Union names from all type files: a config is covered if it appears in any version. + indexDtsNames = new Set() + for (const indexDtsPath of indexDtsPaths) { + const names = getIndexDtsConfigurationNames(indexDtsPath, supportedConfigurationInfo) + for (const name of names) { + indexDtsNames.add(name) + } + } + + // Use only the primary (v6) file for the reverse check: v5-only configs are not + // required to exist in supported-configurations.json. + primaryIndexDtsNames = getIndexDtsConfigurationNames(indexDtsPaths[0], supportedConfigurationInfo) } catch (error) { context.report({ node, @@ -568,7 +625,7 @@ export default { reportMissingConfigurations( context, node, - indexDtsNames, + primaryIndexDtsNames, supportedConfigurationInfo.names, 'configurationMissingInSupportedConfigurations' ) diff --git a/eslint-rules/eslint-config-names-sync.test.mjs b/eslint-rules/eslint-config-names-sync.test.mjs index 46d0913fa0d..beb53a496f3 100644 --- a/eslint-rules/eslint-config-names-sync.test.mjs +++ b/eslint-rules/eslint-config-names-sync.test.mjs @@ -15,13 +15,14 @@ const fixturesDirectory = path.join(process.cwd(), 'eslint-rules/fixtures/config /** * @param {string} fixtureName - * @returns {{ indexDtsPath: string, supportedConfigurationsPath: string }} + * @param {string[]} [typeFiles] + * @returns {{ indexDtsPaths: string[], supportedConfigurationsPath: string }} */ -function getFixtureOptions (fixtureName) { +function getFixtureOptions (fixtureName, typeFiles = ['index.d.ts']) { const fixtureDirectory = path.join(fixturesDirectory, fixtureName) return { - indexDtsPath: path.relative(process.cwd(), path.join(fixtureDirectory, 'index.d.ts')), + indexDtsPaths: typeFiles.map(f => path.relative(process.cwd(), path.join(fixtureDirectory, f))), supportedConfigurationsPath: path.relative( process.cwd(), path.join(fixtureDirectory, 'supported-configurations.json') @@ -46,6 +47,21 @@ ruleTester.run('eslint-config-names-sync', rule, { code: '', options: [getFixtureOptions('internal-env-and-ignored-names')], }, + { + filename: path.join(fixturesDirectory, 'deprecated-skips-cross-check', 'lint-anchor.js'), + code: '', + options: [getFixtureOptions('deprecated-skips-cross-check')], + }, + { + filename: path.join(fixturesDirectory, 'iast-experimental-alias-exception', 'lint-anchor.js'), + code: '', + options: [getFixtureOptions('iast-experimental-alias-exception')], + }, + { + filename: path.join(fixturesDirectory, 'iast-canonical-still-checked', 'lint-anchor.js'), + code: '', + options: [getFixtureOptions('iast-canonical-still-checked', ['index.d.ts', 'index.d.v5.ts'])], + }, ], invalid: [ { diff --git a/eslint-rules/eslint-timer-unref.mjs b/eslint-rules/eslint-timer-unref.mjs new file mode 100644 index 00000000000..713c1a50cf5 --- /dev/null +++ b/eslint-rules/eslint-timer-unref.mjs @@ -0,0 +1,39 @@ +export default { + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce `timer.unref?.()` over `timer.unref()`. Electron does not implement `unref` on ' + + 'timer objects returned by setTimeout/setInterval/etc., so calling it directly would throw.', + }, + fixable: 'code', + schema: [], + messages: { + useOptionalChain: + 'Use `timer.unref?.()` instead of `timer.unref()` — Electron timers do not have an `unref` method.', + }, + }, + + create (context) { + return { + CallExpression (node) { + if ( + node.optional || + node.callee.type !== 'MemberExpression' || + node.callee.computed || + node.callee.property.name !== 'unref' + ) { + return + } + + context.report({ + node, + messageId: 'useOptionalChain', + fix (fixer) { + return fixer.insertTextAfter(node.callee, '?.') + }, + }) + }, + } + }, +} diff --git a/eslint-rules/eslint-timer-unref.test.mjs b/eslint-rules/eslint-timer-unref.test.mjs new file mode 100644 index 00000000000..2a0a0abb2b2 --- /dev/null +++ b/eslint-rules/eslint-timer-unref.test.mjs @@ -0,0 +1,38 @@ +import { RuleTester } from 'eslint' +import rule from './eslint-timer-unref.mjs' + +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 2020 }, +}) + +ruleTester.run('eslint-timer-unref', /** @type {import('eslint').Rule.RuleModule} */ (rule), { + valid: [ + // Already uses optional chaining on the call + 'timer.unref?.()', + 'timer?.unref?.()', + // Computed property — not matched + "timer['unref']()", + // Unrelated method calls + 'timer.ref()', + 'clearTimeout(timer)', + 'obj.unref.call(timer)', + ], + invalid: [ + { + code: 'timer.unref()', + output: 'timer.unref?.()', + errors: 1, + }, + { + code: 'const t = setTimeout(fn, 100); t.unref()', + output: 'const t = setTimeout(fn, 100); t.unref?.()', + errors: 1, + }, + // Optional member access but non-optional call — still needs ?.() + { + code: 'timer?.unref()', + output: 'timer?.unref?.()', + errors: 1, + }, + ], +}) diff --git a/eslint-rules/fixtures/config-names-sync/deprecated-skips-cross-check/index.d.ts b/eslint-rules/fixtures/config-names-sync/deprecated-skips-cross-check/index.d.ts new file mode 100644 index 00000000000..f25d801669b --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/deprecated-skips-cross-check/index.d.ts @@ -0,0 +1,8 @@ +declare namespace tracer { + export interface TracerOptions { + /** + * @env DD_KEPT + */ + kept?: boolean + } +} diff --git a/eslint-rules/fixtures/config-names-sync/deprecated-skips-cross-check/supported-configurations.json b/eslint-rules/fixtures/config-names-sync/deprecated-skips-cross-check/supported-configurations.json new file mode 100644 index 00000000000..24e43cd0af4 --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/deprecated-skips-cross-check/supported-configurations.json @@ -0,0 +1,19 @@ +{ + "supportedConfigurations": { + "DD_KEPT": [ + { + "configurationNames": [ + "kept" + ] + } + ], + "DD_DROPPED_FROM_TYPES": [ + { + "configurationNames": [ + "dropped" + ], + "deprecated": true + } + ] + } +} diff --git a/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/index.d.ts b/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/index.d.ts new file mode 100644 index 00000000000..fc5178cd27f --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/index.d.ts @@ -0,0 +1,10 @@ +declare namespace tracer { + export interface TracerOptions { + iast?: boolean | { + /** + * @env DD_IAST_ENABLED + */ + enabled?: boolean + } + } +} diff --git a/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/index.d.v5.ts b/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/index.d.v5.ts new file mode 100644 index 00000000000..63a394a279e --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/index.d.v5.ts @@ -0,0 +1,14 @@ +declare namespace tracer { + export interface TracerOptions { + iast?: boolean | { + /** + * @env DD_IAST_ENABLED + */ + enabled?: boolean + /** + * @env DD_IAST_SECURITY_CONTROLS_CONFIGURATION + */ + securityControlsConfiguration?: string + } + } +} diff --git a/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/supported-configurations.json b/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/supported-configurations.json new file mode 100644 index 00000000000..f6584030194 --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/iast-canonical-still-checked/supported-configurations.json @@ -0,0 +1,22 @@ +{ + "supportedConfigurations": { + "DD_IAST_ENABLED": [ + { + "configurationNames": [ + "iast.enabled", + "iast", + "experimental.iast.enabled", + "experimental.iast" + ] + } + ], + "DD_IAST_SECURITY_CONTROLS_CONFIGURATION": [ + { + "configurationNames": [ + "iast.securityControlsConfiguration", + "experimental.iast.securityControlsConfiguration" + ] + } + ] + } +} diff --git a/eslint-rules/fixtures/config-names-sync/iast-experimental-alias-exception/index.d.ts b/eslint-rules/fixtures/config-names-sync/iast-experimental-alias-exception/index.d.ts new file mode 100644 index 00000000000..03142772287 --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/iast-experimental-alias-exception/index.d.ts @@ -0,0 +1,15 @@ +declare namespace tracer { + export interface TracerOptions { + iast?: boolean | { + /** + * @env DD_IAST_ENABLED + */ + enabled?: boolean + + /** + * @env DD_IAST_SECURITY_CONTROLS_CONFIGURATION + */ + securityControlsConfiguration?: string + } + } +} diff --git a/eslint-rules/fixtures/config-names-sync/iast-experimental-alias-exception/supported-configurations.json b/eslint-rules/fixtures/config-names-sync/iast-experimental-alias-exception/supported-configurations.json new file mode 100644 index 00000000000..f6584030194 --- /dev/null +++ b/eslint-rules/fixtures/config-names-sync/iast-experimental-alias-exception/supported-configurations.json @@ -0,0 +1,22 @@ +{ + "supportedConfigurations": { + "DD_IAST_ENABLED": [ + { + "configurationNames": [ + "iast.enabled", + "iast", + "experimental.iast.enabled", + "experimental.iast" + ] + } + ], + "DD_IAST_SECURITY_CONTROLS_CONFIGURATION": [ + { + "configurationNames": [ + "iast.securityControlsConfiguration", + "experimental.iast.securityControlsConfiguration" + ] + } + ] + } +} diff --git a/eslint-rules/fixtures/config-names-sync/valid/index.d.ts b/eslint-rules/fixtures/config-names-sync/valid/index.d.ts index 1a3b57c3edf..2910219606f 100644 --- a/eslint-rules/fixtures/config-names-sync/valid/index.d.ts +++ b/eslint-rules/fixtures/config-names-sync/valid/index.d.ts @@ -21,15 +21,6 @@ declare namespace tracer { experimental?: { appsec?: boolean | TracerOptions['appsec'] - - iast?: boolean | IastOptions } } - - interface IastOptions { - /** - * @env DD_IAST_ENABLED - */ - enabled?: boolean - } } diff --git a/eslint-rules/fixtures/config-names-sync/valid/supported-configurations.json b/eslint-rules/fixtures/config-names-sync/valid/supported-configurations.json index 17238484371..1d5b78e9b96 100644 --- a/eslint-rules/fixtures/config-names-sync/valid/supported-configurations.json +++ b/eslint-rules/fixtures/config-names-sync/valid/supported-configurations.json @@ -23,14 +23,6 @@ "experimental.appsec" ] } - ], - "DD_IAST_ENABLED": [ - { - "configurationNames": [ - "experimental.iast.enabled", - "experimental.iast" - ] - } ] } } diff --git a/eslint.config.mjs b/eslint.config.mjs index c8e678cfbb0..002fbaf6951 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -21,6 +21,7 @@ import eslintLogPrintfStyle from './eslint-rules/eslint-log-printf-style.mjs' import eslintProcessEnv from './eslint-rules/eslint-process-env.mjs' import eslintRequireExportExists from './eslint-rules/eslint-require-export-exists.mjs' import eslintSafeTypeOfObject from './eslint-rules/eslint-safe-typeof-object.mjs' +import eslintTimerUnref from './eslint-rules/eslint-timer-unref.mjs' const { dependencies } = JSON.parse(readFileSync('./vendor/package.json', 'utf8')) @@ -99,8 +100,6 @@ export default [ 'integration-tests/esbuild/out.js', // Generated 'integration-tests/esbuild/aws-sdk-out.js', // Generated 'packages/datadog-plugin-graphql/src/tools/index.js', // Inlined from apollo-graphql - 'packages/datadog-plugin-graphql/src/tools/signature.js', // Inlined from apollo-graphql - 'packages/datadog-plugin-graphql/src/tools/transforms.js', // Inlined from apollo-graphql ], }, eslintPluginJs.configs.recommended, @@ -276,8 +275,7 @@ export default [ 'jsdoc/check-param-names': ['error', { disableMissingParamChecks: true }], 'jsdoc/check-tag-names': ['error', { definedTags: ['datadog'] }], // TODO: Enable the rules that we want to use. - // no-defaults: This should be activated, since the defaults will not be picked up in a description. - 'jsdoc/no-defaults': 'off', + 'jsdoc/no-defaults': 'error', 'jsdoc/no-undefined-types': 'off', 'jsdoc/reject-function-type': 'off', 'jsdoc/require-jsdoc': 'off', @@ -384,6 +382,7 @@ export default [ 'eslint-safe-typeof-object': eslintSafeTypeOfObject, 'eslint-log-printf-style': eslintLogPrintfStyle, 'eslint-require-export-exists': eslintRequireExportExists, + 'eslint-timer-unref': eslintTimerUnref, }, }, import: eslintPluginImport, @@ -502,6 +501,7 @@ export default [ 'eslint-rules/eslint-process-env': 'error', 'eslint-rules/eslint-env-aliases': 'error', 'eslint-rules/eslint-log-printf-style': 'error', + 'eslint-rules/eslint-timer-unref': 'error', 'no-restricted-syntax': ['error', { // Inline `.evaluate()` callbacks (Playwright/Puppeteer) are serialized with @@ -579,7 +579,9 @@ export default [ 'eslint.config.mjs', ], rules: { - 'eslint-rules/eslint-config-names-sync': 'error', + 'eslint-rules/eslint-config-names-sync': ['error', { + indexDtsPaths: ['index.d.ts', 'index.d.v5.ts'], + }], }, }, { diff --git a/ext/exporters.js b/ext/exporters.js index d5080ac3680..7ace24e7e1a 100644 --- a/ext/exporters.js +++ b/ext/exporters.js @@ -10,4 +10,5 @@ module.exports = { MOCHA_WORKER: 'mocha_worker', PLAYWRIGHT_WORKER: 'playwright_worker', VITEST_WORKER: 'vitest_worker', + ELECTRON: 'electron', } diff --git a/index.d.ts b/index.d.ts index 76d0df0c0c7..89696a2a281 100644 --- a/index.d.ts +++ b/index.d.ts @@ -245,6 +245,7 @@ interface Plugins { "cypress": tracer.plugins.cypress; "dns": tracer.plugins.dns; "elasticsearch": tracer.plugins.elasticsearch; + "electron": tracer.plugins.electron; "express": tracer.plugins.express; "fastify": tracer.plugins.fastify; "fetch": tracer.plugins.fetch; @@ -699,28 +700,6 @@ declare namespace tracer { */ protocolVersion?: string - /** - * Deprecated in favor of the global versions of the variables provided under this option - * - * @deprecated - * @hidden - */ - ingestion?: { - /** - * Controls the ingestion sample rate (between 0 and 1) between the agent and the backend. - * @env DD_TRACE_SAMPLE_RATE - * Programmatic configuration takes precedence over the environment variables listed above. - */ - sampleRate?: number - - /** - * Controls the ingestion rate limit between the agent and the backend. Defaults to deferring the decision to the agent. - * @env DD_TRACE_RATE_LIMIT - * Programmatic configuration takes precedence over the environment variables listed above. - */ - rateLimit?: number - }; - /** * Whether to enable inferred proxy services. * @default false @@ -743,19 +722,12 @@ declare namespace tracer { */ experimental?: { - /** - * @default false - * @env DD_TRACE_EXPERIMENTAL_B3_ENABLED - * Programmatic configuration takes precedence over the environment variables listed above. - */ - b3?: boolean - /** * Whether to write traces to log output or agentless, rather than send to an agent * @env DD_TRACE_EXPERIMENTAL_EXPORTER * Programmatic configuration takes precedence over the environment variables listed above. */ - exporter?: 'log' | 'agent' | 'datadog' + exporter?: 'log' | 'agent' | 'datadog' | 'electron' /** * Whether to enable the experimental `getRumData` method. @@ -765,34 +737,6 @@ declare namespace tracer { */ enableGetRumData?: boolean - /** - * Configuration of the IAST. Can be a boolean as an alias to `iast.enabled`. - * @deprecated Please use the non-experimental `iast` option instead. - */ - iast?: boolean | IastOptions - - /** - * Configuration of the AppSec. Can be a boolean as an alias to `appsec.enabled`. - * @deprecated Please use the non-experimental `appsec` option instead. - */ - appsec?: boolean | { - /** - * Configuration of Standalone ASM mode - * Deprecated in favor of `apmTracingEnabled`. - * - * @deprecated Please use `apmTracingEnabled` instead. - */ - standalone?: { - /** - * Whether to enable Standalone ASM. - * @default false - * @env DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED - * Programmatic configuration takes precedence over the environment variables listed above. - */ - enabled?: boolean - } - } | TracerOptions['appsec'], - aiguard?: { /** * Set to `true` to enable the SDK. @@ -1070,15 +1014,6 @@ declare namespace tracer { * Programmatic configuration takes precedence over the environment variables listed above. */ enabled?: boolean, - - /** Whether to enable request body collection on RASP event - * @default false - * - * @deprecated Use UI and Remote Configuration to enable extended data collection - * @env DD_APPSEC_RASP_COLLECT_REQUEST_BODY - * Programmatic configuration takes precedence over the environment variables listed above. - */ - bodyCollection?: boolean }, /** * Configuration for stack trace reporting @@ -1105,39 +1040,6 @@ declare namespace tracer { */ maxDepth?: number, }, - /** - * Configuration for extended headers collection tied to security events - * - * @deprecated Use UI and Remote Configuration to enable extended data collection - */ - extendedHeadersCollection?: { - /** Whether to enable extended headers collection - * @default false - * - * @deprecated Use UI and Remote Configuration to enable extended data collection - * @env DD_APPSEC_COLLECT_ALL_HEADERS - * Programmatic configuration takes precedence over the environment variables listed above. - */ - enabled: boolean, - - /** Whether to redact collected headers - * @default true - * - * @deprecated Use UI and Remote Configuration to enable extended data collection - * @env DD_APPSEC_HEADER_COLLECTION_REDACTION_ENABLED - * Programmatic configuration takes precedence over the environment variables listed above. - */ - redaction: boolean, - - /** Specifies the maximum number of headers collected. - * @default 50 - * - * @deprecated Use UI and Remote Configuration to enable extended data collection - * @env DD_APPSEC_MAX_COLLECTED_HEADERS - * Programmatic configuration takes precedence over the environment variables listed above. - */ - maxHeaders: number, - } } /** @@ -1324,7 +1226,7 @@ declare namespace tracer { /** * Whether to use Datadog legacy baggage extraction and injection behavior. - * @default false + * @default true * @env DD_TRACE_LEGACY_BAGGAGE_ENABLED * Programmatic configuration takes precedence over the environment variables listed above. */ @@ -2024,14 +1926,6 @@ declare namespace tracer { */ allowlist?: string | RegExp | ((urlOrPath: string) => boolean) | (string | RegExp | ((urlOrPath: string) => boolean))[]; - /** - * Deprecated in favor of `allowlist`. - * - * @deprecated - * @hidden - */ - whitelist?: string | RegExp | ((urlOrPath: string) => boolean) | (string | RegExp | ((urlOrPath: string) => boolean))[]; - /** * List of URLs/paths that should not be instrumented. Takes precedence over * allowlist if a URL matches an entry in both. @@ -2044,14 +1938,6 @@ declare namespace tracer { */ blocklist?: string | RegExp | ((urlOrPath: string) => boolean) | (string | RegExp | ((urlOrPath: string) => boolean))[]; - /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden - */ - blacklist?: string | RegExp | ((urlOrPath: string) => boolean) | (string | RegExp | ((urlOrPath: string) => boolean))[]; - /** * Custom filter function used to decide whether a URL/path is allowed. * When provided, this takes precedence over allowlist/blocklist configuration. @@ -2457,6 +2343,26 @@ declare namespace tracer { }; } + /** + * This plugin automatically instruments the + * [electron](https://github.com/electron/electron) module. + */ + interface electron extends Instrumentation { + /** + * Whether to enable instrumentation of ipc spans + * + * @default true + */ + ipc?: boolean; + + /** + * Whether to enable instrumentation of net spans + * + * @default true + */ + net?: boolean; + } + /** * This plugin automatically instruments the * [express](http://expressjs.com/) module. @@ -2697,14 +2603,6 @@ declare namespace tracer { */ allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * Deprecated in favor of `allowlist`. - * - * @deprecated - * @hidden - */ - whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** * List of commands that should not be instrumented. Takes precedence over * allowlist if a command matches an entry in both. Commands must be in @@ -2714,14 +2612,6 @@ declare namespace tracer { */ blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden - */ - blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** * Custom filter function used to decide whether a Redis command should be instrumented. * When provided, this takes precedence over allowlist/blocklist configuration. @@ -2754,14 +2644,6 @@ declare namespace tracer { */ allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * Deprecated in favor of `allowlist`. - * - * @deprecated - * @hidden - */ - whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** * List of commands that should not be instrumented. Takes precedence over * allowlist if a command matches an entry in both. Commands must be in @@ -2771,14 +2653,6 @@ declare namespace tracer { */ blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden - */ - blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** * Custom filter function used to decide whether a Valkey command should be instrumented. * When provided, this takes precedence over allowlist/blocklist configuration. @@ -3053,14 +2927,6 @@ declare namespace tracer { */ allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * Deprecated in favor of `allowlist`. - * - * deprecated - * @hidden - */ - whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** * List of commands that should not be instrumented. Takes precedence over * allowlist if a command matches an entry in both. @@ -3069,14 +2935,6 @@ declare namespace tracer { */ blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** - * Deprecated in favor of `blocklist`. - * - * @deprecated - * @hidden - */ - blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[]; - /** * Custom filter function used to decide whether a Redis command should be instrumented. * When provided, this takes precedence over allowlist/blocklist configuration. @@ -3558,16 +3416,6 @@ declare namespace tracer { */ redactionValuePattern?: string, - /** - * Allows to enable security controls. This option is not supported when - * using ESM. - * @deprecated Please use the DD_IAST_SECURITY_CONTROLS_CONFIGURATION - * environment variable instead. This option will be removed in the next major version. - * @env DD_IAST_SECURITY_CONTROLS_CONFIGURATION - * Programmatic configuration takes precedence over the environment variables listed above. - */ - securityControlsConfiguration?: string, - /** * Specifies the verbosity of the sent telemetry. Default 'INFORMATION' * @env DD_IAST_TELEMETRY_VERBOSITY diff --git a/integration-tests/aiguard/index.spec.js b/integration-tests/aiguard/index.spec.js index 9f4a74fcf37..718fa3ab724 100644 --- a/integration-tests/aiguard/index.spec.js +++ b/integration-tests/aiguard/index.spec.js @@ -17,6 +17,16 @@ function assertHasGuardSpan (payload, predicate) { assert.notStrictEqual(matching, undefined) } +function findMetric (series, metricName) { + return series.find(s => s.metric === metricName) +} + +function assertHasTags (metric, expectedTags) { + for (const tag of expectedTags) { + assert.ok(metric.tags.includes(tag), `Expected tag "${tag}" in [${metric.tags}]`) + } +} + describe('AIGuard SDK integration tests', () => { let cwd, appFile, agent, proc, api, url @@ -32,22 +42,25 @@ describe('AIGuard SDK integration tests', () => { await api.close() }) + const baseEnv = () => ({ + DD_SERVICE: 'ai_guard_integration_test', + DD_ENV: 'test', + DD_TRACE_ENABLED: 'true', + DD_TRACE_CLIENT_IP_ENABLED: 'false', + DD_TRACE_AGENT_PORT: String(agent.port), + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + DD_AI_GUARD_ENABLED: 'true', + DD_AI_GUARD_BLOCK: 'true', + DD_AI_GUARD_ENDPOINT: `http://localhost:${api.address().port}`, + DD_API_KEY: 'DD_API_KEY', + DD_APP_KEY: 'DD_APP_KEY', + }) + beforeEach(async () => { agent = await new FakeAgent().start() proc = await spawnProc(appFile, { cwd, - env: { - DD_SERVICE: 'ai_guard_integration_test', - DD_ENV: 'test', - DD_TRACE_ENABLED: 'true', - DD_TRACE_CLIENT_IP_ENABLED: 'false', - DD_TRACE_AGENT_PORT: String(agent.port), - DD_AI_GUARD_ENABLED: 'true', - DD_AI_GUARD_BLOCK: 'true', - DD_AI_GUARD_ENDPOINT: `http://localhost:${api.address().port}`, - DD_API_KEY: 'DD_API_KEY', - DD_APP_KEY: 'DD_APP_KEY', - }, + env: baseEnv(), }) url = `${proc.url}` }) @@ -189,4 +202,111 @@ describe('AIGuard SDK integration tests', () => { }) }) } + + describe('telemetry metrics', () => { + it('reports requests metric with sdk source on direct SDK call', async () => { + await executeRequest(`${url}/allow`, 'GET') + + const checkTelemetry = agent.assertTelemetryReceived({ + fn: ({ payload }) => { + const series = payload.payload.series + const requests = findMetric(series, 'requests') + + assert.ok(requests) + assert.strictEqual(requests.type, 'count') + assertHasTags(requests, ['source:sdk', 'integration:none', 'action:allow', 'error:false']) + }, + requestType: 'generate-metrics', + timeout: 30_000, + resolveAtFirstSuccess: true, + namespace: 'ai_guard', + }) + + await checkTelemetry + }) + + it('reports requests metric with auto source on auto-instrumented call', async () => { + await executeRequest(`${url}/auto?mode=point1&deny=false`) + + const checkTelemetry = agent.assertTelemetryReceived({ + fn: ({ payload }) => { + const series = payload.payload.series + const requests = findMetric(series, 'requests') + + assert.ok(requests) + assert.strictEqual(requests.type, 'count') + assertHasTags(requests, ['source:auto', 'integration:ai', 'action:allow', 'error:false']) + }, + requestType: 'generate-metrics', + timeout: 30_000, + resolveAtFirstSuccess: true, + namespace: 'ai_guard', + }) + + await checkTelemetry + }) + + it('reports requests metric with block tag on blocked evaluation', async () => { + await executeRequest(`${url}/deny`, 'GET', { 'x-blocking-enabled': 'true' }) + + const checkTelemetry = agent.assertTelemetryReceived({ + fn: ({ payload }) => { + const series = payload.payload.series + const requests = findMetric(series, 'requests') + + assert.ok(requests) + assert.strictEqual(requests.type, 'count') + assertHasTags(requests, ['source:sdk', 'integration:none', 'action:deny', 'error:false', 'block:true']) + }, + requestType: 'generate-metrics', + timeout: 30_000, + resolveAtFirstSuccess: true, + namespace: 'ai_guard', + }) + + await checkTelemetry + }) + + it('reports error metric on API failure', async () => { + const agent2 = await new FakeAgent().start() + const proc2 = await spawnProc(appFile, { + cwd, + env: { + ...baseEnv(), + DD_TRACE_AGENT_PORT: String(agent2.port), + DD_AI_GUARD_ENDPOINT: 'http://localhost:1', + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + }, + }) + + try { + // This will fail because the endpoint is unreachable + await executeRequest(`${proc2.url}/allow`, 'GET').catch(() => {}) + + const checkTelemetry = agent2.assertTelemetryReceived({ + fn: ({ payload }) => { + const series = payload.payload.series + const errorMetric = findMetric(series, 'error') + + assert.ok(errorMetric) + assert.strictEqual(errorMetric.type, 'count') + assertHasTags(errorMetric, ['type:client_error', 'source:sdk', 'integration:none']) + + const requests = findMetric(series, 'requests') + assert.ok(requests) + assertHasTags(requests, ['error:true', 'source:sdk', 'integration:none']) + }, + requestType: 'generate-metrics', + timeout: 30_000, + resolveAtFirstSuccess: true, + namespace: 'ai_guard', + }) + + await checkTelemetry + } finally { + await stopProc(proc2) + await agent2.stop() + } + }) + }) }) diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index 07f19ce3b84..52802ec1317 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -55,6 +55,7 @@ let waitingTime = 0 let knownTestsPageIndex = 0 let testManagementResponse = DEFAULT_TEST_MANAGEMENT_TESTS let testManagementResponseStatusCode = DEFAULT_TEST_MANAGEMENT_TESTS_RESPONSE_STATUS +let skippableSuitesResponseStatusCode = 200 class FakeCiVisIntake extends FakeAgent { setKnownTestsResponseCode (statusCode) { @@ -101,6 +102,10 @@ class FakeCiVisIntake extends FakeAgent { testManagementResponseStatusCode = newStatusCode } + setSkippableSuitesResponseCode (statusCode) { + skippableSuitesResponseStatusCode = statusCode + } + async start () { const app = express() app.use(bodyParser.raw({ limit: Infinity, type: 'application/msgpack' })) @@ -230,7 +235,11 @@ class FakeCiVisIntake extends FakeAgent { '/api/v2/ci/tests/skippable', '/evp_proxy/:version/api/v2/ci/tests/skippable', ], (req, res) => { - res.status(200).send(JSON.stringify({ + if (skippableSuitesResponseStatusCode < 200 || skippableSuitesResponseStatusCode >= 300) { + res.status(skippableSuitesResponseStatusCode).send(JSON.stringify({ errors: ['error'] })) + return + } + res.status(skippableSuitesResponseStatusCode).send(JSON.stringify({ data: suitesToSkip, meta: { correlation_id: correlationId, @@ -351,6 +360,7 @@ class FakeCiVisIntake extends FakeAgent { infoResponse = DEFAULT_INFO_RESPONSE testManagementResponseStatusCode = DEFAULT_TEST_MANAGEMENT_TESTS_RESPONSE_STATUS testManagementResponse = DEFAULT_TEST_MANAGEMENT_TESTS + skippableSuitesResponseStatusCode = 200 this.removeAllListeners() if (this.waitingTimeoutId) { clearTimeout(this.waitingTimeoutId) @@ -359,6 +369,92 @@ class FakeCiVisIntake extends FakeAgent { return super.stop() } + // Gather payloads while childProcess runs, then run onPayload once on the + // accumulated buffer after the child emits `'exit'` plus `gracePeriod` ms of HTTP + // drain. `hardTimeout` is a backstop for a genuinely hung child — bump it per-call + // only when a workload's child runtime is provably above the default. + /** + * @param {import('child_process').ChildProcess | NodeJS.EventEmitter} childProcess + * Source of the `'exit'` event. `exitCode` / `signalCode` are read synchronously + * so a child that has already exited is handled correctly. + * @param {(message: object) => boolean} [payloadMatch] Per-message filter; falsy + * accepts everything. + * @param {(payloads: object[]) => void} onPayload Assertion callback, invoked once + * on the post-exit buffer. Callers can read `childProcess.exitCode` immediately + * after the returned promise resolves. + * @param {{ gracePeriod?: number, hardTimeout?: number }} [options] + */ + gatherPayloadsUntilChildExit (childProcess, payloadMatch, onPayload, options = {}) { + const { gracePeriod = 1000, hardTimeout = 30_000 } = options + const payloads = [] + + return new Promise((resolve, reject) => { + let settled = false + let graceTimer = null + + const cleanup = () => { + settled = true + this.off('message', messageHandler) + childProcess.off('exit', exitHandler) + clearTimeout(hardTimer) + clearTimeout(graceTimer) + } + + const messageHandler = (message) => { + if (settled) return + if (!payloadMatch || payloadMatch(message)) { + payloads.push(message) + } + } + + const exitHandler = () => { + if (settled) return + // Hung-child backstop is moot once the child has exited; only the + // grace period applies from here. Without this, a child exiting + // close to `hardTimeout` races the grace timer and rejects with a + // wrong "child still running" message instead of running the + // assertion. + clearTimeout(hardTimer) + graceTimer = setTimeout(() => { + if (settled) return + if (payloads.length === 0) { + cleanup() + reject(new Error( + 'gatherPayloadsUntilChildExit: child exited with no matching payloads ' + + `(after ${gracePeriod}ms grace period)` + )) + return + } + try { + onPayload(payloads) + cleanup() + resolve() + } catch (error) { + cleanup() + reject(error) + } + }, gracePeriod) + } + + const hardTimer = setTimeout(() => { + if (settled) return + cleanup() + reject(new Error( + `gatherPayloadsUntilChildExit: hard timeout of ${hardTimeout}ms expired (child still running)` + )) + }, hardTimeout) + + this.on('message', messageHandler) + + // Child may already have exited (very fast spawn-and-die). + if (childProcess.exitCode !== null || childProcess.signalCode != null) { + queueMicrotask(exitHandler) + } else { + childProcess.once('exit', exitHandler) + } + }) + } + // Similar to gatherPayloads but resolves if enough payloads have been gathered // to make the assertions pass. It times out after maxGatheringTime so it should // always be faster or as fast as gatherPayloads diff --git a/integration-tests/ci-visibility-intake.spec.js b/integration-tests/ci-visibility-intake.spec.js new file mode 100644 index 00000000000..91f48cbbcfe --- /dev/null +++ b/integration-tests/ci-visibility-intake.spec.js @@ -0,0 +1,166 @@ +'use strict' + +const assert = require('node:assert/strict') +const { EventEmitter } = require('node:events') + +const { FakeCiVisIntake } = require('./ci-visibility-intake') + +function fakeChildProcess () { + const child = new EventEmitter() + child.exitCode = null + child.signalCode = null + child.simulateExit = (code = 0) => { + child.exitCode = code + child.emit('exit', code, null) + } + return child +} + +describe('FakeCiVisIntake.gatherPayloadsUntilChildExit', () => { + let intake + + beforeEach(async () => { + intake = await new FakeCiVisIntake().start() + }) + + afterEach(() => intake.stop()) + + it('runs the assertion once after child exit + grace and resolves on success', async () => { + const child = fakeChildProcess() + const promise = intake.gatherPayloadsUntilChildExit( + child, + ({ url }) => url === '/api/v2/citestcycle', + (payloads) => { + assert.strictEqual(payloads.length, 3) + }, + { gracePeriod: 50, hardTimeout: 5000 } + ) + + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 1 } }) + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 2 } }) + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 3 } }) + child.simulateExit(0) + + await promise + }) + + it('does not resolve before child exit, even when the buffer would satisfy the assertion', async () => { + const child = fakeChildProcess() + let assertionRuns = 0 + const promise = intake.gatherPayloadsUntilChildExit( + child, + ({ url }) => url === '/api/v2/citestcycle', + (payloads) => { + assertionRuns += 1 + assert.strictEqual(payloads.length, 1) + }, + { gracePeriod: 50, hardTimeout: 5000 } + ) + + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 1 } }) + await new Promise((resolve) => setTimeout(resolve, 100)) + assert.strictEqual(assertionRuns, 0, 'onPayload must not run before child exit') + + child.simulateExit(0) + await promise + assert.strictEqual(assertionRuns, 1, 'onPayload runs exactly once on the post-exit buffer') + }) + + it('picks up payloads that arrive during the grace period after child exit', async () => { + const child = fakeChildProcess() + const promise = intake.gatherPayloadsUntilChildExit( + child, + ({ url }) => url === '/api/v2/citestcycle', + (payloads) => { + assert.strictEqual(payloads.length, 2) + }, + { gracePeriod: 100, hardTimeout: 5000 } + ) + + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 1 } }) + child.simulateExit(0) + setImmediate(() => { + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 2 } }) + }) + + await promise + }) + + it('rejects with a clear error when the child exits without any matching payloads', async () => { + const child = fakeChildProcess() + const promise = intake.gatherPayloadsUntilChildExit( + child, + () => false, + () => assert.fail('onPayload must not run when no payloads matched'), + { gracePeriod: 50, hardTimeout: 5000 } + ) + + child.simulateExit(1) + + await assert.rejects(promise, /child exited with no matching payloads/) + }) + + it('rejects with the caller assertion error when the post-exit buffer fails the assertion', async () => { + const child = fakeChildProcess() + const promise = intake.gatherPayloadsUntilChildExit( + child, + ({ url }) => url === '/api/v2/citestcycle', + (payloads) => { + assert.strictEqual(payloads.length, 5, 'expected five payloads') + }, + { gracePeriod: 50, hardTimeout: 5000 } + ) + + intake.emit('message', { url: '/api/v2/citestcycle', payload: { i: 1 } }) + child.simulateExit(0) + + await assert.rejects(promise, /expected five payloads/) + }) + + it('rejects with a hard-timeout error when the child neither exits nor produces payloads', async () => { + const child = fakeChildProcess() + const promise = intake.gatherPayloadsUntilChildExit( + child, + () => false, + () => {}, + { gracePeriod: 50, hardTimeout: 100 } + ) + + await assert.rejects(promise, /hard timeout of 100ms/) + }) + + it('does not surface a hard-timeout error when the child exits just before the backstop', async () => { + const child = fakeChildProcess() + const promise = intake.gatherPayloadsUntilChildExit( + child, + ({ url }) => url === '/api/v2/citestcycle', + (payloads) => { + assert.strictEqual(payloads.length, 1) + }, + { gracePeriod: 50, hardTimeout: 100 } + ) + + setTimeout(() => { + intake.emit('message', { url: '/api/v2/citestcycle', payload: {} }) + child.simulateExit(0) + }, 80) + + await promise + }) + + it('treats a child that already exited as exit + grace', async () => { + const child = fakeChildProcess() + child.exitCode = 0 + + intake.emit('message', { url: '/api/v2/citestcycle', payload: {} }) + + const promise = intake.gatherPayloadsUntilChildExit( + child, + ({ url }) => url === '/api/v2/citestcycle', + () => assert.fail('pre-subscribe payloads must not be captured'), + { gracePeriod: 50, hardTimeout: 5000 } + ) + + await assert.rejects(promise, /child exited with no matching payloads/) + }) +}) diff --git a/integration-tests/ci-visibility/automatic-log-submission-playwright/automatic-log-submission-test.js b/integration-tests/ci-visibility/automatic-log-submission-playwright/automatic-log-submission-test.js index 912f124b872..864ce722ca6 100644 --- a/integration-tests/ci-visibility/automatic-log-submission-playwright/automatic-log-submission-test.js +++ b/integration-tests/ci-visibility/automatic-log-submission-playwright/automatic-log-submission-test.js @@ -10,7 +10,7 @@ test.beforeEach(async ({ page }) => { test.describe('playwright', () => { test('should be able to log to the console', async ({ page }) => { - test.step('log to the console', async () => { + await test.step('log to the console', async () => { logger.log('info', 'Hello simple log!') }) diff --git a/integration-tests/ci-visibility/git-cache.spec.js b/integration-tests/ci-visibility/git-cache.spec.js index aae5134f8ca..91a7b0808bc 100644 --- a/integration-tests/ci-visibility/git-cache.spec.js +++ b/integration-tests/ci-visibility/git-cache.spec.js @@ -45,8 +45,11 @@ describe('git-cache integration tests', () => { beforeEach(() => { process.env.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED = 'true' process.env.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR = cacheDir - // We need this, otherwise the file is already loaded and the cache is disabled + // git-cache reads its config from the dd-trace config singleton on first + // use; clearing both modules from the require cache makes the next + // `require(...)` rebuild the singleton from the env we just set. delete require.cache[require.resolve('../../packages/dd-trace/src/plugins/util/git-cache')] + delete require.cache[require.resolve('../../packages/dd-trace/src/config')] gitCache = require('../../packages/dd-trace/src/plugins/util/git-cache') originalPath = process.env.PATH @@ -148,6 +151,7 @@ describe('git-cache integration tests', () => { delete process.env.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED delete require.cache[require.resolve('../../packages/dd-trace/src/plugins/util/git-cache')] + delete require.cache[require.resolve('../../packages/dd-trace/src/config')] const disabledGitCache = require('../../packages/dd-trace/src/plugins/util/git-cache') const firstResult = disabledGitCache.cachedExec('git', GET_COMMIT_MESSAGE_COMMAND_ARGS) @@ -172,11 +176,39 @@ describe('git-cache integration tests', () => { assert.match(secondError.message, /git/) }) + it('should cache when DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED is true and cache dir is unset', function () { + delete process.env.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR + + delete require.cache[require.resolve('../../packages/dd-trace/src/plugins/util/git-cache')] + const defaultDirGitCache = require('../../packages/dd-trace/src/plugins/util/git-cache') + + const firstResultStr = String(defaultDirGitCache.cachedExec('git', GET_COMMIT_MESSAGE_COMMAND_ARGS)).trim() + assert.strictEqual(firstResultStr, FIXED_COMMIT_MESSAGE) + + const cacheKey = defaultDirGitCache.getCacheKey('git', GET_COMMIT_MESSAGE_COMMAND_ARGS) + const cacheFilePath = defaultDirGitCache.getCacheFilePath(cacheKey) + assert.ok(cacheFilePath.includes('dd-trace-git-cache')) + assert.strictEqual(fs.existsSync(cacheFilePath), true) + + removeGitFromPath() + + const secondResult = String(defaultDirGitCache.cachedExec('git', GET_COMMIT_MESSAGE_COMMAND_ARGS)).trim() + assert.strictEqual(secondResult, firstResultStr) + + fs.rmSync(cacheFilePath, { force: true }) + try { + fs.rmdirSync(path.dirname(cacheFilePath)) + } catch { + // Ignore, if the directory is not empty + } + }) + context('invalid DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR', () => { function runInvalidCacheTest (invalidCacheDir) { process.env.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR = invalidCacheDir delete require.cache[require.resolve('../../packages/dd-trace/src/plugins/util/git-cache')] + delete require.cache[require.resolve('../../packages/dd-trace/src/config')] const invalidDirGitCache = require('../../packages/dd-trace/src/plugins/util/git-cache') const firstResult = invalidDirGitCache.cachedExec('git', GET_COMMIT_MESSAGE_COMMAND_ARGS) diff --git a/integration-tests/ci-visibility/jest-plugin-tests/jest-focus.js b/integration-tests/ci-visibility/jest-plugin-tests/jest-focus.js index 1f5c3cb4c83..a214ed32d95 100644 --- a/integration-tests/ci-visibility/jest-plugin-tests/jest-focus.js +++ b/integration-tests/ci-visibility/jest-plugin-tests/jest-focus.js @@ -6,7 +6,8 @@ describe('jest-test-focused', () => { it('will be skipped', () => { assert.strictEqual(true, true) }) - // eslint-disable-next-line + + // eslint-disable-next-line mocha/no-exclusive-tests it.only('can do focused test', () => { assert.strictEqual(true, true) }) diff --git a/integration-tests/ci-visibility/jest-stack-on-import/jest-stack-on-import-test.js b/integration-tests/ci-visibility/jest-stack-on-import/jest-stack-on-import-test.js new file mode 100644 index 00000000000..e5a93b98c45 --- /dev/null +++ b/integration-tests/ci-visibility/jest-stack-on-import/jest-stack-on-import-test.js @@ -0,0 +1,9 @@ +'use strict' + +const stack = require('./read-stack') + +describe('stack during module import', () => { + it('can read a default error stack while loading the test module', () => { + expect(stack).toContain('Error: stack from module import') + }) +}) diff --git a/integration-tests/ci-visibility/jest-stack-on-import/read-stack.js b/integration-tests/ci-visibility/jest-stack-on-import/read-stack.js new file mode 100644 index 00000000000..7371c9ef7ba --- /dev/null +++ b/integration-tests/ci-visibility/jest-stack-on-import/read-stack.js @@ -0,0 +1,9 @@ +'use strict' + +const stack = new Error('stack from module import').stack + +if (!stack.includes('stack from module import')) { + throw new Error('Expected stack to include the original error message') +} + +module.exports = stack diff --git a/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed-failing.js b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed-failing.js new file mode 100644 index 00000000000..c9a96be8322 --- /dev/null +++ b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed-failing.js @@ -0,0 +1,9 @@ +'use strict' + +it('top-level failing test', () => { + throw new Error('intentional failure') +}) + +describe('a passing describe block', () => { + it('passing nested test', () => {}) +}) diff --git a/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed.js b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed.js new file mode 100644 index 00000000000..99de8bebc5a --- /dev/null +++ b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed.js @@ -0,0 +1,7 @@ +'use strict' + +it('top-level passing test', () => {}) + +describe('a describe block', () => { + it('nested passing test', () => {}) +}) diff --git a/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed2.js b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed2.js new file mode 100644 index 00000000000..a5c83a28978 --- /dev/null +++ b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-mixed2.js @@ -0,0 +1,7 @@ +'use strict' + +it('top-level passing test 2', () => {}) + +describe('a describe block 2', () => { + it('nested passing test 2', () => {}) +}) diff --git a/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-no-describe.js b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-no-describe.js new file mode 100644 index 00000000000..23ed89a3c52 --- /dev/null +++ b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-no-describe.js @@ -0,0 +1,11 @@ +'use strict' + +// Deliberately no describe() wrapper. +// Tests are direct children of mocha's root suite. +// This is the structure that exposes the suite-not-reported bug: +// the 'suite' event handler in mocha/main.js only runs testSuiteStartCh +// for non-root suites with suite.tests.length > 0. When there are no +// non-root suites at all, no suite context is ever created and +// no test_suite_end event is emitted. +it('top-level passing test', () => {}) +it('top-level passing test two', () => {}) diff --git a/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-no-describe2.js b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-no-describe2.js new file mode 100644 index 00000000000..fed0bca5cf5 --- /dev/null +++ b/integration-tests/ci-visibility/mocha-plugin-tests/top-level-it-no-describe2.js @@ -0,0 +1,4 @@ +'use strict' + +it('top-level passing test 2', () => {}) +it('top-level passing test two 2', () => {}) diff --git a/integration-tests/ci-visibility/playwright-efd-duration/efd-duration-test.js b/integration-tests/ci-visibility/playwright-efd-duration/efd-duration-test.js new file mode 100644 index 00000000000..4e9d67649ec --- /dev/null +++ b/integration-tests/ci-visibility/playwright-efd-duration/efd-duration-test.js @@ -0,0 +1,14 @@ +'use strict' + +const { test, expect } = require('@playwright/test') + +test.describe('efd duration retries', () => { + test('instant test', async () => { + expect(1 + 1).toBe(2) + }) + + test('slightly slow test', async () => { + await new Promise(resolve => setTimeout(resolve, 11_000)) + expect(1 + 1).toBe(2) + }) +}) diff --git a/integration-tests/ci-visibility/playwright-efd-projects/project-duration-test.js b/integration-tests/ci-visibility/playwright-efd-projects/project-duration-test.js new file mode 100644 index 00000000000..8d26a09f507 --- /dev/null +++ b/integration-tests/ci-visibility/playwright-efd-projects/project-duration-test.js @@ -0,0 +1,12 @@ +'use strict' + +const { test, expect } = require('@playwright/test') + +test.describe('efd project duration', () => { + test('project scoped test', async ({ browserName }, testInfo) => { + if (browserName && testInfo.project.name === 'second-chromium') { + await new Promise(resolve => setTimeout(resolve, 6_000)) + } + expect(1 + 1).toBe(2) + }) +}) diff --git a/integration-tests/ci-visibility/playwright-efd-repeat-duration/repeat-duration-test.js b/integration-tests/ci-visibility/playwright-efd-repeat-duration/repeat-duration-test.js new file mode 100644 index 00000000000..dfe8d817cd7 --- /dev/null +++ b/integration-tests/ci-visibility/playwright-efd-repeat-duration/repeat-duration-test.js @@ -0,0 +1,13 @@ +'use strict' + +const { test, expect } = require('@playwright/test') + +test.describe('efd repeat duration', () => { + test('repeat-scoped test', async () => { + if (test.info().repeatEachIndex === 0) { + await new Promise(resolve => setTimeout(resolve, 6_000)) + } + + expect(1 + 1).toBe(2) + }) +}) diff --git a/integration-tests/ci-visibility/playwright-efd-repeat/repeat-each-test.js b/integration-tests/ci-visibility/playwright-efd-repeat/repeat-each-test.js new file mode 100644 index 00000000000..3097d051caf --- /dev/null +++ b/integration-tests/ci-visibility/playwright-efd-repeat/repeat-each-test.js @@ -0,0 +1,9 @@ +'use strict' + +const { test, expect } = require('@playwright/test') + +test.describe('efd repeat each', () => { + test('native repeat test', async () => { + expect(1 + 1).toBe(2) + }) +}) 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 5b781fffe04..bdda60fdf87 100644 --- a/integration-tests/ci-visibility/test-optimization-wrong-init.spec.js +++ b/integration-tests/ci-visibility/test-optimization-wrong-init.spec.js @@ -19,7 +19,10 @@ const testFrameworks = [ { testFramework: 'jest', command: 'node ./ci-visibility/test-optimization-wrong-init/run-jest.js', - expectedOutput: 'PASS ci-visibility/test-optimization-wrong-init/sum-wrong-init-test.js', + expectedOutput: [ + 'PASS ci-visibility/test-optimization-wrong-init/sum-wrong-init-test.js', + 'Test Suites:\\s+1 passed, 1 total', + ].join('|'), }, { testFramework: 'vitest', diff --git a/integration-tests/ci-visibility/web-app-server.js b/integration-tests/ci-visibility/web-app-server.js index d09369166d2..07169a0115c 100644 --- a/integration-tests/ci-visibility/web-app-server.js +++ b/integration-tests/ci-visibility/web-app-server.js @@ -1,20 +1,44 @@ 'use strict' -// File to spin an HTTP server that returns an HTML for playwright to visit -const http = require('http') +// File to spin an HTTP server that returns an HTML for browser tests to visit +const http = require('node:http') + const coverage = require('../ci-visibility/fixtures/istanbul-map-fixture.json') -function createWebAppServer () { - const server = http.createServer((req, res) => { - // Close after each response so browser drivers don't reuse a socket the - // server has already FIN'd between requests (keep-alive race → `socket hang up`). - res.setHeader('Content-Type', 'text/html') - res.setHeader('Connection', 'close') - res.writeHead(200) - res.end(` - - - Hello World +const DEFAULT_HOST = '127.0.0.1' +const DEFAULT_BODY = '
Hello World
' + +/** + * @typedef {object} WebAppServerOptions + * @property {string} [body] - HTML body fragment rendered by the test app. + * @property {boolean} [includeCoverage=true] - Whether to expose the Istanbul fixture on window.__coverage__. + * @property {boolean} [includeRum=true] - Whether to expose the fake DD_RUM helper. + * @property {string} [title] - HTML title rendered by the test app. + */ + +/** + * @typedef {object} StartedWebAppServer + * @property {string} baseUrl - Base URL for browser tests. + * @property {number} port - Port used by the server. + * @property {import('node:http').Server} server - Running HTTP server. + */ + +/** + * Builds the static HTML returned by the browser test web app. + * + * @param {WebAppServerOptions} [options] + * @returns {string} + */ +function getWebAppHtml (options = {}) { + const { + body = DEFAULT_BODY, + includeCoverage = true, + includeRum = true, + title = 'Hello World', + } = options + + const rumScript = includeRum + ? ` - -
Hello World
- + ` + : '' + + const coverageScript = includeCoverage + ? ` + ` + : '' + + return ` + + + ${title} + ${rumScript} + + ${body} + + ${coverageScript} - `) + ` +} + +/** + * Creates a stateless HTTP server for browser integration tests. + * + * @param {WebAppServerOptions} [options] + * @returns {import('node:http').Server} + */ +function createWebAppServer (options) { + const html = getWebAppHtml(options) + const server = http.createServer((req, res) => { + // Close after each response so browser drivers don't reuse a socket the + // server has already FIN'd between requests (keep-alive race → `socket hang up`). + res.setHeader('Content-Type', 'text/html') + res.setHeader('Connection', 'close') + res.writeHead(200) + res.end(html) }) server.on('error', (error) => { @@ -49,5 +102,58 @@ function createWebAppServer () { return server } +/** + * Starts a web app server on a loopback-only ephemeral port. + * + * @param {WebAppServerOptions & { host?: string }} [options] + * @returns {Promise} + */ +function startWebAppServer (options = {}) { + const { host = DEFAULT_HOST, ...serverOptions } = options + const server = createWebAppServer(serverOptions) + + return new Promise((resolve, reject) => { + server.once('error', onError) + server.listen(0, host, () => { + server.removeListener('error', onError) + + const address = server.address() + if (!address || typeof address === 'string') { + reject(new Error('Web app server did not bind to a TCP port')) + return + } + + resolve({ + baseUrl: `http://${host}:${address.port}`, + port: address.port, + server, + }) + }) + + function onError (error) { + server.removeListener('error', onError) + reject(error) + } + }) +} + +/** + * Stops a web app server if it is running. + * + * @param {StartedWebAppServer|import('node:http').Server|undefined} webAppServer + * @returns {Promise} + */ +function stopWebAppServer (webAppServer) { + const server = webAppServer?.server || webAppServer + + if (!server?.listening) { + return Promise.resolve() + } + + return new Promise(resolve => server.close(() => resolve())) +} + module.exports = createWebAppServer() module.exports.createWebAppServer = createWebAppServer +module.exports.startWebAppServer = startWebAppServer +module.exports.stopWebAppServer = stopWebAppServer diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index ee8c0346554..572a9d6b321 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -72,7 +72,10 @@ const { TEST_FRAMEWORK_VERSION, CI_APP_ORIGIN, TEST_SKIP_REASON, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, TEST_FINAL_STATUS, } = require('../../packages/dd-trace/src/plugins/util/test') const { SAMPLING_PRIORITY } = require('../../ext/tags') @@ -81,6 +84,13 @@ const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/ const { NODE_MAJOR } = require('../../version') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../packages/dd-trace/src/constants') +function assertItrSkippingEnabledTags (events, expected) { + const testSuite = events.find(event => event.type === 'test_suite_end').content + assert.strictEqual(testSuite.meta[TEST_ITR_SKIPPING_ENABLED], expected) + const test = events.find(event => event.type === 'test').content + assert.strictEqual(test.meta[TEST_ITR_SKIPPING_ENABLED], expected) +} + const version = process.env.CUCUMBER_VERSION || 'latest' const onlyLatestIt = version === 'latest' ? it : it.skip @@ -329,19 +339,108 @@ describe(`cucumber@${version} commonJS`, () => { logsEndpoint = isAgentless ? '/api/v2/logs' : '/debugger/v1/input' }) - it('tags session and children with _dd.ci.library_configuration_error when settings fails 4xx', async () => { - receiver.setSettingsResponseCode(404) - 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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') - const testEvent = events.find(event => event.type === 'test') - assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + context('error tags', () => { + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fails 4xx', + async () => { + receiver.setSettingsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + }) + childProcess = exec(runTestsCommand, { cwd, env: envVars }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.skippable_tests when request fails 4xx', + async () => { + receiver.setSkippableSuitesResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { cwd, env: envVars }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { cwd, env: envVars }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests when request fails', + async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual( + testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual( + testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + }) + childProcess = exec(runTestsCommand, { cwd, env: envVars }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) - childProcess = exec(runTestsCommand, { cwd, env: envVars }) - await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) const runModes = ['serial'] @@ -646,6 +745,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'false') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'false') + assertItrSkippingEnabledTags(payload.events, 'false') }, ({ url }) => url.endsWith('/api/v2/citestcycle')).then(() => done()).catch(done) childProcess = exec( @@ -720,6 +820,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_TYPE], 'suite') assert.strictEqual(testModule.metrics[TEST_ITR_SKIPPING_COUNT], 1) + assertItrSkippingEnabledTags(eventsRequest.payload.events, 'true') done() }).catch(done) @@ -763,6 +864,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assertItrSkippingEnabledTags(payload.events, 'true') }, ({ url }) => url.endsWith('/api/v2/citestcycle')).then(() => done()).catch(done) childProcess = exec( @@ -962,6 +1064,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') assert.strictEqual(testModule.metrics[TEST_ITR_SKIPPING_COUNT], 0) + assertItrSkippingEnabledTags(events, 'true') }, 25000) childProcess = exec( @@ -1130,6 +1233,7 @@ describe(`cucumber@${version} commonJS`, () => { tests.forEach(test => { assert.ok(!test.meta[TEST_SUITE].includes('farewell')) }) + assertItrSkippingEnabledTags(events, 'true') }) childProcess = exec( diff --git a/integration-tests/cypress/cypress-atr.spec.js b/integration-tests/cypress/cypress-atr.spec.js index bd578dc82d6..da0888d14bf 100644 --- a/integration-tests/cypress/cypress-atr.spec.js +++ b/integration-tests/cypress/cypress-atr.spec.js @@ -11,10 +11,11 @@ const { getCiVisAgentlessConfig, getCiVisEvpProxyConfig, assertObjectContains, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') const { TEST_STATUS, TEST_SOURCE_FILE, @@ -30,7 +31,6 @@ const { } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR, NODE_MAJOR } = require('../../version') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion @@ -95,7 +95,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -104,66 +104,23 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined + }) - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + after(async () => { + await stopWebAppServer(webAppServer) }) context('flaky test retries', () => { @@ -259,7 +216,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -314,7 +271,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_CIVISIBILITY_FLAKY_RETRY_ENABLED: 'false', SPEC_PATTERN: specToRun, }, @@ -372,7 +329,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', SPEC_PATTERN: specToRun, }, @@ -423,7 +380,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', SPEC_PATTERN: specToRun, }, @@ -470,7 +427,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, CYPRESS_TEST_ISOLATION: 'false', }, @@ -531,7 +488,7 @@ moduleTypes.forEach(({ cwd: `${cwd}/ci-visibility/subproject`, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, }, } ) @@ -596,7 +553,7 @@ moduleTypes.forEach(({ cwd, env: { ...getCiVisEvpProxyConfig(receiver.port), - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } diff --git a/integration-tests/cypress/cypress-efd.spec.js b/integration-tests/cypress/cypress-efd.spec.js index 3e3601f3aea..64101960248 100644 --- a/integration-tests/cypress/cypress-efd.spec.js +++ b/integration-tests/cypress/cypress-efd.spec.js @@ -10,23 +10,25 @@ const { useSandbox, getCiVisEvpProxyConfig, assertObjectContains, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') const { TEST_STATUS, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, + TEST_EARLY_FLAKE_ABORT_REASON, TEST_RETRY_REASON, TEST_NAME, TEST_HAS_FAILED_ALL_RETRIES, TEST_RETRY_REASON_TYPES, + TEST_FINAL_STATUS, } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR, NODE_MAJOR } = require('../../version') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion @@ -92,7 +94,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -101,66 +103,23 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined + }) - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + after(async () => { + await stopWebAppServer(webAppServer) }) context('early flake detection', () => { @@ -222,7 +181,70 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: specToRun, + }, + } + ) + + await Promise.all([ + once(childProcess, 'exit'), + receiverPromise, + ]) + }) + + it('uses the retry count from the matching slow_test_retries bucket', async () => { + receiver.setSettings({ + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': 2, + '10s': 0, + }, + faulty_session_threshold: 100, + }, + known_tests_enabled: true, + }) + + receiver.setKnownTests({ + cypress: {}, + }) + + const receiverPromise = 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 instantTests = tests.filter(test => test.resource === + 'cypress/e2e/efd-duration.cy.js.efd duration retries instant test' + ) + assert.strictEqual(instantTests.length, 3) + assert.strictEqual( + instantTests.filter(test => test.meta[TEST_RETRY_REASON] === TEST_RETRY_REASON_TYPES.efd).length, + 2 + ) + + const slowTests = tests.filter(test => test.resource === + 'cypress/e2e/efd-duration.cy.js.efd duration retries slightly slow test' + ) + assert.strictEqual(slowTests.length, 1) + assert.strictEqual(slowTests[0].meta[TEST_IS_NEW], 'true') + assert.strictEqual(slowTests[0].meta[TEST_EARLY_FLAKE_ABORT_REASON], 'slow') + assert.strictEqual(slowTests[0].meta[TEST_STATUS], 'pass') + assert.strictEqual(slowTests[0].meta[TEST_FINAL_STATUS], 'pass') + assert.ok(!(TEST_IS_RETRY in slowTests[0].meta)) + }, 30_000) + + const envVars = getCiVisEvpProxyConfig(receiver.port) + const specToRun = 'cypress/e2e/efd-duration.cy.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -280,7 +302,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: 'false', }, @@ -334,7 +356,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/skipped-test.js', }, } @@ -388,7 +410,62 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: specToRun, + }, + } + ) + + await Promise.all([ + once(childProcess, 'exit'), + receiverPromise, + ]) + }) + + it('bails out of EFD if the percentage of new test files is too high', async () => { + receiver.setSettings({ + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': NUM_RETRIES_EFD, + }, + faulty_session_threshold: 0, + }, + known_tests_enabled: true, + }) + + receiver.setKnownTests({ + cypress: {}, + }) + + const envVars = getCiVisEvpProxyConfig(receiver.port) + + const receiverPromise = 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) + assert.strictEqual(tests.length, 2) + + const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') + assert.strictEqual(newTests.length, 0) + + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + assert.strictEqual(retriedTests.length, 0) + + const testSession = events.find(event => event.type === 'test_session_end').content + assert.ok(!(TEST_EARLY_FLAKE_ENABLED in testSession.meta)) + assert.strictEqual(testSession.meta[TEST_EARLY_FLAKE_ABORT_REASON], 'faulty') + }, 60000) + + const specToRun = 'cypress/e2e/spec.cy.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -446,7 +523,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: 'false', }, @@ -505,7 +582,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -571,7 +648,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, CYPRESS_TEST_ISOLATION: 'false', }, @@ -668,7 +745,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } diff --git a/integration-tests/cypress/cypress-final-status.spec.js b/integration-tests/cypress/cypress-final-status.spec.js index 835c88556e1..2daf7274760 100644 --- a/integration-tests/cypress/cypress-final-status.spec.js +++ b/integration-tests/cypress/cypress-final-status.spec.js @@ -8,10 +8,11 @@ const { sandboxCwd, useSandbox, getCiVisEvpProxyConfig, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') const { TEST_STATUS, TEST_IS_NEW, @@ -24,7 +25,6 @@ const { } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR, NODE_MAJOR } = require('../../version') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion @@ -89,7 +89,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -98,66 +98,23 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined + }) - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + after(async () => { + await stopWebAppServer(webAppServer) }) // These tests require Cypress >=10 features (defineConfig, setupNodeEvents) @@ -225,7 +182,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -319,7 +276,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -403,7 +360,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -489,7 +446,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -538,7 +495,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '0', SPEC_PATTERN: specToRun, }, @@ -598,7 +555,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -669,7 +626,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -722,7 +679,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -783,7 +740,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } diff --git a/integration-tests/cypress/cypress-impacted-tests.spec.js b/integration-tests/cypress/cypress-impacted-tests.spec.js index f9edfea4cb7..5ed1586c71b 100644 --- a/integration-tests/cypress/cypress-impacted-tests.spec.js +++ b/integration-tests/cypress/cypress-impacted-tests.spec.js @@ -12,10 +12,11 @@ const { useSandbox, getCiVisEvpProxyConfig, assertObjectContains, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') const { TEST_SOURCE_FILE, TEST_IS_NEW, @@ -37,7 +38,6 @@ const { } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR, NODE_MAJOR } = require('../../version') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion @@ -103,7 +103,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -112,66 +112,23 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined + }) - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + after(async () => { + await stopWebAppServer(webAppServer) }) context('libraries capabilities', () => { @@ -207,7 +164,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_TEST_SESSION_NAME: 'my-test-session-name', SPEC_PATTERN: specToRun, }, @@ -347,7 +304,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, GITHUB_BASE_REF: '', ...extraEnvVars, @@ -463,7 +420,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, GITHUB_BASE_REF: '', CYPRESS_TEST_ISOLATION: 'false', @@ -553,7 +510,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, GITHUB_BASE_REF: '', }, diff --git a/integration-tests/cypress/cypress-itr.spec.js b/integration-tests/cypress/cypress-itr.spec.js index 05c5694e7a7..1471583bf80 100644 --- a/integration-tests/cypress/cypress-itr.spec.js +++ b/integration-tests/cypress/cypress-itr.spec.js @@ -10,10 +10,11 @@ const { getCiVisAgentlessConfig, getCiVisEvpProxyConfig, assertObjectContains, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') const { TEST_STATUS, TEST_CODE_COVERAGE_ENABLED, @@ -27,12 +28,18 @@ const { } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR, NODE_MAJOR } = require('../../version') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion const hookFile = 'dd-trace/loader-hook.mjs' +function assertItrSkippingEnabledTags (events, expected) { + const testSuite = events.find(event => event.type === 'test_suite_end').content + assert.strictEqual(testSuite.meta[TEST_ITR_SKIPPING_ENABLED], expected) + const test = events.find(event => event.type === 'test').content + assert.strictEqual(test.meta[TEST_ITR_SKIPPING_ENABLED], expected) +} + function shouldTestsRun (type) { if (DD_MAJOR === 5) { if (NODE_MAJOR <= 16) { @@ -91,7 +98,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -100,66 +107,23 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined + }) - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + after(async () => { + await stopWebAppServer(webAppServer) }) context('intelligent test runner', () => { @@ -179,7 +143,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, } @@ -224,7 +188,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, } @@ -270,6 +234,7 @@ moduleTypes.forEach(({ assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') assert.strictEqual(testModule.metrics[TEST_ITR_SKIPPING_COUNT], 1) assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_TYPE], 'test') + assertItrSkippingEnabledTags(events, 'true') }, 25000) const coverageRequestPromise = receiver @@ -292,7 +257,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/{other,spec}.cy.js', }, } @@ -347,7 +312,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/other.cy.js', }, } @@ -422,7 +387,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/{other,spec}.cy.js', }, } @@ -491,7 +456,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/{other,spec}.cy.js', }, } @@ -528,6 +493,7 @@ moduleTypes.forEach(({ assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') assert.strictEqual(testModule.metrics[TEST_ITR_SKIPPING_COUNT], 0) + assertItrSkippingEnabledTags(events, 'true') }, 30000) const skippableRequestPromise = receiver @@ -544,7 +510,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, } @@ -581,7 +547,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, } @@ -634,7 +600,7 @@ moduleTypes.forEach(({ cwd: `${cwd}/ci-visibility/subproject`, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, }, } ) @@ -674,7 +640,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, CYPRESS_ENABLE_INCOMPATIBLE_PLUGIN: '1', SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, diff --git a/integration-tests/cypress/cypress-reporting.spec.js b/integration-tests/cypress/cypress-reporting.spec.js index a40c9ba8d40..ace060f39d6 100644 --- a/integration-tests/cypress/cypress-reporting.spec.js +++ b/integration-tests/cypress/cypress-reporting.spec.js @@ -14,10 +14,11 @@ const { getCiVisAgentlessConfig, getCiVisEvpProxyConfig, assertObjectContains, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') const coverageFixture = require('../ci-visibility/fixtures/istanbul-map-fixture.json') const { TEST_STATUS, @@ -35,7 +36,10 @@ const { TEST_LEVEL_EVENT_TYPES, DD_TEST_IS_USER_PROVIDED_SERVICE, TEST_NAME, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE, ERROR_TYPE, COMPONENT } = require('../../packages/dd-trace/src/constants') @@ -45,7 +49,6 @@ const { resolveSourceLineForTest, } = require('../../packages/datadog-plugin-cypress/src/source-map-utils') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion @@ -147,7 +150,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -156,66 +159,23 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined + }) - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + after(async () => { + await stopWebAppServer(webAppServer) }) it('instruments tests with the APM protocol (old agents)', async () => { @@ -295,7 +255,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -321,7 +281,7 @@ moduleTypes.forEach(({ cwd, env: { ...restEnvVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, }, } ) @@ -380,7 +340,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -453,7 +413,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-*.js', }, } @@ -493,7 +453,7 @@ moduleTypes.forEach(({ env: { ...envVars, NODE_OPTIONS: '-r dd-trace/ci/init', - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -540,7 +500,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -586,7 +546,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -633,7 +593,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -685,7 +645,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -736,7 +696,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -856,7 +816,7 @@ moduleTypes.forEach(({ cwd: subprojectDir, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, }, } ) @@ -913,7 +873,7 @@ moduleTypes.forEach(({ // a separate OTEL collector. The Test Optimization exporter // must win inside isCiVisibility mode. OTEL_TRACES_EXPORTER: 'otlp', - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -966,7 +926,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, }) @@ -1056,7 +1016,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -1098,7 +1058,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, CYPRESS_ENABLE_AFTER_RUN_CUSTOM: '1', CYPRESS_ENABLE_AFTER_SPEC_CUSTOM: '1', CYPRESS_ENABLE_MANUAL_PLUGIN: '1', @@ -1152,7 +1112,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/basic-pass.js', }, } @@ -1206,7 +1166,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, CYPRESS_ENABLE_AFTER_RUN_CUSTOM: '1', CYPRESS_ENABLE_AFTER_SPEC_CUSTOM: '1', SPEC_PATTERN: 'cypress/e2e/basic-pass.js', @@ -1282,7 +1242,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/dist/spec-source-line.cy.js', }, }) @@ -1395,7 +1355,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/dist/spec-source-line-fallback.cy.js', }, }) @@ -1443,7 +1403,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/dist/spec-source-line-no-match.cy.js', }, }) @@ -1468,10 +1428,11 @@ moduleTypes.forEach(({ ) assert.ok(jsInvocationDetailsEvent, 'plain-js invocationDetails test event should exist') - assert.strictEqual( - jsInvocationDetailsEvent.content.metrics[TEST_SOURCE_START], - 243, - 'should keep invocationDetails line directly for plain JS specs without source maps' + // The exact value is a Cypress-generated bundle line that shifts when support code changes. + // It should stay as a generated invocationDetails line instead of resolving to the fixture declaration. + assert.ok( + jsInvocationDetailsEvent.content.metrics[TEST_SOURCE_START] > 100, + 'should keep generated invocationDetails line directly for plain JS specs without source maps' ) assert.ok( jsInvocationDetailsEvent.content.meta[TEST_SOURCE_FILE].endsWith('spec-source-line-invocation.cy.js'), @@ -1485,7 +1446,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec-source-line-invocation.cy.js', }, }) @@ -1559,7 +1520,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec-source-line.cy.ts', }, }) @@ -1574,35 +1535,178 @@ moduleTypes.forEach(({ assert.strictEqual(exitCode, 0, 'cypress process should exit successfully') }) - it('tags session and children with _dd.ci.library_configuration_error when settings fails 4xx', async () => { - const envVars = getCiVisAgentlessConfig(receiver.port) + context('error tags', () => { + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fails 4xx', + async () => { + const envVars = getCiVisAgentlessConfig(receiver.port) - receiver.setSettingsResponseCode(404) - 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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true', - 'test_session_end should have _dd.ci.library_configuration_error tag') - const testEvent = events.find(event => event.type === 'test') - assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true', - 'test event should have _dd.ci.library_configuration_error tag (from getSessionRequestErrorTags)') + receiver.setSettingsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true', + 'test_session_end should have _dd.ci.library_configuration_error.settings tag') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true', + 'test_module_end should have _dd.ci.library_configuration_error.settings tag') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true', + 'test_suite_end should have _dd.ci.library_configuration_error.settings tag') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true', + 'test event should have _dd.ci.library_configuration_error.settings tag') + }) + + childProcess = exec( + testCommand, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: 'cypress/e2e/spec.cy.js', + }, + } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) - childProcess = exec( - testCommand, - { - cwd, - env: { - ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, - SPEC_PATTERN: 'cypress/e2e/spec.cy.js', - }, - } - ) + it( + 'tags session and children with _dd.ci.library_configuration_error.skippable_tests when request fails 4xx', + async () => { + const envVars = getCiVisAgentlessConfig(receiver.port) - await Promise.all([eventsPromise, once(childProcess, 'exit')]) + receiver.setSkippableSuitesResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true', + 'test_session_end should have _dd.ci.library_configuration_error.skippable_tests tag') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true', + 'test_module_end should have _dd.ci.library_configuration_error.skippable_tests tag') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true', + 'test_suite_end should have _dd.ci.library_configuration_error.skippable_tests tag') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true', + 'test event should have _dd.ci.library_configuration_error.skippable_tests tag') + }) + + childProcess = exec( + testCommand, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: 'cypress/e2e/spec.cy.js', + }, + } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + const envVars = getCiVisAgentlessConfig(receiver.port) + + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true', + 'test_session_end should have _dd.ci.library_configuration_error.known_tests tag') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true', + 'test_module_end should have _dd.ci.library_configuration_error.known_tests tag') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true', + 'test_suite_end should have _dd.ci.library_configuration_error.known_tests tag') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true', + 'test event should have _dd.ci.library_configuration_error.known_tests tag') + }) + + childProcess = exec( + testCommand, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: 'cypress/e2e/spec.cy.js', + }, + } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests when request fails', + async () => { + const envVars = getCiVisAgentlessConfig(receiver.port) + + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true', + 'test_session_end should have _dd.ci.library_configuration_error.test_management_tests tag') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual( + testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true', + 'test_module_end should have _dd.ci.library_configuration_error.test_management_tests tag' + ) + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true', + 'test_suite_end should have _dd.ci.library_configuration_error.test_management_tests tag' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual( + testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true', + 'test event should have _dd.ci.library_configuration_error.test_management_tests tag' + ) + }) + + childProcess = exec( + testCommand, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: 'cypress/e2e/spec.cy.js', + }, + } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) }) it('does not crash if badly init', async () => { @@ -1625,7 +1729,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_SITE: '= invalid = url', SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, @@ -1814,7 +1918,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_TAGS: 'test.customtag:customvalue,test.customtag2:customvalue2', DD_TEST_SESSION_NAME: 'my-test-session', DD_SERVICE: undefined, @@ -1858,7 +1962,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, } diff --git a/integration-tests/cypress/cypress-test-management.spec.js b/integration-tests/cypress/cypress-test-management.spec.js index b3fe63b1e75..31451e0896b 100644 --- a/integration-tests/cypress/cypress-test-management.spec.js +++ b/integration-tests/cypress/cypress-test-management.spec.js @@ -3,7 +3,6 @@ const assert = require('node:assert/strict') const { exec } = require('node:child_process') const { once } = require('node:events') -const http = require('node:http') const semver = require('semver') const { @@ -11,10 +10,12 @@ const { useSandbox, getCiVisEvpProxyConfig, assertObjectContains, + stopCiVisTestEnv, warmCypressBinary, } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const { createWebAppServer } = require('../ci-visibility/web-app-server') +const { startWebAppServer, stopWebAppServer } = require('../ci-visibility/web-app-server') +const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') const { TEST_STATUS, TEST_IS_NEW, @@ -33,7 +34,6 @@ const { } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR, NODE_MAJOR } = require('../../version') -const RECEIVER_STOP_TIMEOUT = 20000 const requestedVersion = process.env.CYPRESS_VERSION const oldestVersion = DD_MAJOR >= 6 ? '12.0.0' : '6.7.0' const version = requestedVersion === 'oldest' ? oldestVersion : requestedVersion @@ -98,7 +98,7 @@ moduleTypes.forEach(({ } this.timeout(80_000) - let cwd, receiver, childProcess, webAppPort, webAppServer, secondWebAppServer + let cwd, receiver, childProcess, webAppBaseUrl, webAppServer, secondWebAppBaseUrl, secondWebAppServer // cypress-fail-fast is required as an incompatible plugin. // typescript is required to compile .cy.ts spec files in the pre-compiled JS tests. @@ -107,73 +107,35 @@ moduleTypes.forEach(({ before(async function () { cwd = sandboxCwd() await warmCypressBinary(cwd) + + const webApp = await startWebAppServer() + webAppBaseUrl = webApp.baseUrl + webAppServer = webApp.server + + if (version === 'latest') { + const secondWebApp = await startWebAppServer({ + body: '
Hella World
', + includeCoverage: false, + includeRum: false, + title: 'Hella World', + }) + secondWebAppBaseUrl = secondWebApp.baseUrl + secondWebAppServer = secondWebApp.server + } }) after(async () => { - // Cleanup second web app server if it exists - if (secondWebAppServer) { - await new Promise(resolve => secondWebAppServer.close(resolve)) - } + await stopWebAppServer(secondWebAppServer) + await stopWebAppServer(webAppServer) }) beforeEach(async function () { receiver = await new FakeCiVisIntake().start() - - // Create a fresh web server for each test to avoid state issues - webAppServer = createWebAppServer() - await /** @type {Promise} */ (new Promise((resolve, reject) => { - webAppServer.once('error', reject) - webAppServer.listen(0, 'localhost', () => { - webAppPort = webAppServer.address().port - webAppServer.removeListener('error', reject) - resolve() - }) - })) }) - // Cypress child processes can sometimes hang or take longer to - // terminate. This can cause `FakeCiVisIntake#stop` to be delayed - // because there are pending connections. afterEach(async () => { - if (childProcess && childProcess.pid) { - try { - childProcess.kill('SIGKILL') - } catch (error) { - // Process might already be dead - this is fine, ignore error - } - - // Don't wait for exit - Cypress processes can hang indefinitely in uninterruptible I/O - // The OS will clean up zombies, and fresh server per test prevents port conflicts - } - - // Close web server before stopping receiver - if (webAppServer) { - await /** @type {Promise} */ (new Promise((resolve) => { - webAppServer.close((err) => { - if (err) { - // eslint-disable-next-line no-console - console.error('Web server close error:', err) - } - resolve() - }) - })) - } - - // Add timeout to prevent hanging - const stopPromise = receiver.stop() - const timeoutPromise = new Promise((resolve, reject) => - setTimeout(() => reject(new Error('Receiver stop timeout')), RECEIVER_STOP_TIMEOUT) - ) - - try { - await Promise.race([stopPromise, timeoutPromise]) - } catch (error) { - // eslint-disable-next-line no-console - console.warn('Receiver stop timed out:', error.message) - } - - // Small delay to allow OS to release ports - await new Promise(resolve => setTimeout(resolve, 100)) + await stopCiVisTestEnv({ childProcess, receiver }) + childProcess = undefined }) context('known tests without early flake detection', () => { @@ -193,8 +155,24 @@ moduleTypes.forEach(({ const envVars = getCiVisEvpProxyConfig(receiver.port) - const receiverPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const specToRun = 'cypress/e2e/spec.cy.js' + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: specToRun, + DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: 'false', + }, + } + ) + + await receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ 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) assert.strictEqual(tests.length, 2) @@ -208,26 +186,8 @@ moduleTypes.forEach(({ const testSession = events.find(event => event.type === 'test_session_end').content assert.ok(!(TEST_EARLY_FLAKE_ENABLED in testSession.meta)) - }, 25000) - - const specToRun = 'cypress/e2e/spec.cy.js' - childProcess = exec( - version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, - { - cwd, - env: { - ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, - SPEC_PATTERN: specToRun, - DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED: 'false', - }, } ) - - await Promise.all([ - once(childProcess, 'exit'), - receiverPromise, - ]) }) }) @@ -236,34 +196,6 @@ moduleTypes.forEach(({ it('does not crash for multi origin tests', async () => { const envVars = getCiVisEvpProxyConfig(receiver.port) - const receiverPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { - const events = payloads.flatMap(({ payload }) => payload.events) - .filter(event => event.type !== 'span') - assert.strictEqual(events.length, 4) - - const test = events.find(event => event.type === 'test').content - assert.strictEqual(test.resource, 'cypress/e2e/multi-origin.js.tests multiple origins') - assert.strictEqual(test.meta[TEST_STATUS], 'pass') - // cypress@latest esm + multi-origin browser context switching adds cold-start overhead - // the suite-level `warmCypressBinary` (commonJS path) doesn't reach. - }, 50_000) - - secondWebAppServer = http.createServer((req, res) => { - res.setHeader('Content-Type', 'text/html') - res.writeHead(200) - res.end(` - - -
Hella World
- - `) - }) - - const secondWebAppPort = await new Promise(resolve => { - secondWebAppServer.listen(0, 'localhost', () => resolve(secondWebAppServer.address().port)) - }) - const specToRun = 'cypress/e2e/multi-origin.js' childProcess = exec( @@ -272,8 +204,8 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, - CYPRESS_BASE_URL_SECOND: `http://localhost:${secondWebAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, + CYPRESS_BASE_URL_SECOND: secondWebAppBaseUrl, SPEC_PATTERN: specToRun, DD_TRACE_DEBUG: 'true', }, @@ -281,26 +213,30 @@ moduleTypes.forEach(({ ) await Promise.all([ - once(childProcess, 'exit'), once(childProcess.stdout, 'end'), once(childProcess.stderr, 'end'), - receiverPromise, + // cypress@latest esm + multi-origin browser context switching adds cold-start overhead + // that the suite-level `warmCypressBinary` (commonJS path) doesn't reach, so this one + // child run takes measurably longer than the rest of the suite and earns its own backstop. + receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ url }) => url.endsWith('/api/v2/citestcycle'), + payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + .filter(event => event.type !== 'span') + assert.strictEqual(events.length, 4) + + const test = events.find(event => event.type === 'test').content + assert.strictEqual(test.resource, 'cypress/e2e/multi-origin.js.tests multiple origins') + assert.strictEqual(test.meta[TEST_STATUS], 'pass') + }, + { hardTimeout: 50_000 } + ), ]) }) } it('sets _dd.test.is_user_provided_service to true if DD_SERVICE is used', async () => { - const receiverPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { - const events = payloads.flatMap(({ payload }) => payload.events) - - const testEvents = events.filter(event => event.type === 'test') - - testEvents.forEach(({ content: { meta } }) => { - assert.strictEqual(meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'true') - }) - }, 25000) - const envVars = getCiVisEvpProxyConfig(receiver.port) childProcess = exec( @@ -309,17 +245,26 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, DD_SERVICE: 'my-service', SPEC_PATTERN: 'cypress/e2e/spec.cy.js', }, } ) - await Promise.all([ - once(childProcess, 'exit'), - receiverPromise, - ]) + await receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ url }) => url.endsWith('/api/v2/citestcycle'), + payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const testEvents = events.filter(event => event.type === 'test') + + testEvents.forEach(({ content: { meta } }) => { + assert.strictEqual(meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'true') + }) + } + ) }) context('test management', () => { @@ -342,15 +287,15 @@ moduleTypes.forEach(({ }) }) - const getTestAssertions = ({ + const awaitTestAssertions = ({ isAttemptToFix, shouldAlwaysPass, shouldFailSometimes, isQuarantined, isDisabled, - }) => + }, child) => receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + .gatherPayloadsUntilChildExit(child, ({ 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 testSession = events.find(event => event.type === 'test_session_end').content @@ -419,7 +364,7 @@ moduleTypes.forEach(({ } } } - }, 25000) + }) /** * @param {{ @@ -439,13 +384,6 @@ moduleTypes.forEach(({ isDisabled, extraEnvVars = {}, } = {}) => { - const testAssertionsPromise = getTestAssertions({ - isAttemptToFix, - shouldAlwaysPass, - shouldFailSometimes, - isQuarantined, - isDisabled, - }) let stdout = '' const envVars = getCiVisEvpProxyConfig(receiver.port) @@ -458,7 +396,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, ...extraEnvVars, ...(shouldAlwaysPass ? { CYPRESS_SHOULD_ALWAYS_PASS: '1' } : {}), @@ -476,10 +414,13 @@ moduleTypes.forEach(({ process.stderr.write(data) }) - const [[exitCode]] = await Promise.all([ - once(childProcess, 'exit'), - testAssertionsPromise, - ]) + await awaitTestAssertions({ + isAttemptToFix, + shouldAlwaysPass, + shouldFailSometimes, + isQuarantined, + isDisabled, + }, childProcess) if (isAttemptToFix) { assert.match(stdout, /Datadog Test Optimization: attempting to fix .*attempt to fix is attempt to fix/) @@ -514,9 +455,9 @@ moduleTypes.forEach(({ } if (shouldAlwaysPass) { - assert.strictEqual(exitCode, 0) + assert.strictEqual(childProcess.exitCode, 0) } else { - assert.strictEqual(exitCode, 1) + assert.strictEqual(childProcess.exitCode, 1) } } @@ -538,6 +479,61 @@ moduleTypes.forEach(({ await runAttemptToFixTest({ isAttemptToFix: true, shouldFailSometimes: true }) }) + it('keeps after hook failures on attempt to fix tests', async () => { + receiver.setSettings({ test_management: { enabled: true, attempt_to_fix_retries: 3 } }) + receiver.setTestManagementTests({ + cypress: { + suites: { + 'cypress/e2e/attempt-to-fix-after-hook.js': { + tests: { + 'attempt to fix after hook passes before after hook fails': { + properties: { + attempt_to_fix: true, + }, + }, + }, + }, + }, + }, + }) + + const envVars = getCiVisEvpProxyConfig(receiver.port) + const specToRun = 'cypress/e2e/attempt-to-fix-after-hook.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: specToRun, + }, + } + ) + + await receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ 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 attemptToFixTests = tests + .filter(test => test.meta[TEST_NAME] === 'attempt to fix after hook passes before after hook fails') + .sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : 0)) + + assert.strictEqual(attemptToFixTests.length, 4) + + const lastAttempt = attemptToFixTests[attemptToFixTests.length - 1] + assert.strictEqual(lastAttempt.meta[TEST_STATUS], 'fail') + assert.match(lastAttempt.meta[ERROR_MESSAGE], /error in after hook/) + assert.strictEqual(lastAttempt.meta[TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED], 'false') + } + ) + + assert.strictEqual(childProcess.exitCode, 1) + }) + it('does not attempt to fix tests if test management is not enabled', async () => { receiver.setSettings({ test_management: { enabled: false, attempt_to_fix_retries: 3 } }) @@ -568,22 +564,6 @@ moduleTypes.forEach(({ known_tests_enabled: 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 atfTests = tests.filter( - t => t.meta[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] === 'true' - ) - assert.ok(atfTests.length > 0) - for (const test of atfTests) { - assert.ok( - !(TEST_IS_NEW in test.meta), - 'ATF test that is in known tests should not be tagged as new' - ) - } - }, 25000) - const envVars = getCiVisEvpProxyConfig(receiver.port) const specToRun = 'cypress/e2e/attempt-to-fix.js' @@ -593,17 +573,31 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, CYPRESS_SHOULD_ALWAYS_PASS: '1', }, } ) - await Promise.all([ - once(childProcess, 'exit'), - eventsPromise, - ]) + await receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ 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 atfTests = tests.filter( + t => t.meta[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] === 'true' + ) + assert.ok(atfTests.length > 0) + for (const test of atfTests) { + assert.ok( + !(TEST_IS_NEW in test.meta), + 'ATF test that is in known tests should not be tagged as new' + ) + } + } + ) }) it('ignores quarantine when attempting to fix a test', async () => { @@ -670,9 +664,9 @@ moduleTypes.forEach(({ }) }) - const getTestAssertions = (isDisabling) => + const awaitTestAssertions = (isDisabling, child) => receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + .gatherPayloadsUntilChildExit(child, ({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { const events = payloads.flatMap(({ payload }) => payload.events) const failedTest = events.find(event => event.type === 'test').content const testSession = events.find(event => event.type === 'test_session_end').content @@ -692,11 +686,9 @@ moduleTypes.forEach(({ assert.strictEqual(failedTest.meta[TEST_STATUS], 'fail') assert.ok(!(TEST_MANAGEMENT_IS_DISABLED in failedTest.meta)) } - }, 25000) + }) const runDisableTest = async (isDisabling, extraEnvVars = {}) => { - const testAssertionsPromise = getTestAssertions(isDisabling) - const envVars = getCiVisEvpProxyConfig(receiver.port) const specToRun = 'cypress/e2e/disable.js' @@ -707,22 +699,19 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, ...extraEnvVars, }, } ) - const [[exitCode]] = await Promise.all([ - once(childProcess, 'exit'), - testAssertionsPromise, - ]) + await awaitTestAssertions(isDisabling, childProcess) if (isDisabling) { - assert.strictEqual(exitCode, 0) + assert.strictEqual(childProcess.exitCode, 0) } else { - assert.strictEqual(exitCode, 1) + assert.strictEqual(childProcess.exitCode, 1) } } @@ -764,9 +753,9 @@ moduleTypes.forEach(({ }) }) - const getTestAssertions = (isQuarantining) => + const awaitTestAssertions = (isQuarantining, child) => receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + .gatherPayloadsUntilChildExit(child, ({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { const events = payloads.flatMap(({ payload }) => payload.events) const failedTest = events.find(event => event.type === 'test').content const testSession = events.find(event => event.type === 'test_session_end').content @@ -792,11 +781,9 @@ moduleTypes.forEach(({ assert.strictEqual(failedTest.meta[TEST_STATUS], 'fail') assert.ok(!(TEST_MANAGEMENT_IS_QUARANTINED in failedTest.meta)) } - }, 25000) + }) const runQuarantineTest = async (isQuarantining, extraEnvVars = {}) => { - const testAssertionsPromise = getTestAssertions(isQuarantining) - const envVars = getCiVisEvpProxyConfig(receiver.port) const specToRun = 'cypress/e2e/quarantine.js' @@ -807,22 +794,19 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, ...extraEnvVars, }, } ) - const [[exitCode]] = await Promise.all([ - once(childProcess, 'exit'), - testAssertionsPromise, - ]) + await awaitTestAssertions(isQuarantining, childProcess) if (isQuarantining) { - assert.strictEqual(exitCode, 0) + assert.strictEqual(childProcess.exitCode, 0) } else { - assert.strictEqual(exitCode, 1) + assert.strictEqual(childProcess.exitCode, 1) } } @@ -850,20 +834,7 @@ moduleTypes.forEach(({ test_management: { enabled: true }, flaky_test_retries_enabled: false, }) - receiver.setTestManagementTestsResponseCode(500) - - // Request module waits before retrying; browser runs are slow — need longer gather timeout - const eventsPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { - const events = payloads.flatMap(({ payload }) => payload.events) - const testSessionEnd = events.find(event => event.type === 'test_session_end') - assert.ok(testSessionEnd, 'expected test_session_end event in payloads') - const testSession = testSessionEnd.content - assert.ok(!(TEST_MANAGEMENT_ENABLED in testSession.meta)) - const tests = events.filter(event => event.type === 'test').map(event => event.content) - // it is not retried - assert.strictEqual(tests.length, 1) - }, 60000) + receiver.setTestManagementTestsResponseCode(404) const envVars = getCiVisEvpProxyConfig(receiver.port) @@ -875,17 +846,27 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, DD_TRACE_DEBUG: '1', }, } ) - await Promise.all([ - once(childProcess, 'exit'), - eventsPromise, - ]) + await receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ url }) => url.endsWith('/api/v2/citestcycle'), + (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSessionEnd = events.find(event => event.type === 'test_session_end') + assert.ok(testSessionEnd, 'expected test_session_end event in payloads') + const testSession = testSessionEnd.content + assert.ok(!(TEST_MANAGEMENT_ENABLED in testSession.meta)) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + // it is not retried + assert.strictEqual(tests.length, 1) + } + ) }) over12It('does not retry attempt to fix tests when testIsolation is false', async () => { @@ -909,8 +890,28 @@ moduleTypes.forEach(({ }, }) - const receiverPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const envVars = getCiVisEvpProxyConfig(receiver.port) + + const specToRun = 'cypress/e2e/attempt-to-fix.js' + + childProcess = exec( + version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, + { + cwd, + env: { + ...envVars, + CYPRESS_BASE_URL: webAppBaseUrl, + SPEC_PATTERN: specToRun, + CYPRESS_SHOULD_ALWAYS_PASS: '1', + CYPRESS_TEST_ISOLATION: 'false', + }, + } + ) + + await receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ 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 testSession = events.find(event => event.type === 'test_session_end').content @@ -932,30 +933,8 @@ moduleTypes.forEach(({ assert.ok(!(TEST_IS_RETRY in test.meta)) assert.ok(!(TEST_RETRY_REASON in test.meta)) }) - }, 25000) - - const envVars = getCiVisEvpProxyConfig(receiver.port) - - const specToRun = 'cypress/e2e/attempt-to-fix.js' - - childProcess = exec( - version === 'latest' ? testCommand : `${testCommand} --spec ${specToRun}`, - { - cwd, - env: { - ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, - SPEC_PATTERN: specToRun, - CYPRESS_SHOULD_ALWAYS_PASS: '1', - CYPRESS_TEST_ISOLATION: 'false', - }, } ) - - await Promise.all([ - once(childProcess, 'exit'), - receiverPromise, - ]) }) it('retries attempt to fix tests in the correct order (right after original test)', async () => { @@ -984,44 +963,6 @@ moduleTypes.forEach(({ }, }) - const receiverPromise = 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) - - // 1 test with attempt to fix (1 original + 3 retries) + 2 tests without = 6 tests total - assert.equal(tests.length, 6) - - // Extract test execution order with full details - const testExecutionOrder = tests.map(test => ({ - name: test.meta[TEST_NAME], - isRetry: test.meta[TEST_IS_RETRY] === 'true', - isAttemptToFix: test.meta[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] === 'true', - })) - - // Expected order: - // 1. "first test" (original, no retries) - // 2. "second test" (original) - // 3. "second test" (retry 1) - // 4. "second test" (retry 2) - // 5. "second test" (retry 3) - // 6. "third test" (original, no retries) - - assertObjectContains(testExecutionOrder, [ - { name: 'attempt to fix order first test', isRetry: false, isAttemptToFix: false }, - { name: 'attempt to fix order second test', isRetry: false, isAttemptToFix: true }, - { name: 'attempt to fix order second test', isRetry: true, isAttemptToFix: true }, - { name: 'attempt to fix order second test', isRetry: true, isAttemptToFix: true }, - { name: 'attempt to fix order second test', isRetry: true, isAttemptToFix: true }, - { name: 'attempt to fix order third test', isRetry: false, isAttemptToFix: false }, - ]) - - const testSession = events.find(event => event.type === 'test_session_end').content - assertObjectContains(testSession.meta, { - [TEST_MANAGEMENT_ENABLED]: 'true', - }) - }, 25000) - const envVars = getCiVisEvpProxyConfig(receiver.port) const specToRun = 'cypress/e2e/attempt-to-fix-order.js' @@ -1032,7 +973,7 @@ moduleTypes.forEach(({ cwd, env: { ...envVars, - CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, + CYPRESS_BASE_URL: webAppBaseUrl, SPEC_PATTERN: specToRun, }, } @@ -1046,10 +987,48 @@ moduleTypes.forEach(({ }) await Promise.all([ - once(childProcess, 'exit'), once(childProcess.stdout, 'end'), once(childProcess.stderr, 'end'), - receiverPromise, + receiver.gatherPayloadsUntilChildExit( + childProcess, + ({ 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) + + // 1 test with attempt to fix (1 original + 3 retries) + 2 tests without = 6 tests total + assert.equal(tests.length, 6) + + // Extract test execution order with full details + const testExecutionOrder = tests.map(test => ({ + name: test.meta[TEST_NAME], + isRetry: test.meta[TEST_IS_RETRY] === 'true', + isAttemptToFix: test.meta[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] === 'true', + })) + + // Expected order: + // 1. "first test" (original, no retries) + // 2. "second test" (original) + // 3. "second test" (retry 1) + // 4. "second test" (retry 2) + // 5. "second test" (retry 3) + // 6. "third test" (original, no retries) + + assertObjectContains(testExecutionOrder, [ + { name: 'attempt to fix order first test', isRetry: false, isAttemptToFix: false }, + { name: 'attempt to fix order second test', isRetry: false, isAttemptToFix: true }, + { name: 'attempt to fix order second test', isRetry: true, isAttemptToFix: true }, + { name: 'attempt to fix order second test', isRetry: true, isAttemptToFix: true }, + { name: 'attempt to fix order second test', isRetry: true, isAttemptToFix: true }, + { name: 'attempt to fix order third test', isRetry: false, isAttemptToFix: false }, + ]) + + const testSession = events.find(event => event.type === 'test_session_end').content + assertObjectContains(testSession.meta, { + [TEST_MANAGEMENT_ENABLED]: 'true', + }) + } + ), ]) assert.match(testOutput, /Retrying "attempt to fix order second test" because it is an attempt to fix/) diff --git a/integration-tests/cypress/e2e/attempt-to-fix-after-hook.js b/integration-tests/cypress/e2e/attempt-to-fix-after-hook.js new file mode 100644 index 00000000000..6562eef6ca9 --- /dev/null +++ b/integration-tests/cypress/e2e/attempt-to-fix-after-hook.js @@ -0,0 +1,13 @@ +/* eslint-disable */ + +describe('attempt to fix after hook', () => { + after(() => { + throw new Error('error in after hook') + }) + + it('passes before after hook fails', () => { + cy.visit('/') + .get('.hello-world') + .should('have.text', 'Hello World') + }) +}) diff --git a/integration-tests/cypress/e2e/efd-duration.cy.js b/integration-tests/cypress/e2e/efd-duration.cy.js new file mode 100644 index 00000000000..6946e62f33b --- /dev/null +++ b/integration-tests/cypress/e2e/efd-duration.cy.js @@ -0,0 +1,13 @@ +/* eslint-disable */ +'use strict' + +describe('efd duration retries', () => { + it('instant test', () => { + expect(1 + 1).to.equal(2) + }) + + it('slightly slow test', () => { + cy.wait(5100) + expect(1 + 1).to.equal(2) + }) +}) diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index a1daf328ff5..d9b08f5ffa0 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -235,7 +235,7 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle * @param {string} [options.deployedFile] - The deployed file path. If not provided, will be inferred from the stack * trace. * @param {string} [options.sourceFile] - The source file path. Defaults to `deployedFile` if not provided. - * @param {number} [options.stackIndex=0] - The stack index to use when inferring the file from the stack trace. + * @param {number} [options.stackIndex] - The stack index to use when inferring the file from the stack trace. * @returns {BreakpointInfo[]} An array of breakpoint information objects found in the file. */ function getBreakpointInfo ({ deployedFile, sourceFile = deployedFile, stackIndex = 0 } = {}) { diff --git a/integration-tests/electron/app/index.html b/integration-tests/electron/app/index.html new file mode 100644 index 00000000000..f6207f8ca4d --- /dev/null +++ b/integration-tests/electron/app/index.html @@ -0,0 +1,10 @@ + + + + + + Electron Integration Test + + + + diff --git a/integration-tests/electron/app/main.js b/integration-tests/electron/app/main.js new file mode 100644 index 00000000000..86bc4599157 --- /dev/null +++ b/integration-tests/electron/app/main.js @@ -0,0 +1,69 @@ +'use strict' + +const { channel } = require('node:diagnostics_channel') + +// dd-trace is bundled inside the binary alongside the app source. +// No external path or NODE_OPTIONS injection needed. +require('dd-trace').init({ + service: 'electron-integration-test', + flushInterval: 0, + plugins: false, + experimental: { exporter: 'electron' }, +}).use('electron', true) + +// Forward spans exported via the electron exporter to the test process via IPC. +channel('datadog:apm:electron:export').subscribe(traces => { + process.send({ name: 'traces', payload: traces }) +}) + +const path = require('path') +const { app, BrowserWindow, ipcMain, net } = require('electron/main') + +const BRIDGE_CONFIG_CHANNEL = 'datadog:bridge-config' + +// The Datadog preload script calls ipcRenderer.sendSync(BRIDGE_CONFIG_CHANNEL) +// at load time, so this handler must be registered before any BrowserWindow loads. +ipcMain.on(BRIDGE_CONFIG_CHANNEL, event => { + event.returnValue = null +}) + +let win + +app.whenReady().then(() => { + // Create the window immediately so it is ready before any test command arrives. + win = new BrowserWindow({ + show: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + }, + }) + + win.loadFile(path.join(__dirname, 'index.html')) + + // Only signal ready after the renderer has finished loading so that any + // subsequent IPC message sent by the test will find a live webContents. + win.webContents.once('did-finish-load', () => process.send('ready')) + + process.on('message', ({ name, url }) => { + switch (name) { + case 'quit': + return app.quit() + + // Trigger an outgoing HTTP request so we get an http.request span. + case 'http': + return net.fetch(url).catch(() => {}) + + // Send an IPC message from the main process to the renderer so we get + // an electron.main.send span. + case 'ipc': + return win.webContents.send('ping') + + // Ask the renderer to check DatadogEventBridge and report back. + case 'bridge': + ipcMain.once('bridge-result', (_event, result) => { + process.send({ name: 'bridge-result', result }) + }) + return win.webContents.send('check-bridge') + } + }) +}) diff --git a/integration-tests/electron/app/preload.js b/integration-tests/electron/app/preload.js new file mode 100644 index 00000000000..beaeaf9fb09 --- /dev/null +++ b/integration-tests/electron/app/preload.js @@ -0,0 +1,29 @@ +'use strict' + +const { ipcRenderer } = require('electron/renderer') + +// Receive the ping from the main process. +// The electron instrumentation wraps ipcRenderer.on so this creates an +// electron.renderer.receive span. +ipcRenderer.on('ping', () => {}) + +// Check that DatadogEventBridge was injected by the Datadog preload script. +ipcRenderer.on('check-bridge', () => { + const bridge = window.DatadogEventBridge + let sendSuccess = false + + try { + bridge.send('test-payload') + sendSuccess = true + } catch (_e) { + // sendSuccess stays false + } + + ipcRenderer.send('bridge-result', { + exists: !!bridge, + capabilities: bridge?.getCapabilities(), + privacyLevel: bridge?.getPrivacyLevel(), + allowedHosts: bridge?.getAllowedWebViewHosts(), + sendSuccess, + }) +}) diff --git a/integration-tests/electron/electron.spec.js b/integration-tests/electron/electron.spec.js new file mode 100644 index 00000000000..9215441c19c --- /dev/null +++ b/integration-tests/electron/electron.spec.js @@ -0,0 +1,204 @@ +'use strict' + +const assert = require('assert') +const { execFileSync, spawn } = require('child_process') +const fs = require('fs') +const http = require('http') +const path = require('path') + +const { useSandbox, sandboxCwd } = require('../helpers') +const { assertTraceReceived } = require('./helpers') + +describe('Electron integration', function () { + let httpServer + let httpPort + let binaryPath + let child + + // Create a sandbox with electron and @electron/packager installed alongside dd-trace. + // The app source files are copied in; dd-trace is then installed into the app directory + // from the pre-packed sandbox tgz so electron-packager bundles it inside the binary. + useSandbox(['electron', '@electron/packager'], false, [path.join(__dirname, 'app')]) + + before(async function () { + this.timeout(30_000) + + const sandboxFolder = sandboxCwd() + const appDir = path.join(sandboxFolder, 'app') + + // createSandbox packs dd-trace into a .tgz one level above the sandbox folder. + // We reference it with a file: URL so npm installs the exact local build. + const ddTraceTgz = path.join(path.dirname(sandboxFolder), 'dd-trace.tgz') + + // Write the app's package.json with dd-trace as a bundled dependency so that + // electron-packager includes it inside the binary together with the app source. + fs.writeFileSync( + path.join(appDir, 'package.json'), + JSON.stringify({ + name: 'ElectronTest', + version: '1.0.0', + main: 'main.js', + dependencies: { 'dd-trace': `file:${ddTraceTgz}` }, + }) + ) + + // Install dd-trace and its transitive dependencies into the app directory. + execFileSync('npm', ['install'], { cwd: appDir, stdio: 'pipe' }) + + // Use the electron version already installed in the sandbox so that + // electron-packager finds the cached binary and does not re-download it. + const electronVersion = require( + path.join(sandboxFolder, 'node_modules', 'electron', 'package.json') + ).version + + // Build a real standalone Electron binary. Running a compiled binary rather + // than `electron ` is a more faithful test of production behaviour. + const outDir = path.join(sandboxFolder, 'dist') + fs.mkdirSync(outDir, { recursive: true }) + + execFileSync( + path.join(sandboxFolder, 'node_modules', '.bin', 'electron-packager'), + [ + appDir, + 'ElectronTest', + `--platform=${process.platform}`, + `--arch=${process.arch}`, + `--electron-version=${electronVersion}`, + `--out=${outDir}`, + '--overwrite', + ], + { cwd: sandboxFolder, stdio: 'pipe' } + ) + + // The layout produced by electron-packager differs per platform: + // macOS → /ElectronTest-darwin-/ElectronTest.app/Contents/MacOS/ElectronTest + // Linux → /ElectronTest-linux-/ElectronTest + // Windows→ /ElectronTest-win32-/ElectronTest.exe + const packageDir = path.join(outDir, `ElectronTest-${process.platform}-${process.arch}`) + if (process.platform === 'darwin') { + binaryPath = path.join(packageDir, 'ElectronTest.app', 'Contents', 'MacOS', 'ElectronTest') + } else if (process.platform === 'win32') { + binaryPath = path.join(packageDir, 'ElectronTest.exe') + } else { + binaryPath = path.join(packageDir, 'ElectronTest') + } + + await new Promise(resolve => { + httpServer = http.createServer((_req, res) => { + res.writeHead(200) + res.end() + }) + httpServer.listen(0, '127.0.0.1', () => { + httpPort = httpServer.address().port + resolve() + }) + }) + }) + + after(async function () { + if (httpServer) await new Promise(resolve => httpServer.close(resolve)) + }) + + beforeEach(function (done) { + this.timeout(15_000) + + child = spawn(binaryPath, process.platform === 'linux' ? ['--no-sandbox'] : [], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + windowsHide: true, + }) + child.on('error', done) + // 'ready' is sent by main.js only after the renderer window has finished + // loading, so IPC commands can be issued as soon as this fires. + child.on('message', msg => { if (msg === 'ready') done() }) + }) + + afterEach(done => { + const proc = child + child = null + proc.send({ name: 'quit' }) + proc.once('close', done) + }) + + it('should create an http.request span for net.fetch calls', done => { + assertTraceReceived(child, ({ payload }) => { + const spans = payload.flat() + const span = spans.find(s => s.name === 'http.request') + if (!span) throw new Error('No http.request span found') + + assert.strictEqual(span.type, 'http') + assert.strictEqual(span.resource, 'GET') + assert.strictEqual(span.service, 'electron-integration-test') + assert.strictEqual(span.error, 0) + assert.strictEqual(span.meta.component, 'electron') + assert.strictEqual(span.meta['span.kind'], 'client') + assert.strictEqual(span.meta['http.method'], 'GET') + assert.strictEqual(span.meta['http.status_code'], '200') + assert.strictEqual(span.meta['http.url'], `http://127.0.0.1:${httpPort}/`) + }).then(done, done) + + child.send({ name: 'http', url: `http://127.0.0.1:${httpPort}/` }) + }) + + it('should create an electron.main.send span for IPC send from main to renderer', done => { + assertTraceReceived(child, ({ payload }) => { + const spans = payload.flat() + const span = spans.find(s => s.name === 'electron.main.send') + if (!span) throw new Error('No electron.main.send span found') + + assert.strictEqual(span.resource, 'ping') + assert.strictEqual(span.service, 'electron-integration-test') + assert.strictEqual(span.error, 0) + assert.strictEqual(span.meta.component, 'electron') + assert.strictEqual(span.meta['span.kind'], 'producer') + }).then(done, done) + + child.send({ name: 'ipc' }) + }) + + it('should inject DatadogEventBridge in the renderer process', done => { + function handler (msg) { + if (!msg || msg.name !== 'bridge-result') return + child.removeListener('message', handler) + try { + assert.strictEqual(msg.result.exists, true, 'DatadogEventBridge should exist on window') + assert.strictEqual(msg.result.capabilities, '[]') + assert.ok(msg.result.privacyLevel, 'privacyLevel should be set') + assert.ok(msg.result.sendSuccess, 'bridge.send() should not throw') + done() + } catch (e) { + done(e) + } + } + + child.on('message', handler) + child.send({ name: 'bridge' }) + }) + + it('should produce spans for both HTTP and IPC when both operations are triggered', done => { + // Register both assertions before triggering anything so no message is missed. + const httpSpanSeen = assertTraceReceived(child, ({ payload }) => { + const spans = payload.flat() + const span = spans.find(s => s.name === 'http.request') + if (!span) throw new Error('No http.request span found') + + assert.strictEqual(span.type, 'http') + assert.strictEqual(span.meta.component, 'electron') + assert.strictEqual(span.meta['http.status_code'], '200') + assert.strictEqual(span.meta['http.url'], `http://127.0.0.1:${httpPort}/`) + }) + + const ipcSpanSeen = assertTraceReceived(child, ({ payload }) => { + const spans = payload.flat() + const span = spans.find(s => s.name === 'electron.main.send') + if (!span) throw new Error('No electron.main.send span found') + + assert.strictEqual(span.meta.component, 'electron') + assert.strictEqual(span.meta['span.kind'], 'producer') + }) + + Promise.all([httpSpanSeen, ipcSpanSeen]).then(() => done(), done) + + child.send({ name: 'http', url: `http://127.0.0.1:${httpPort}/` }) + child.send({ name: 'ipc' }) + }) +}) diff --git a/integration-tests/electron/helpers.js b/integration-tests/electron/helpers.js new file mode 100644 index 00000000000..247e6427895 --- /dev/null +++ b/integration-tests/electron/helpers.js @@ -0,0 +1,42 @@ +'use strict' + +/** + * Wait for the child process to emit a `{ name: 'traces', payload }` IPC message + * that satisfies `fn`. Resolves on first success, rejects on timeout. + * + * @param {import('child_process').ChildProcess} child + * @param {(msg: { payload: object[][] }) => void} fn + * @param {number} [timeout] + * @returns {Promise} + */ +function assertTraceReceived (child, fn, timeout = 2000) { + let resolve, reject + const errors = [] + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + + const timeoutObj = setTimeout(() => { + const errorsMsg = errors.length === 0 ? '' : `, additionally:\n${errors.map(e => e.stack).join('\n')}\n===\n` + reject(new Error(`timeout${errorsMsg}`)) + child.removeListener('message', handler) + }, timeout) + + function handler (msg) { + if (!msg || msg.name !== 'traces') return + try { + fn({ payload: msg.payload }) + clearTimeout(timeoutObj) + resolve() + child.removeListener('message', handler) + } catch (e) { + errors.push(e) + } + } + + child.on('message', handler) + return promise +} + +module.exports = { assertTraceReceived } diff --git a/integration-tests/esbuild/package.json b/integration-tests/esbuild/package.json index 84f8ad6904c..e1d4f4f1c45 100644 --- a/integration-tests/esbuild/package.json +++ b/integration-tests/esbuild/package.json @@ -20,13 +20,13 @@ "author": "Thomas Hunter II ", "license": "ISC", "dependencies": { - "@apollo/server": "5.5.0", + "@apollo/server": "5.5.1", "@koa/router": "15.5.0", "aws-sdk": "2.1693.0", "axios": "1.16.0", "express": "4.22.1", "knex": "3.2.10", "koa": "3.2.0", - "openai": "6.35.0" + "openai": "6.37.0" } } diff --git a/integration-tests/helpers/fake-agent.js b/integration-tests/helpers/fake-agent.js index afac42eafc4..5db9fd6f685 100644 --- a/integration-tests/helpers/fake-agent.js +++ b/integration-tests/helpers/fake-agent.js @@ -97,7 +97,7 @@ module.exports = class FakeAgent extends EventEmitter { /** * Add a config object to be returned by the fake Remote Config endpoint. * @param {object} config - Object containing the Remote Config "file" and metadata - * @param {number} [config.orgId=2] - The Datadog organization ID + * @param {number} [config.orgId] - The Datadog organization ID. Defaults to 2. * @param {string} config.product - The Remote Config product name * @param {string} config.id - The Remote Config config ID * @param {string} [config.name] - The Remote Config "name". Defaults to the sha256 hash of `config.id` @@ -218,9 +218,9 @@ module.exports = class FakeAgent extends EventEmitter { * @param {Function} [options.fn] - Function called with each matching telemetry message. If it throws, * the error is collected and the listener stays alive for the next message. Defaults to a no-op. * @param {string} options.requestType - The telemetry request type to match. - * @param {number} [options.timeout=30_000] - Timeout in milliseconds before the promise rejects. - * @param {number} [options.expectedMessageCount=1] - Number of matching messages to wait for. - * @param {boolean} [options.resolveAtFirstSuccess=false] - Resolve as soon as `fn` first runs without throwing. + * @param {number} [options.timeout] - Timeout in milliseconds before the promise rejects. + * @param {number} [options.expectedMessageCount] - Number of matching messages to wait for. + * @param {boolean} [options.resolveAtFirstSuccess] - Resolve as soon as `fn` first runs without throwing. * @param {string} [options.namespace] - If set, only consider messages whose payload namespace equals this value. * @returns {Promise} A promise that resolves when the expected telemetry messages are received and `fn` has * run successfully. If `fn` throws on every matching message, the promise rejects once `timeout` is reached. @@ -288,8 +288,8 @@ module.exports = class FakeAgent extends EventEmitter { * @param {object} options * @param {() => Promise} options.trigger Fired once the listener is in place. * @param {(group: object[]) => boolean} options.predicate Group-level filter. - * @param {number} [options.expectedCount=1] Resolve after this many matching groups arrive. - * @param {number} [options.timeout=30_000] Timeout in milliseconds before the promise rejects. + * @param {number} [options.expectedCount] Resolve after this many matching groups arrive. + * @param {number} [options.timeout] Timeout in milliseconds before the promise rejects. * @returns {Promise} The matching groups, in arrival order. */ collectGroups ({ trigger, predicate, expectedCount = 1, timeout = 30_000 }) { diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index fd8b247e7de..c0b0e3d7ff1 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -277,8 +277,8 @@ function spawnProcAndExpectExit (filename, options = {}, stdioHandler, stderrHan * * @param {childProcess.ChildProcess|undefined} proc - Process to stop. * @param {object} [options] - Stop options. - * @param {NodeJS.Signals} [options.signal='SIGTERM'] - Signal to send before escalating. - * @param {number} [options.timeoutMs=defaultStopProcTimeoutMs] - Max wait per signal in milliseconds. + * @param {NodeJS.Signals} [options.signal] - Signal to send before escalating. Defaults to `SIGTERM`. + * @param {number} [options.timeoutMs] - Max wait per signal in milliseconds. Defaults to the stop-proc timeout. * @returns {Promise} */ async function stopProc (proc, options = {}) { @@ -307,6 +307,27 @@ async function stopProc (proc, options = {}) { } } +/** + * Tear down a Test Optimization integration fixture between tests. + * + * Awaits each step so the next test starts from a clean slate — letting the + * previous child outlive `afterEach` leaks sockets and file descriptors that + * the next Cypress / Playwright run then races against. + * + * @param {object} env + * @param {childProcess.ChildProcess} [env.childProcess] - Test child to stop. + * @param {import('http').Server} [env.webAppServer] - Web fixture server to close. + * @param {FakeAgent} [env.receiver] - Fake agent / intake to stop. + * @returns {Promise} + */ +async function stopCiVisTestEnv ({ childProcess, webAppServer, receiver }) { + await stopProc(childProcess) + if (webAppServer?.listening) { + await /** @type {Promise} */ (new Promise((resolve) => webAppServer.close(() => resolve()))) + } + await receiver?.stop() +} + /** * Wait for a process to exit for up to `timeoutMs`. * @@ -1112,6 +1133,28 @@ function assertUUID (actual, msg = 'not a valid UUID') { assert.match(actual, /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, msg) } +/** + * Recursively `Object.freeze` an object plus everything reachable from it. Use + * in tests when an instrumentation contract says "we won't mutate this": any + * accidental write to the value or its nested structures throws synchronously + * under strict mode, so the test fails at the offending assignment instead of + * far downstream via deep-equality. + * + * @template T + * @param {T} value + * @returns {T} + */ +function deepFreeze (value) { + if (value === null || typeof value !== 'object' || Object.isFrozen(value)) { + return value + } + Object.freeze(value) + for (const key of Reflect.ownKeys(value)) { + deepFreeze(value[key]) + } + return value +} + module.exports = { ANY_NUMBER, ANY_STRING, @@ -1120,7 +1163,9 @@ module.exports = { hookFile, assertObjectContains, assertUUID, + deepFreeze, stopProc, + stopCiVisTestEnv, spawnProc, spawnProcAndExpectExit, telemetryForwarder, diff --git a/integration-tests/jest/jest.core.spec.js b/integration-tests/jest/jest.core.spec.js index 1febf3204f0..c6be5afbcf6 100644 --- a/integration-tests/jest/jest.core.spec.js +++ b/integration-tests/jest/jest.core.spec.js @@ -1108,6 +1108,42 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { }) }) + it('keeps default stack formatting when imported modules read error stacks', async () => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const suites = events.filter(event => event.type === 'test_suite_end') + const stackImportSuites = suites.filter( + suite => suite.content.meta[TEST_SUITE] === + 'ci-visibility/jest-stack-on-import/jest-stack-on-import-test.js' + ) + + assert.strictEqual(stackImportSuites.length, 1) + assert.strictEqual(stackImportSuites[0].content.meta[TEST_STATUS], 'pass') + }) + + childProcess = exec(runTestsCommand, { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'jest-stack-on-import/jest-stack-on-import-test', + SHOULD_CHECK_RESULTS: 'true', + }, + }) + childProcess.stdout?.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr?.on('data', (chunk) => { + testOutput += chunk.toString() + }) + + const [exitCode] = await once(childProcess, 'exit') + + assert.strictEqual(exitCode, 0, testOutput) + assert.doesNotMatch(testOutput, /originalPrepareStackTrace is not a function/) + await eventsPromise + }) + it('reports parsing errors in the test file', (done) => { const eventsPromise = receiver .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { diff --git a/integration-tests/jest/jest.itr-efd.spec.js b/integration-tests/jest/jest.itr-efd.spec.js index a3e4852ad51..6dc50804665 100644 --- a/integration-tests/jest/jest.itr-efd.spec.js +++ b/integration-tests/jest/jest.itr-efd.spec.js @@ -42,7 +42,10 @@ const { TEST_HAS_FAILED_ALL_RETRIES, TEST_RETRY_REASON_TYPES, TEST_FINAL_STATUS, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, } = require('../../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') const { DD_MAJOR, NODE_MAJOR } = require('../../version') @@ -61,6 +64,13 @@ const JEST_VERSION = requestedJestVersion === 'oldest' ? oldestJestVersion : req const onlyLatestIt = JEST_VERSION === 'latest' ? it : it.skip const shouldInstallJestEnvironmentJsdom = JEST_VERSION === 'latest' || Number(JEST_VERSION.split('.')[0]) >= 28 +function assertItrSkippingEnabledTags (events, expected) { + const testSuite = events.find(event => event.type === 'test_suite_end').content + assert.strictEqual(testSuite.meta[TEST_ITR_SKIPPING_ENABLED], expected) + const test = events.find(event => event.type === 'test').content + assert.strictEqual(test.meta[TEST_ITR_SKIPPING_ENABLED], expected) +} + // TODO: add ESM tests describe(`jest@${JEST_VERSION} commonJS`, () => { let receiver @@ -233,6 +243,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'false') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'false') + assertItrSkippingEnabledTags(payload.events, 'false') }, ({ url }) => url === '/api/v2/citestcycle').then(() => done()).catch(done) childProcess = exec( @@ -298,6 +309,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_TYPE], 'suite') assert.strictEqual(testModule.metrics[TEST_ITR_SKIPPING_COUNT], 1) + assertItrSkippingEnabledTags(eventsRequest.payload.events, 'true') done() }).catch(done) @@ -380,6 +392,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assertItrSkippingEnabledTags(payload.events, 'true') }, ({ url }) => url === '/api/v2/citestcycle').then(() => done()).catch(done) childProcess = exec( @@ -584,6 +597,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assertItrSkippingEnabledTags(events, 'true') }, 25000) childProcess = exec( @@ -898,45 +912,242 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { }) context('error tags', () => { - it('tags session and children with _dd.ci.library_configuration_error when settings fails 4xx', async () => { - receiver.setSettingsResponseCode(404) - const eventsPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') - const testEvent = events.find(event => event.type === 'test') - assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fails 4xx', + async () => { + receiver.setSettingsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), }) - childProcess = exec(runTestsCommand, { - cwd, - env: getCiVisAgentlessConfig(receiver.port), + await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) - await Promise.all([eventsPromise, once(childProcess, 'exit')]) - }) - context('when jest is using workers to run tests in parallel', () => { - it('tags session and children with _dd.ci.library_configuration_error when settings fails 4xx', async () => { - receiver.setSettingsResponseCode(404) + it( + 'tags session and children with _dd.ci.library_configuration_error.skippable_tests when request fails 4xx', + async () => { + receiver.setSkippableSuitesResponseCode(404) const eventsPromise = receiver .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + assert.strictEqual(testSession.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') const testEvent = events.find(event => event.type === 'test') assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') }) childProcess = exec(runTestsCommand, { cwd, - env: { - ...getCiVisAgentlessConfig(receiver.port), - RUN_IN_PARALLEL: 'true', - }, + env: getCiVisAgentlessConfig(receiver.port), + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), }) await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests when request fails', + async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual( + testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + context('when jest is using workers to run tests in parallel', () => { + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fails 4xx', + async () => { + receiver.setSettingsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + RUN_IN_PARALLEL: 'true', + }, + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.skippable_tests when request fails 4xx', + async () => { + receiver.setSkippableSuitesResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + RUN_IN_PARALLEL: 'true', + }, + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + RUN_IN_PARALLEL: 'true', + }, + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests when request fails', + async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual( + testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual( + testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + }) + childProcess = exec(runTestsCommand, { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + RUN_IN_PARALLEL: 'true', + }, + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) }) }) @@ -1074,7 +1285,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { const atrRetries = tests.filter(t => t.meta[TEST_RETRY_REASON] === TEST_RETRY_REASON_TYPES.atr) assert.strictEqual(efdRetries.length, NUM_RETRIES_EFD) assert.strictEqual(atrRetries.length, 0) - }) + }, 30_000) childProcess = exec( runTestsCommand, @@ -2638,7 +2849,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { ) assert.strictEqual(efdRetries.length, 2) testEvents.forEach(t => assert.strictEqual(t.meta[TEST_IS_NEW], 'true')) - }) + }, 30_000) childProcess = exec( runTestsCommand, @@ -2682,7 +2893,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { ) assert.strictEqual(efdRetries.length, 1) testEvents.forEach(t => assert.strictEqual(t.meta[TEST_IS_NEW], 'true')) - }, 25_000) + }, 30_000) childProcess = exec( runTestsCommand, @@ -2724,7 +2935,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testEvent.meta[TEST_IS_NEW], 'true') assert.strictEqual(testEvent.meta[TEST_EARLY_FLAKE_ABORT_REASON], 'slow') assert.ok(!(TEST_IS_RETRY in testEvent.meta), 'should not be retried') - }, 20_000) + }, 30_000) childProcess = exec( runTestsCommand, diff --git a/integration-tests/jest/jest.test-management.spec.js b/integration-tests/jest/jest.test-management.spec.js index 5a1856bc19d..18e58d006c1 100644 --- a/integration-tests/jest/jest.test-management.spec.js +++ b/integration-tests/jest/jest.test-management.spec.js @@ -1383,12 +1383,17 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { .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 suites = events.filter(event => event.type === 'test_suite_end').map(event => event.content) const testSession = events.find(event => event.type === 'test_session_end').content if (isQuarantining) { assert.strictEqual(testSession.meta[TEST_MANAGEMENT_ENABLED], 'true') // test session is passed even though a test fails because the test is quarantined assert.strictEqual(testSession.meta[TEST_STATUS], 'pass') + const quarantinedSuite = suites.find( + suite => suite.meta[TEST_SUITE] === 'ci-visibility/test-management/test-quarantine-1.js' + ) + assert.strictEqual(quarantinedSuite.meta[TEST_STATUS], 'pass') } else { assert.ok(!(TEST_MANAGEMENT_ENABLED in testSession.meta)) assert.strictEqual(testSession.meta[TEST_STATUS], 'fail') diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index b96fa7136e9..8e8807cae15 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -70,7 +70,10 @@ const { TEST_FRAMEWORK_VERSION, LIBRARY_VERSION, TEST_PARAMETERS, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, TEST_FINAL_STATUS, } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -83,6 +86,13 @@ const { } = require('../../packages/dd-trace/src/constants') const { DD_MAJOR, VERSION: ddTraceVersion } = require('../../version') +function assertItrSkippingEnabledTags (events, expected) { + const testSuite = events.find(event => event.type === 'test_suite_end').content + assert.strictEqual(testSuite.meta[TEST_ITR_SKIPPING_ENABLED], expected) + const test = events.find(event => event.type === 'test').content + assert.strictEqual(test.meta[TEST_ITR_SKIPPING_ENABLED], expected) +} + const runTestsCommand = 'node ./ci-visibility/run-mocha.js' const runTestsWithCoverageCommand = `./node_modules/nyc/bin/nyc.js -r=text-summary ${runTestsCommand}` const testFile = 'ci-visibility/run-mocha.js' @@ -1118,6 +1128,142 @@ describe(`mocha@${MOCHA_VERSION}`, function () { once(childProcess, 'exit'), ]) }) + + context('suite reporting - tests with no describe wrapper', () => { + // When a test file uses only top-level it() calls (no describe() block), + // mocha never emits a non-root 'suite' event for that file. + // + // The 'suite' handler in packages/datadog-instrumentations/src/mocha/main.js + // only fires testSuiteStartCh when it encounters a non-root suite with + // suite.tests.length > 0. With no non-root suites at all, the condition is + // never met: no suite context is stored in testFileToSuiteCtx, no suite + // span is created by the plugin, and no test_suite_end event is emitted. + // + // Meanwhile tests run and report normally as mocha.test spans. + + it('reports a suite event for a single file with only top-level tests', async () => { + const suiteFile = 'ci-visibility/mocha-plugin-tests/top-level-it-no-describe.js' + 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(e => e.content) + const suiteEvents = events.filter(event => event.type === 'test_suite_end').map(e => e.content) + + assert.strictEqual(tests.length, 2) + assert.strictEqual(suiteEvents.length, 1) + assertObjectContains(suiteEvents[0], { meta: { [TEST_SUITE]: suiteFile, [TEST_STATUS]: 'pass' } }) + tests.forEach(test => assert.strictEqual(test.meta[TEST_SUITE], suiteFile)) + }) + + childProcess = exec( + `node node_modules/mocha/bin/mocha ./${suiteFile}`, + { cwd, env: envVars } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it('reports suite events for two files with only top-level tests', async () => { + const suiteFiles = [ + 'ci-visibility/mocha-plugin-tests/top-level-it-no-describe.js', + 'ci-visibility/mocha-plugin-tests/top-level-it-no-describe2.js', + ] + 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(e => e.content) + const suiteEvents = events.filter(event => event.type === 'test_suite_end').map(e => e.content) + + assert.strictEqual(tests.length, 4) + assert.strictEqual(suiteEvents.length, 2) + suiteFiles.forEach(suiteFile => { + const suite = suiteEvents.find(e => e.meta[TEST_SUITE] === suiteFile) + assert.ok(suite, `Expected suite event for ${suiteFile}`) + assert.strictEqual(suite.meta[TEST_STATUS], 'pass') + }) + tests.forEach(test => assert.ok(suiteFiles.includes(test.meta[TEST_SUITE]))) + }) + + childProcess = exec( + 'node node_modules/mocha/bin/mocha' + + ` ./${suiteFiles[0]} ./${suiteFiles[1]}`, + { cwd, env: envVars } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it('reports a suite event for a file with mixed top-level and nested tests', async () => { + const suiteFile = 'ci-visibility/mocha-plugin-tests/top-level-it-mixed.js' + 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(e => e.content) + const suiteEvents = events.filter(event => event.type === 'test_suite_end').map(e => e.content) + + assert.strictEqual(tests.length, 2) + assert.strictEqual(suiteEvents.length, 1) + assertObjectContains(suiteEvents[0], { meta: { [TEST_SUITE]: suiteFile, [TEST_STATUS]: 'pass' } }) + tests.forEach(test => assert.strictEqual(test.meta[TEST_SUITE], suiteFile)) + }) + + childProcess = exec( + `node node_modules/mocha/bin/mocha ./${suiteFile}`, + { cwd, env: envVars } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it('reports suite events for two files with mixed top-level and nested tests', async () => { + const suiteFiles = [ + 'ci-visibility/mocha-plugin-tests/top-level-it-mixed.js', + 'ci-visibility/mocha-plugin-tests/top-level-it-mixed2.js', + ] + 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(e => e.content) + const suiteEvents = events.filter(event => event.type === 'test_suite_end').map(e => e.content) + + assert.strictEqual(tests.length, 4) + assert.strictEqual(suiteEvents.length, 2) + suiteFiles.forEach(suiteFile => { + const suite = suiteEvents.find(e => e.meta[TEST_SUITE] === suiteFile) + assert.ok(suite, `Expected suite event for ${suiteFile}`) + assert.strictEqual(suite.meta[TEST_STATUS], 'pass') + }) + tests.forEach(test => assert.ok(suiteFiles.includes(test.meta[TEST_SUITE]))) + }) + + childProcess = exec( + 'node node_modules/mocha/bin/mocha' + + ` ./${suiteFiles[0]} ./${suiteFiles[1]}`, + { cwd, env: envVars } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it('reports fail status when a top-level test fails alongside passing nested tests', async () => { + const suiteFile = 'ci-visibility/mocha-plugin-tests/top-level-it-mixed-failing.js' + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const suiteEvents = events.filter(event => event.type === 'test_suite_end').map(e => e.content) + + assert.strictEqual(suiteEvents.length, 1) + assertObjectContains(suiteEvents[0], { meta: { [TEST_SUITE]: suiteFile, [TEST_STATUS]: 'fail' } }) + }) + + childProcess = exec( + `node node_modules/mocha/bin/mocha ./${suiteFile}`, + { cwd, env: envVars } + ) + + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + }) }) }) @@ -1700,6 +1846,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'false') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'false') + assertItrSkippingEnabledTags(payload.events, 'false') }, ({ url }) => url === '/api/v2/citestcycle').then(() => done()).catch(done) childProcess = exec( @@ -1762,6 +1909,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_TYPE], 'suite') assert.strictEqual(testModule.metrics[TEST_ITR_SKIPPING_COUNT], 1) + assertItrSkippingEnabledTags(eventsRequest.payload.events, 'true') done() }).catch(done) @@ -1844,6 +1992,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assertItrSkippingEnabledTags(payload.events, 'true') }, ({ url }) => url === '/api/v2/citestcycle').then(() => done()).catch(done) childProcess = exec( @@ -2056,6 +2205,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'true') assert.strictEqual(testModule.meta[TEST_ITR_SKIPPING_ENABLED], 'true') + assertItrSkippingEnabledTags(events, 'true') }, 25000) childProcess = exec( @@ -2171,6 +2321,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { 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') + assertItrSkippingEnabledTags(events, 'true') }) childProcess = exec( @@ -2191,23 +2342,115 @@ describe(`mocha@${MOCHA_VERSION}`, function () { }) context('error tags', () => { - it('tags session and children with _dd.ci.library_configuration_error when settings fails 4xx', async () => { - receiver.setSettingsResponseCode(404) - const eventsPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') - const testEvent = events.find(event => event.type === 'test') - assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fails 4xx', + async () => { + receiver.setSettingsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), }) - childProcess = exec(runTestsCommand, { - cwd, - env: getCiVisAgentlessConfig(receiver.port), + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.skippable_tests when request fails 4xx', + async () => { + receiver.setSkippableSuitesResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests when request fails', + async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual( + testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + }) + childProcess = exec(runTestsCommand, { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) - await Promise.all([eventsPromise, once(childProcess, 'exit')]) - }) }) context('early flake detection', () => { diff --git a/integration-tests/playwright.config.js b/integration-tests/playwright.config.js index 0efce106a30..5fb0c46e81a 100644 --- a/integration-tests/playwright.config.js +++ b/integration-tests/playwright.config.js @@ -23,6 +23,15 @@ if (process.env.ADD_EXTRA_PLAYWRIGHT_PROJECT) { }) } +if (process.env.ADD_DUPLICATE_PLAYWRIGHT_PROJECT) { + projects.push({ + name: 'second-chromium', + use: { + ...devices['Desktop Chrome'], + }, + }) +} + const config = { baseURL: process.env.PW_BASE_URL, testDir: process.env.TEST_DIR || './ci-visibility/playwright-tests', diff --git a/integration-tests/playwright/playwright-efd.spec.js b/integration-tests/playwright/playwright-efd.spec.js index cd2058fdeb4..d86f725478e 100644 --- a/integration-tests/playwright/playwright-efd.spec.js +++ b/integration-tests/playwright/playwright-efd.spec.js @@ -22,6 +22,7 @@ const { TEST_RETRY_REASON, TEST_HAS_FAILED_ALL_RETRIES, TEST_NAME, + TEST_BROWSER_NAME, TEST_RETRY_REASON_TYPES, } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_MAJOR } = require('../../version') @@ -29,6 +30,7 @@ const { DD_MAJOR } = require('../../version') const { PLAYWRIGHT_VERSION } = process.env const NUM_RETRIES_EFD = 3 +const PLAYWRIGHT_EFD_GATHER_TIMEOUT = 60000 const latest = 'latest' const oldest = DD_MAJOR >= 6 ? '1.38.0' : '1.18.0' @@ -188,7 +190,7 @@ versions.forEach((version) => { // all but one has been retried assert.strictEqual(totalRetriedTests.length, totalNewTests.length - 2) - }) + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', @@ -200,6 +202,226 @@ versions.forEach((version) => { }, } ) + await Promise.all([ + once(childProcess, 'exit'), + receiverPromise, + ]) + }) + + it('uses the retry count from the matching slow_test_retries bucket', async () => { + receiver.setSettings({ + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': 2, + '10s': 1, + '30s': 0, + }, + faulty_session_threshold: 100, + }, + known_tests_enabled: true, + }) + + receiver.setKnownTests({ + playwright: {}, + }) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + + const slowTests = tests.filter(test => + test.meta[TEST_NAME] === 'efd duration retries slightly slow test' + ) + assert.strictEqual(slowTests.length, 1) + assert.strictEqual(slowTests[0].meta[TEST_IS_NEW], 'true') + assert.strictEqual(slowTests[0].meta[TEST_EARLY_FLAKE_ABORT_REASON], 'slow') + assert.ok(!(TEST_IS_RETRY in slowTests[0].meta)) + }, 45_000) + + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: './ci-visibility/playwright-efd-duration', + PLAYWRIGHT_WORKERS: '2', + }, + } + ) + await Promise.all([ + once(childProcess, 'exit'), + receiverPromise, + ]) + }) + + it('keeps duration retry counts scoped by Playwright project', async () => { + receiver.setSettings({ + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': 2, + '10s': 0, + }, + faulty_session_threshold: 100, + }, + known_tests_enabled: true, + }) + + receiver.setKnownTests({ + playwright: {}, + }) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const projectTests = tests.filter(test => + test.meta[TEST_NAME] === 'efd project duration project scoped test' + ) + const fastProjectTests = projectTests.filter(test => test.meta[TEST_BROWSER_NAME] === 'chromium') + const slowProjectTests = projectTests.filter(test => test.meta[TEST_BROWSER_NAME] === 'second-chromium') + + assert.strictEqual(fastProjectTests.length, 3) + assert.strictEqual( + fastProjectTests.filter(test => test.meta[TEST_IS_RETRY] === 'true').length, + 2 + ) + assert.strictEqual(slowProjectTests.length, 1) + assert.strictEqual(slowProjectTests[0].meta[TEST_IS_NEW], 'true') + assert.strictEqual(slowProjectTests[0].meta[TEST_EARLY_FLAKE_ABORT_REASON], 'slow') + assert.ok(!(TEST_IS_RETRY in slowProjectTests[0].meta)) + }, 60_000) + + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: './ci-visibility/playwright-efd-projects', + ADD_DUPLICATE_PLAYWRIGHT_PROJECT: '1', + PLAYWRIGHT_WORKERS: '2', + }, + } + ) + await Promise.all([ + once(childProcess, 'exit'), + receiverPromise, + ]) + }) + + it('does not treat native repeat-each executions as EFD retries', async () => { + receiver.setSettings({ + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': 0, + }, + faulty_session_threshold: 100, + }, + known_tests_enabled: true, + }) + + receiver.setKnownTests({ + playwright: {}, + }) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const repeatedTests = tests.filter(test => + test.meta[TEST_NAME] === 'efd repeat each native repeat test' + ) + + assert.strictEqual(repeatedTests.length, 3) + for (const repeatedTest of repeatedTests) { + assert.strictEqual(repeatedTest.meta[TEST_IS_NEW], 'true') + assert.ok(!(TEST_IS_RETRY in repeatedTest.meta)) + assert.ok(!(TEST_RETRY_REASON in repeatedTest.meta)) + } + }, 45_000) + + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js --repeat-each=3', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: './ci-visibility/playwright-efd-repeat', + PLAYWRIGHT_WORKERS: '2', + }, + } + ) + + await Promise.all([ + once(childProcess, 'exit'), + receiverPromise, + ]) + }) + + it('keeps duration retry counts scoped by native repeat-each index', async () => { + receiver.setSettings({ + early_flake_detection: { + enabled: true, + slow_test_retries: { + '5s': 2, + '10s': 0, + }, + faulty_session_threshold: 100, + }, + known_tests_enabled: true, + }) + + receiver.setKnownTests({ + playwright: {}, + }) + + const receiverPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/api/v2/citestcycle', (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const repeatedTests = tests.filter(test => + test.meta[TEST_NAME] === 'efd repeat duration repeat-scoped test' + ) + + assert.strictEqual(repeatedTests.length, 4) + for (const repeatedTest of repeatedTests) { + assert.strictEqual(repeatedTest.meta[TEST_IS_NEW], 'true') + } + + const slowRepeatTests = repeatedTests.filter( + test => test.meta[TEST_EARLY_FLAKE_ABORT_REASON] === 'slow' + ) + assert.strictEqual(slowRepeatTests.length, 1) + assert.ok(!(TEST_IS_RETRY in slowRepeatTests[0].meta)) + + const retriedTests = repeatedTests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + assert.strictEqual(retriedTests.length, 2) + for (const retriedTest of retriedTests) { + assert.strictEqual(retriedTest.meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.efd) + } + + const fastOriginalTests = repeatedTests.filter(test => + !(TEST_IS_RETRY in test.meta) && !(TEST_EARLY_FLAKE_ABORT_REASON in test.meta) + ) + assert.strictEqual(fastOriginalTests.length, 1) + }, 60_000) + + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js --repeat-each=2', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: './ci-visibility/playwright-efd-repeat-duration', + PLAYWRIGHT_WORKERS: '1', + }, + } + ) await Promise.all([ once(childProcess, 'exit'), @@ -256,7 +478,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }) + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', @@ -328,7 +550,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }) + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', @@ -379,7 +601,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }, 60000) + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', @@ -448,7 +670,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }) + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', @@ -503,7 +725,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }) + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js', @@ -564,7 +786,7 @@ versions.forEach((version) => { assert.strictEqual(newTests.length, 0) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') assert.strictEqual(retriedTests.length, 0) - }), + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT), ]) }) @@ -627,14 +849,8 @@ versions.forEach((version) => { assert.strictEqual(test.meta[TEST_STATUS], 'fail') }) - // Only the last retry should have TEST_HAS_FAILED_ALL_RETRIES set - const lastRetry = newTests[newTests.length - 1] - assert.strictEqual(lastRetry.meta[TEST_HAS_FAILED_ALL_RETRIES], 'true') - - // Earlier attempts should not have the flag - for (let i = 0; i < newTests.length - 1; i++) { - assert.ok(!(TEST_HAS_FAILED_ALL_RETRIES in newTests[i].meta)) - } + const failedAllRetries = newTests.filter(test => test.meta[TEST_HAS_FAILED_ALL_RETRIES] === 'true') + assert.strictEqual(failedAllRetries.length, 1) // --retries works normally for old flaky tests const oldFlakyTests = tests.filter( @@ -647,7 +863,7 @@ versions.forEach((version) => { assert.strictEqual(passedFlakyTests[0].meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.ext) const failedFlakyTests = oldFlakyTests.filter(test => test.meta[TEST_STATUS] === 'fail') assert.strictEqual(failedFlakyTests.length, 1) - }), + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT), ]) }) @@ -710,14 +926,8 @@ versions.forEach((version) => { assert.strictEqual(test.meta[TEST_STATUS], 'fail') }) - // Only the last retry should have TEST_HAS_FAILED_ALL_RETRIES set - const lastRetry = newTests[newTests.length - 1] - assert.strictEqual(lastRetry.meta[TEST_HAS_FAILED_ALL_RETRIES], 'true') - - // Earlier attempts should not have the flag - for (let i = 0; i < newTests.length - 1; i++) { - assert.ok(!(TEST_HAS_FAILED_ALL_RETRIES in newTests[i].meta)) - } + const failedAllRetries = newTests.filter(test => test.meta[TEST_HAS_FAILED_ALL_RETRIES] === 'true') + assert.strictEqual(failedAllRetries.length, 1) // ATR works normally for old flaky tests const oldFlakyTests = tests.filter( @@ -730,7 +940,7 @@ versions.forEach((version) => { assert.strictEqual(passedFlakyTests[0].meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.atr) const failedFlakyTests = oldFlakyTests.filter(test => test.meta[TEST_STATUS] === 'fail') assert.strictEqual(failedFlakyTests.length, 1) - }), + }, PLAYWRIGHT_EFD_GATHER_TIMEOUT), ]) }) }) diff --git a/integration-tests/playwright/playwright-impacted-tests.spec.js b/integration-tests/playwright/playwright-impacted-tests.spec.js index a5131a51577..d700e42f32b 100644 --- a/integration-tests/playwright/playwright-impacted-tests.spec.js +++ b/integration-tests/playwright/playwright-impacted-tests.spec.js @@ -201,7 +201,7 @@ versions.forEach((version) => { assert.strictEqual(retriedTestNew, isNew ? NUM_RETRIES_EFD * 2 : 0) assert.strictEqual(retriedTestsWithReason, NUM_RETRIES_EFD * 2) } - }, 25000) + }, 60000) const runImpactedTest = async ( { isModified, isEfd = false, isNew = false }, @@ -264,6 +264,7 @@ versions.forEach((version) => { enabled: true, slow_test_retries: { '5s': NUM_RETRIES_EFD, + '10s': NUM_RETRIES_EFD, }, }, known_tests_enabled: true, diff --git a/integration-tests/playwright/playwright-reporting.spec.js b/integration-tests/playwright/playwright-reporting.spec.js index e100bbc4dbe..aad1d65502c 100644 --- a/integration-tests/playwright/playwright-reporting.spec.js +++ b/integration-tests/playwright/playwright-reporting.spec.js @@ -35,7 +35,9 @@ const { DD_CAPABILITIES_FAILED_TEST_REPLAY, TEST_NAME, DD_CAPABILITIES_IMPACTED_TESTS, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -51,6 +53,13 @@ versions.forEach((version) => { if (PLAYWRIGHT_VERSION === 'oldest' && version !== oldest) return if (PLAYWRIGHT_VERSION === 'latest' && version !== latest) return + // TODO: Remove this once we drop suppport for v5 + const contextNewVersions = (...args) => { + if (satisfies(version, '>=1.38.0') || version === 'latest') { + context(...args) + } + } + describe(`playwright@${version}`, function () { let cwd, receiver, childProcess, webAppPort, webAppServer @@ -98,32 +107,127 @@ versions.forEach((version) => { reportMethods.forEach((reportMethod) => { context(`reporting via ${reportMethod}`, () => { - it('tags session and children with _dd.ci.library_configuration_error when settings fail 4xx', async () => { - const envVars = reportMethod === 'agentless' - ? getCiVisAgentlessConfig(receiver.port) - : getCiVisEvpProxyConfig(receiver.port) - receiver.setSettingsResponseCode(404) - 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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') - const testEvent = events.find(event => event.type === 'test') - assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + context('error tags', () => { + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fail 4xx', + async () => { + const envVars = reportMethod === 'agentless' + ? getCiVisAgentlessConfig(receiver.port) + : getCiVisEvpProxyConfig(receiver.port) + receiver.setSettingsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + }) + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...envVars, + PW_BASE_URL: `http://localhost:${webAppPort}`, + DD_TEST_SESSION_NAME: 'my-test-session', + }, + } + ) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) - childProcess = exec( - './node_modules/.bin/playwright test -c playwright.config.js', - { - cwd, - env: { - ...envVars, - PW_BASE_URL: `http://localhost:${webAppPort}`, - DD_TEST_SESSION_NAME: 'my-test-session', - }, - } - ) - await Promise.all([eventsPromise, once(childProcess, 'exit')]) + + // No skippable_tests test: playwright does not request skippable suites (TIA unsupported). + + contextNewVersions('new version requests', () => { + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + const envVars = reportMethod === 'agentless' + ? getCiVisAgentlessConfig(receiver.port) + : getCiVisEvpProxyConfig(receiver.port) + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end').content + assert.strictEqual(testModule.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + }) + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...envVars, + PW_BASE_URL: `http://localhost:${webAppPort}`, + DD_TEST_SESSION_NAME: 'my-test-session', + }, + } + ) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests ' + + 'when request fail', + async () => { + const envVars = reportMethod === 'agentless' + ? getCiVisAgentlessConfig(receiver.port) + : getCiVisEvpProxyConfig(receiver.port) + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + 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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testModule = events.find(event => event.type === 'test_module_end').content + assert.strictEqual(testModule.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual( + testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + }) + childProcess = exec( + './node_modules/.bin/playwright test -c playwright.config.js', + { + cwd, + env: { + ...envVars, + PW_BASE_URL: `http://localhost:${webAppPort}`, + DD_TEST_SESSION_NAME: 'my-test-session', + }, + } + ) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + }) }) it('can run and report tests', (done) => { diff --git a/integration-tests/playwright/playwright-test-management.spec.js b/integration-tests/playwright/playwright-test-management.spec.js index 30a62521ce4..720c99c8a1a 100644 --- a/integration-tests/playwright/playwright-test-management.spec.js +++ b/integration-tests/playwright/playwright-test-management.spec.js @@ -33,6 +33,8 @@ const { DD_MAJOR } = require('../../version') const { PLAYWRIGHT_VERSION } = process.env +const PLAYWRIGHT_TEST_MANAGEMENT_GATHER_TIMEOUT = 60000 + const latest = 'latest' const oldest = DD_MAJOR >= 6 ? '1.38.0' : '1.18.0' const versions = [oldest, latest] @@ -339,7 +341,7 @@ versions.forEach((version) => { assert.strictEqual(passedFlakyTest.length, 1) assert.strictEqual(failedFlakyTest.length, 1) } - }, 30000) + }, PLAYWRIGHT_TEST_MANAGEMENT_GATHER_TIMEOUT) /** * @param {{ @@ -502,7 +504,7 @@ versions.forEach((version) => { 'ATF test that is in known tests should not be tagged as new' ) } - }) + }, PLAYWRIGHT_TEST_MANAGEMENT_GATHER_TIMEOUT) childProcess = exec( './node_modules/.bin/playwright test -c playwright.config.js attempt-to-fix-test.js', @@ -684,7 +686,7 @@ versions.forEach((version) => { assert.ok(!(TEST_MANAGEMENT_IS_DISABLED in test.meta)) } }) - }, 25000) + }, PLAYWRIGHT_TEST_MANAGEMENT_GATHER_TIMEOUT) const runDisableTest = async (isDisabling, extraEnvVars) => { const testAssertionsPromise = getTestAssertions(isDisabling) @@ -818,7 +820,7 @@ versions.forEach((version) => { assert.ok(!(TEST_MANAGEMENT_IS_QUARANTINED in quarantinedTests[0].meta)) assert.ok(!(TEST_MANAGEMENT_ENABLED in testSession.meta)) } - }, 25000) + }, PLAYWRIGHT_TEST_MANAGEMENT_GATHER_TIMEOUT) /** * @param {{ diff --git a/integration-tests/standalone-asm/index.js b/integration-tests/standalone-asm/index.js index e4c021bd119..004322c0af6 100644 --- a/integration-tests/standalone-asm/index.js +++ b/integration-tests/standalone-asm/index.js @@ -4,11 +4,9 @@ const options = { appsec: { enabled: true, }, - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, } diff --git a/integration-tests/vitest/vitest.core.spec.js b/integration-tests/vitest/vitest.core.spec.js index 2808b663681..57e17bcc82e 100644 --- a/integration-tests/vitest/vitest.core.spec.js +++ b/integration-tests/vitest/vitest.core.spec.js @@ -40,7 +40,9 @@ const { TEST_HAS_DYNAMIC_NAME, VITEST_POOL, TEST_IS_TEST_FRAMEWORK_WORKER, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { NODE_MAJOR } = require('../../version') @@ -302,26 +304,102 @@ versions.forEach((version) => { }) context('error tags', () => { - it('tags session and children with _dd.ci.library_configuration_error when settings fails', async () => { - receiver.setSettingsResponseCode(404) - const eventsPromise = receiver - .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') - const testEvent = events.find(event => event.type === 'test') - assert.ok(testEvent, 'should have test event') - assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR], 'true') + it( + 'tags session and children with _dd.ci.library_configuration_error.settings when settings fails 4xx', + async () => { + receiver.setSettingsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS], 'true') + }) + childProcess = exec('./node_modules/.bin/vitest run', { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + }, }) - childProcess = exec('./node_modules/.bin/vitest run', { - cwd, - env: { - ...getCiVisAgentlessConfig(receiver.port), - NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', - }, + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + // No skippable_tests test: vitest does not request skippable suites (TIA unsupported). + + it( + 'tags session and children with _dd.ci.library_configuration_error.known_tests when request fails 4xx', + async () => { + receiver.setSettings({ known_tests_enabled: true }) + receiver.setKnownTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual(testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual(testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual(testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS], 'true') + }) + childProcess = exec('./node_modules/.bin/vitest run', { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + }, + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) + }) + + it( + 'tags session and children with _dd.ci.library_configuration_error.test_management_tests when request fails', + async () => { + receiver.setSettings({ test_management: { enabled: true } }) + receiver.setTestManagementTestsResponseCode(404) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === '/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[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true') + const testModule = events.find(event => event.type === 'test_module_end') + assert.ok(testModule, 'should have test module event') + assert.strictEqual( + testModule.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testSuiteEvent = events.find(event => event.type === 'test_suite_end') + assert.ok(testSuiteEvent, 'should have test suite event') + assert.strictEqual( + testSuiteEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + const testEvent = events.find(event => event.type === 'test') + assert.ok(testEvent, 'should have test event') + assert.strictEqual( + testEvent.content.meta[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS], 'true' + ) + }) + childProcess = exec('./node_modules/.bin/vitest run', { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + }, + }) + await Promise.all([eventsPromise, once(childProcess, 'exit')]) }) - await Promise.all([eventsPromise, once(childProcess, 'exit')]) - }) }) it('sends telemetry with test_session metric when telemetry is enabled', async () => { @@ -809,12 +887,12 @@ versions.forEach((version) => { }) }) - it('uses the retry count from the matching slow_test_retries bucket', (done) => { + it('uses the retry count from the matching slow_test_retries bucket', async () => { receiver.setSettings({ early_flake_detection: { enabled: true, slow_test_retries: { - '5s': 2, + '5s': 1, '10s': 0, }, faulty_session_threshold: 100, @@ -834,10 +912,10 @@ versions.forEach((version) => { const instantTests = tests.filter(test => test.meta[TEST_NAME] === 'early flake detection can retry tests that always pass' ) - assert.strictEqual(instantTests.length, 3) + assert.strictEqual(instantTests.length, 2) assert.strictEqual( instantTests.filter(test => test.meta[TEST_RETRY_REASON] === TEST_RETRY_REASON_TYPES.efd).length, - 2 + 1 ) const slowTests = tests.filter(test => @@ -847,10 +925,10 @@ versions.forEach((version) => { assert.strictEqual(slowTests[0].meta[TEST_IS_NEW], 'true') assert.strictEqual(slowTests[0].meta[TEST_EARLY_FLAKE_ABORT_REASON], 'slow') assert.ok(!(TEST_IS_RETRY in slowTests[0].meta)) - }, 70_000) + }, 55_000) childProcess = exec( - './node_modules/.bin/vitest run', + './node_modules/.bin/vitest run -t "can retry tests that always pass|slightly slow duration bucket test"', { cwd, env: { @@ -862,12 +940,8 @@ versions.forEach((version) => { } ) - childProcess.on('exit', (exitCode) => { - eventsPromise.then(() => { - assert.strictEqual(exitCode, 0) - done() - }).catch(done) - }) + const [[exitCode]] = await Promise.all([once(childProcess, 'exit'), eventsPromise]) + assert.strictEqual(exitCode, 0) }) it('fails if all the attempts fail', (done) => { @@ -992,6 +1066,7 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const testSession = events.find(event => event.type === 'test_session_end').content + assert.ok(!(TEST_EARLY_FLAKE_ENABLED in testSession.meta)) assert.strictEqual(testSession.meta[TEST_EARLY_FLAKE_ABORT_REASON], 'faulty') const tests = events.filter(event => event.type === 'test').map(event => event.content) @@ -1002,6 +1077,9 @@ versions.forEach((version) => { ) // no new tests assert.strictEqual(newTests.length, 0) + + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -1136,6 +1214,7 @@ versions.forEach((version) => { assert.ok(testSessionEnd, 'expected test_session_end event in payloads') const testSessionEvent = testSessionEnd.content assert.strictEqual(testSessionEvent.meta[TEST_STATUS], 'fail') + assert.ok(!(TEST_EARLY_FLAKE_ENABLED in testSessionEvent.meta)) }, 60000) childProcess = exec( diff --git a/package.json b/package.json index c44f8934cf1..a85ebd63c28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.102.0", + "version": "5.103.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", @@ -79,6 +79,7 @@ "test:integration:cypress:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/cypress/${SPEC:-cypress-*}.spec.js\"", "test:integration:debugger": "mocha --timeout 60000 \"integration-tests/debugger/*.spec.js\"", "test:integration:debugger:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/debugger/*.spec.js\"", + "test:integration:electron": "mocha \"integration-tests/electron/*.spec.js\"", "test:integration:esbuild": "mocha --timeout 60000 \"integration-tests/esbuild/*.spec.js\"", "test:integration:esbuild:coverage": "node ./integration-tests/coverage/run-suite.js --timeout 60000 \"integration-tests/esbuild/*.spec.js\"", "test:integration:webpack": "mocha --timeout 60000 \"integration-tests/webpack/*.spec.js\"", @@ -159,20 +160,20 @@ "version.js" ], "dependencies": { - "dc-polyfill": "^0.1.10", + "dc-polyfill": "^0.1.11", "import-in-the-middle": "^3.0.1" }, "optionalDependencies": { "@datadog/libdatadog": "0.9.3", "@datadog/native-appsec": "11.0.1", "@datadog/native-iast-taint-tracking": "4.1.0", - "@datadog/native-metrics": "3.1.1", - "@datadog/openfeature-node-server": "^1.1.2", + "@datadog/native-metrics": "3.1.2", + "@datadog/openfeature-node-server": "1.1.2", "@datadog/pprof": "5.14.1", "@datadog/wasm-js-rewriter": "5.0.1", "@opentelemetry/api": ">=1.0.0 <1.10.0", "@opentelemetry/api-logs": "<1.0.0", - "oxc-parser": "^0.128.0" + "oxc-parser": "^0.129.0" }, "devDependencies": { "@actions/core": "^3.0.1", @@ -187,17 +188,17 @@ "@types/mocha": "^10.0.10", "@types/node": "^18.19.106", "@types/sinon": "^21.0.1", - "axios": "^1.15.2", + "axios": "^1.16.0", "benchmark": "^2.1.4", "body-parser": "^2.2.2", "bun": "1.3.13", "codeowners-audit": "^2.9.0", "eslint": "^9.39.2", - "eslint-plugin-cypress": "^6.4.0", + "eslint-plugin-cypress": "^6.4.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-mocha": "^11.2.0", - "eslint-plugin-n": "^17.23.2", + "eslint-plugin-n": "^18.0.1", "eslint-plugin-promise": "^7.3.0", "eslint-plugin-sonarjs": "^4.0.3", "eslint-plugin-unicorn": "^64.0.0", @@ -213,7 +214,7 @@ "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", "multer": "^2.1.1", - "nock": "^13.5.6", + "nock": "^14.0.15", "node-preload": "^0.2.1", "nyc": "^18.0.0", "octokit": "^5.0.3", @@ -223,11 +224,11 @@ "retry": "^0.13.1", "semifies": "^1.0.0", "semver": "^7.7.2", - "sinon": "^21.1.2", + "sinon": "^22.0.0", "tiktoken": "^1.0.21", "typescript": "^6.0.3", "workerpool": "^10.0.2", - "yaml": "^2.8.3", + "yaml": "^2.8.4", "yarn-deduplicate": "^6.0.2" } } diff --git a/packages/datadog-esbuild/src/utils.js b/packages/datadog-esbuild/src/utils.js index cf2e4704871..3c8e720ccbc 100644 --- a/packages/datadog-esbuild/src/utils.js +++ b/packages/datadog-esbuild/src/utils.js @@ -83,9 +83,9 @@ function getSource (url, { format }) { * * @param {object} moduleData * @param {string} moduleData.path - * @param {boolean} [moduleData.internal = false] + * @param {boolean} [moduleData.internal] * @param {object} moduleData.context - * @param {boolean} [moduleData.excludeDefault = false] + * @param {boolean} [moduleData.excludeDefault] * @returns {Promise} */ async function processModule ({ path, internal = false, context, excludeDefault = false }) { diff --git a/packages/datadog-instrumentations/src/ai.js b/packages/datadog-instrumentations/src/ai.js index 58e1e3b4e72..3cdbd8c4efd 100644 --- a/packages/datadog-instrumentations/src/ai.js +++ b/packages/datadog-instrumentations/src/ai.js @@ -20,7 +20,7 @@ const wrappedModels = new WeakSet() */ function publishToAIGuard (messages) { return new Promise((resolve, reject) => { - aiguardChannel.publish({ messages, resolve, reject }) + aiguardChannel.publish({ messages, integration: 'ai', resolve, reject }) }) } diff --git a/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js b/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js index a73e816e0f4..9e26c66f046 100644 --- a/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +++ b/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js @@ -7,6 +7,7 @@ const { addHook, channel, } = require('./helpers/instrument') +const { cloneMessages } = require('./helpers/kafka') // Create channels for Confluent Kafka JavaScript const channels = { @@ -211,46 +212,62 @@ function instrumentKafkaJS (kafkaJS) { return send.apply(this, arguments) } + const disableHeaderInjection = disabledHeaderWeakSet.has(producer) + + // Hand the underlying client a shallow clone so neither + // injection nor the client's auto-fields (it sets + // `headers: null` on messages without headers) ever + // touch caller-owned objects. With injection disabled the + // clone must not seed `headers: {}` either: brokers that + // reject any header field cannot recover otherwise. + let outgoingPayload = payload + if (payload && Array.isArray(payload.messages)) { + outgoingPayload = { + ...payload, + messages: cloneMessages(payload.messages, !disableHeaderInjection), + } + } + const ctx = { - topic: payload?.topic, - messages: payload?.messages || [], + topic: outgoingPayload?.topic, + messages: outgoingPayload?.messages || [], bootstrapServers: kafka._ddBrokers, - disableHeaderInjection: disabledHeaderWeakSet.has(producer), + disableHeaderInjection, } return channels.producerStart.runStores(ctx, () => { try { - const result = send.apply(this, arguments) + const result = send.call(this, outgoingPayload) result.then((res) => { ctx.result = res channels.producerCommit.publish(ctx) channels.producerFinish.publish(ctx) - }, (err) => { - if (err) { - // Fixes bug where we would inject message headers for kafka brokers - // that don't support headers (version <0.11). On the error, we disable - // header injection. Tnfortunately the error name / type is not more specific. - // This approach is implemented by other tracers as well. - if (err.name === 'KafkaJSError' && err.type === 'ERR_UNKNOWN') { + }, (error) => { + if (error) { + // KafkaJS-compat reports `ERR_UNKNOWN` for brokers + // <0.11 that cannot parse headers. Stop injecting + // for this producer; subsequent sends to the same + // broker succeed. + if (error.name === 'KafkaJSError' && error.type === 'ERR_UNKNOWN') { disabledHeaderWeakSet.add(producer) log.error( // eslint-disable-next-line @stylistic/max-len 'Kafka Broker responded with UNKNOWN_SERVER_ERROR (-1). Please look at broker logs for more information. Tracer message header injection for Kafka is disabled.' ) } - ctx.error = err + ctx.error = error channels.producerError.publish(ctx) } channels.producerFinish.publish(ctx) }) return result - } catch (e) { - ctx.error = e + } catch (error) { + ctx.error = error channels.producerError.publish(ctx) channels.producerFinish.publish(ctx) - throw e + throw error } }) } diff --git a/packages/datadog-instrumentations/src/couchbase.js b/packages/datadog-instrumentations/src/couchbase.js index a10df013c40..8e7159e704e 100644 --- a/packages/datadog-instrumentations/src/couchbase.js +++ b/packages/datadog-instrumentations/src/couchbase.js @@ -1,12 +1,27 @@ 'use strict' -const { errorMonitor } = require('events') +const { tracingChannel } = require('dc-polyfill') + const shimmer = require('../../datadog-shimmer') const { - channel, addHook, } = require('./helpers/instrument') +// One TracingChannel per traced operation, looked up at module init so the +// hot path only does property reads on a stable handle. +const queryCh = tracingChannel('apm:couchbase:query') +const upsertCh = tracingChannel('apm:couchbase:upsert') +const insertCh = tracingChannel('apm:couchbase:insert') +const replaceCh = tracingChannel('apm:couchbase:replace') + +/** @type {Map>} */ +const opChannelByName = new Map([ + ['query', queryCh], + ['upsert', upsertCh], + ['insert', insertCh], + ['replace', replaceCh], +]) + function findCallbackIndex (args, lowerbound = 2) { for (let i = args.length - 1; i >= lowerbound; i--) { if (typeof args[i] === 'function') return i @@ -19,244 +34,78 @@ function getQueryResource (q) { return q && (typeof q === 'string' ? q : q.statement) } -function wrapAllNames (names, action) { - for (const name of names) { - action(name) +// Hand-rolled instead of `tracingChannel.tracePromise`: synchronous +// `res.then(...)` dodges the `Promise.resolve(thenable)` microtask race on +// SDK v3.2.x / v4.0-v4.4 (lazy listener attachment), and external `.on()` +// is forbidden on v4.5.0+ (JSCBC-1301 depromisify). See commit body. +/** + * @param {import('node:diagnostics_channel').TracingChannel} ch + * Pinned per-op channel. + * @param {(...callArgs: unknown[]) => unknown} fn The SDK method being traced. + * @param {object} ctx Mutated to record `result` / `error`. + * @param {object} thisArg + * @param {unknown[]} args Forwarded to `fn` verbatim. + */ +function traceV3 (ch, fn, ctx, thisArg, args) { + if (!ch.start.hasSubscribers) return fn.apply(thisArg, args) + const cbIndex = findCallbackIndex(args, 1) + if (cbIndex >= 0) { + return ch.traceCallback(fn, cbIndex, ctx, thisArg, ...args) } -} - -function wrapCallback (callback, ctx, channelPrefix) { - const callbackStartCh = channel(`${channelPrefix}:callback:start`) - const callbackFinishCh = channel(`${channelPrefix}:callback:finish`) - - const wrapped = callbackStartCh.runStores(ctx, () => { - return function (...args) { - return callbackFinishCh.runStores(ctx, () => { - return callback.apply(this, args) - }) - } - }) - Object.defineProperty(wrapped, '_dd_wrapped', { value: true }) - return wrapped -} - -function wrapQuery (query) { - return function (q, params, callback) { - const cb = arguments[arguments.length - 1] - if (typeof cb === 'function') { - const ctx = {} - arguments[arguments.length - 1] = wrapCallback(cb, ctx, 'apm:couchbase:query') - } - - return query.apply(this, arguments) - } -} - -function wrapCallbackFinish (callback, thisArg, _args, errorCh, finishCh, ctx, channelPrefix) { - const callbackStartCh = channel(`${channelPrefix}:callback:start`) - const callbackFinishCh = channel(`${channelPrefix}:callback:finish`) - - const wrapped = callbackStartCh.runStores(ctx, () => { - return function finish (error, result) { - return callbackFinishCh.runStores(ctx, () => { - if (error) { - ctx.error = error - errorCh.publish(ctx) - } - finishCh.publish(ctx) - return callback.apply(thisArg, [error, result]) - }) - } - }) - Object.defineProperty(wrapped, '_dd_wrapped', { value: true }) - return wrapped -} - -function wrap (prefix, fn) { - const startCh = channel(prefix + ':start') - const finishCh = channel(prefix + ':finish') - const errorCh = channel(prefix + ':error') - - return function (...args) { - if (!startCh.hasSubscribers) { - return fn.apply(this, args) - } - - const callbackIndex = findCallbackIndex(args, 1) - - if (callbackIndex < 0) return fn.apply(this, args) - - const ctx = { bucket: { name: this.name || this._name }, seedNodes: this._dd_hosts } - return startCh.runStores(ctx, () => { - const cb = args[callbackIndex] - - args[callbackIndex] = shimmer.wrapFunction(cb, (cb) => { - return wrapCallbackFinish(cb, this, args, errorCh, finishCh, ctx, prefix) - }) - - try { - return fn.apply(this, args) - } catch (error) { - ctx.error = error - void error.stack // trigger getting the stack at the original throwing point - errorCh.publish(ctx) - - throw error - } - }) - } -} - -// semver >=2 <3 -function wrapMaybeInvoke (_maybeInvoke, channelPrefix) { - return function (fn, args) { - if (!Array.isArray(args)) return _maybeInvoke.apply(this, arguments) - - const callbackIndex = findCallbackIndex(args, 0) - - if (callbackIndex === -1) return _maybeInvoke.apply(this, arguments) - - const callback = args[callbackIndex] - - if (typeof callback === 'function' && !callback._dd_wrapped) { - const ctx = {} - args[callbackIndex] = wrapCallback(callback, ctx, channelPrefix) - } - - return _maybeInvoke.apply(this, arguments) - } -} - -// semver >=3 - -function wrapCBandPromise (fn, name, startData, thisArg, args) { - const startCh = channel(`apm:couchbase:${name}:start`) - const finishCh = channel(`apm:couchbase:${name}:finish`) - const errorCh = channel(`apm:couchbase:${name}:error`) - - if (!startCh.hasSubscribers) return fn.apply(thisArg, args) - - const ctx = startData - return startCh.runStores(ctx, () => { + return ch.start.runStores(ctx, () => { try { - const cbIndex = findCallbackIndex(args, 1) - if (cbIndex >= 0) { - // v3 offers callback or promises event handling - // NOTE: this does not work with v3.2.0-3.2.1 cluster.query, as there is a bug in the couchbase source code - args[cbIndex] = shimmer.wrapFunction(args[cbIndex], (cb) => { - return wrapCallbackFinish(cb, thisArg, args, errorCh, finishCh, ctx, `apm:couchbase:${name}`) - }) - } const res = fn.apply(thisArg, args) - - // semver >=3 will always return promise by default res.then( (result) => { ctx.result = result - finishCh.publish(ctx) + ch.asyncStart.publish(ctx) + ch.asyncEnd.publish(ctx) }, - (err) => { - ctx.error = err - errorCh.publish(ctx) - finishCh.publish(ctx) + (error) => { + ctx.error = error + ch.error.publish(ctx) + ch.asyncStart.publish(ctx) + ch.asyncEnd.publish(ctx) } ) return res - } catch (e) { - void e.stack - ctx.error = e - errorCh.publish(ctx) - throw e + } catch (error) { + ctx.error = error + ch.error.publish(ctx) + throw error + } finally { + ch.end.publish(ctx) } }) } -function wrapWithName (name) { +/** + * @param {string} name Operation name (`upsert`, `insert`, `replace`). + */ +function wrapV3WithName (name) { + const ch = opChannelByName.get(name) return function (operation) { - return function (...args) { // no arguments used by us - return wrapCBandPromise(operation, name, { + return function (...args) { + const ctx = { collection: { name: this._name || '_default' }, bucket: { name: this._scope._bucket._name }, seedNodes: this._dd_connStr, - }, this, args) + } + return traceV3(ch, operation, ctx, this, args) } } } +/** + * @param {(...args: unknown[]) => unknown} query Original `Cluster.prototype.query`. + */ function wrapV3Query (query) { - return function (q) { - const resource = getQueryResource(q) - return wrapCBandPromise(query, 'query', { resource, seedNodes: this._connStr }, this, arguments) + return function (...args) { + const ctx = { resource: getQueryResource(args[0]), seedNodes: this._connStr } + return traceV3(queryCh, query, ctx, this, args) } } -// semver >=2 <3 -addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.12'] }, Bucket => { - shimmer.wrap(Bucket.prototype, '_maybeInvoke', maybeInvoke => { - return wrapMaybeInvoke(maybeInvoke, 'apm:couchbase:bucket:maybeInvoke') - }) - - const startCh = channel('apm:couchbase:query:start') - const finishCh = channel('apm:couchbase:query:finish') - const errorCh = channel('apm:couchbase:query:error') - - shimmer.wrap(Bucket.prototype, 'query', query => wrapQuery(query)) - - shimmer.wrap(Bucket.prototype, '_n1qlReq', _n1qlReq => function (host, q, adhoc, emitter) { - if (!startCh.hasSubscribers) { - return _n1qlReq.apply(this, arguments) - } - - if (!emitter || !emitter.once) return _n1qlReq.apply(this, arguments) - - const n1qlQuery = getQueryResource(q) - - const ctx = { resource: n1qlQuery, bucket: { name: this.name || this._name }, seedNodes: this._dd_hosts } - return startCh.runStores(ctx, () => { - emitter.once('rows', () => { - finishCh.publish(ctx) - }) - - emitter.once(errorMonitor, (error) => { - if (!error) return - ctx.error = error - errorCh.publish(ctx) - finishCh.publish(ctx) - }) - - try { - return _n1qlReq.apply(this, arguments) - } catch (err) { - void err.stack // trigger getting the stack at the original throwing point - ctx.error = err - errorCh.publish(ctx) - - throw err - } - }) - }) - - wrapAllNames(['upsert', 'insert', 'replace', 'append', 'prepend'], name => { - shimmer.wrap(Bucket.prototype, name, fn => wrap(`apm:couchbase:${name}`, fn)) - }) -}) - -addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.12'] }, Cluster => { - shimmer.wrap(Cluster.prototype, '_maybeInvoke', maybeInvoke => { - return wrapMaybeInvoke(maybeInvoke, 'apm:couchbase:cluster:maybeInvoke') - }) - - shimmer.wrap(Cluster.prototype, 'query', query => wrapQuery(query)) - shimmer.wrap(Cluster.prototype, 'openBucket', openBucket => { - return function (...args) { - const bucket = openBucket.apply(this, args) - const hosts = this.dsnObj.hosts - bucket._dd_hosts = hosts.map(hostAndPort => hostAndPort.join(':')).join(',') - return bucket - } - }) -}) - // semver >=3 <3.2.0 addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^3.0.7', '^3.1.3'] }, Bucket => { @@ -271,9 +120,9 @@ addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^3.0.7', '^3.1.3 }) addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['^3.0.7', '^3.1.3'] }, Collection => { - wrapAllNames(['upsert', 'insert', 'replace'], name => { - shimmer.wrap(Collection.prototype, name, wrapWithName(name)) - }) + for (const name of ['upsert', 'insert', 'replace']) { + shimmer.wrap(Collection.prototype, name, wrapV3WithName(name)) + } }) addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^3.0.7', '^3.1.3'] }, Cluster => { @@ -286,9 +135,9 @@ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^3.0.7', '^3.1. addHook({ name: 'couchbase', file: 'dist/collection.js', versions: ['>=3.2.2'] }, collection => { const Collection = collection.Collection - wrapAllNames(['upsert', 'insert', 'replace'], name => { - shimmer.wrap(Collection.prototype, name, wrapWithName(name)) - }) + for (const name of ['upsert', 'insert', 'replace']) { + shimmer.wrap(Collection.prototype, name, wrapV3WithName(name)) + } }) addHook({ name: 'couchbase', file: 'dist/bucket.js', versions: ['>=3.2.2'] }, bucket => { diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 791f5d6b625..2fc45794236 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -653,7 +653,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin const skippableResponse = await getChannelPromise(skippableSuitesCh) errorSkippableRequest = skippableResponse.err - skippableSuites = skippableResponse.skippableSuites + skippableSuites = skippableResponse.skippableSuites ?? [] if (!errorSkippableRequest) { const filteredPickles = isCoordinator diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js new file mode 100644 index 00000000000..86bcff24f09 --- /dev/null +++ b/packages/datadog-instrumentations/src/electron.js @@ -0,0 +1,240 @@ +'use strict' + +const { join } = require('path') +const { wrap } = require('../../datadog-shimmer') +const { addHook, channel, tracingChannel } = require('./helpers/instrument') + +const requestCh = tracingChannel('apm:electron:net:request') +const mainReceiveCh = tracingChannel('apm:electron:ipc:main:receive') +const mainHandleCh = tracingChannel('apm:electron:ipc:main:handle') +const mainSendCh = tracingChannel('apm:electron:ipc:main:send') +const rendererPatchedCh = channel('apm:electron:ipc:renderer:patched') +const rendererReceiveCh = tracingChannel('apm:electron:ipc:renderer:receive') +const rendererSendCh = tracingChannel('apm:electron:ipc:renderer:send') + +const listeners = {} +const handlers = {} + +function createWrapRequest (ch) { + return function wrapRequest (request) { + return function (...args) { + if (!ch.start.hasSubscribers) return request.apply(this, arguments) + + const ctx = { args } + + return ch.start.runStores(ctx, () => { + try { + const req = request.apply(this, ctx.args) + const emit = req.emit + + ctx.req = req + + req.emit = function (eventName, arg) { + /* eslint-disable no-fallthrough */ + switch (eventName) { + case 'response': + ctx.res = arg + ctx.res.on('error', error => { + ctx.error = error + ch.error.publish(ctx) + ch.asyncStart.publish(ctx) + }) + ctx.res.on('end', () => ch.asyncStart.publish(ctx)) + break + case 'error': + ctx.error = arg + ch.error.publish(ctx) + case 'abort': + ch.asyncStart.publish(ctx) + } + + return emit.apply(this, arguments) + } + + return req + } catch (e) { + ctx.error = e + ch.error.publish(ctx) + throw e + } finally { + ch.end.publish(ctx) + } + }) + } + } +} + +function createWrapAddListener (ch, mappings) { + return function wrapAddListener (addListener) { + return function (channel, listener) { + const wrappedListener = (event, ...args) => { + const ctx = { args, channel, event } + + return ch.tracePromise(() => listener.call(this, event, ...args), ctx) + } + + const mapping = mappings[channel] || new WeakMap() + const wrapper = mapping.get(listener) || wrappedListener + + mapping.set(listener, wrapper) + + return addListener.call(this, channel, wrappedListener) + } + } +} + +function createWrapRemoveListener (mappings) { + return function wrapRemoveListener (removeListener) { + return function (channel, listener) { + const mapping = mappings[channel] + + if (mapping) { + const wrapper = mapping.get(listener) + + if (wrapper) { + return removeListener.call(this, channel, wrapper) + } + } + + return removeListener.call(this, channel, listener) + } + } +} + +function createWrapRemoveAllListeners (mappings) { + return function wrapRemoveAllListeners (removeAllListeners) { + return function (channel) { + if (channel) { + delete mappings[channel] + } else { + for (const key of Object.keys(mappings)) delete mappings[key] + } + + return removeAllListeners.call(this, channel) + } + } +} + +function createWrapSend (ch, promise = false) { + const trace = (promise ? ch.tracePromise : ch.traceSync).bind(ch) + + return function wrapSend (send) { + return function (channel, ...args) { + const ctx = { args, channel, self: this } + + return trace(() => send.call(this, channel, ...args), ctx) + } + } +} + +function wrapSendToFrame (send) { + return function (frameId, channel, ...args) { + const ctx = { args, channel, frameId, self: this } + + return mainSendCh.traceSync(() => send.call(this, frameId, channel, ...args), ctx) + } +} + +function wrapBrowserWindow (electron) { + const moduleExports = {} + + class DatadogBrowserWindow extends electron.BrowserWindow { + constructor (options = {}) { + const win = super(options) + + win.webContents.session.registerPreloadScript({ + type: 'frame', // TODO: service-worker + filePath: join(__dirname, 'electron', 'preload.js'), + }) + + // BrowserWindow doesn't support subclassing because it's all native code + // so we return an instance of it instead of the subclass. + return win + } + } + + Object.defineProperty(moduleExports, 'BrowserWindow', { + enumerable: true, + get: () => DatadogBrowserWindow, + configurable: false, + }) + + for (const key of Reflect.ownKeys(electron)) { + const descriptor = Reflect.getOwnPropertyDescriptor(electron, key) + + if (key === 'BrowserWindow') continue + + Object.defineProperty(moduleExports, key, descriptor) + } + + return moduleExports +} + +function wrapWebContents (proto) { + const descriptor = Object.getOwnPropertyDescriptor(proto, 'webContents') + const wrapped = new WeakSet() + const wrapSend = createWrapSend(mainSendCh) + + Object.defineProperty(proto, 'webContents', { + get () { + const webContents = descriptor.get.apply(this) + + if (!wrapped.has(webContents)) { + // wrap(webContents, 'postMessage', wrapSend) + wrap(webContents, 'send', wrapSend) + wrap(webContents, 'sendToFrame', wrapSendToFrame) + + wrapped.add(webContents) + } + + return webContents + }, + }) +} + +addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { + // Electron exports a string in Node and an object in Electron. + if (typeof electron === 'string') return electron + + const { BrowserWindow, ipcMain, ipcRenderer, net } = electron + + if (net) { + // This also covers `fetch` as it uses `request` under the hood. + wrap(net, 'request', createWrapRequest(requestCh)) + } + + if (ipcRenderer) { + wrap(ipcRenderer, 'invoke', createWrapSend(rendererSendCh, true)) + // wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh)) + wrap(ipcRenderer, 'send', createWrapSend(rendererSendCh)) + wrap(ipcRenderer, 'sendSync', createWrapSend(rendererSendCh)) + wrap(ipcRenderer, 'sendToHost', createWrapSend(rendererSendCh)) + + wrap(ipcRenderer, 'addListener', createWrapAddListener(rendererReceiveCh, listeners)) + wrap(ipcRenderer, 'off', createWrapRemoveListener(listeners)) + wrap(ipcRenderer, 'on', createWrapAddListener(rendererReceiveCh, listeners)) + wrap(ipcRenderer, 'once', createWrapAddListener(rendererReceiveCh, listeners)) + wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners)) + wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + + ipcRenderer.send('datadog:apm:renderer:patched') + } else { + wrap(ipcMain, 'addListener', createWrapAddListener(mainReceiveCh, listeners)) + wrap(ipcMain, 'handle', createWrapAddListener(mainHandleCh, handlers)) + wrap(ipcMain, 'handleOnce', createWrapAddListener(mainHandleCh, handlers)) + wrap(ipcMain, 'off', createWrapRemoveListener(listeners)) + wrap(ipcMain, 'on', createWrapAddListener(mainReceiveCh, listeners)) + wrap(ipcMain, 'once', createWrapAddListener(mainReceiveCh, listeners)) + wrap(ipcMain, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers)) + wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners)) + + ipcMain.once('datadog:apm:renderer:patched', event => rendererPatchedCh.publish(event)) + + wrapWebContents(BrowserWindow.prototype) + + electron = wrapBrowserWindow(electron) + } + + return electron +}) diff --git a/packages/datadog-instrumentations/src/electron/preload.js b/packages/datadog-instrumentations/src/electron/preload.js new file mode 100644 index 00000000000..df62472e9f5 --- /dev/null +++ b/packages/datadog-instrumentations/src/electron/preload.js @@ -0,0 +1,42 @@ +'use strict' + +// eslint-disable-next-line n/no-missing-require +const { contextBridge, ipcRenderer } = require('electron') + +const BRIDGE_CHANNEL = 'datadog:bridge-send' +const CONFIG_CHANNEL = 'datadog:bridge-config' + +// Privacy levels matching @datadog/browser-core DefaultPrivacyLevel +const MASK = 'mask' + +const config = ipcRenderer.sendSync(CONFIG_CHANNEL) + +const defaultPrivacyLevel = config?.defaultPrivacyLevel ?? MASK +const configuredHosts = config?.allowedWebViewHosts ?? [] +// eslint-disable-next-line no-undef +const allowedHosts = [...new Set([location.hostname, ...configuredHosts])] + +const bridge = { + getCapabilities () { + return '[]' + }, + getPrivacyLevel () { + return defaultPrivacyLevel + }, + getAllowedWebViewHosts () { + return JSON.stringify(allowedHosts) + }, + send (msg) { + ipcRenderer.send(BRIDGE_CHANNEL, msg) + }, +} + +// Support both contextIsolation enabled (default) and disabled + +window.DatadogEventBridge = bridge + +try { + contextBridge.exposeInMainWorld('DatadogEventBridge', bridge) +} catch { + // exposeInMainWorld throws when contextIsolation is disabled +} diff --git a/packages/datadog-instrumentations/src/fetch.js b/packages/datadog-instrumentations/src/fetch.js index 8848a1437bc..23bda2b60ad 100644 --- a/packages/datadog-instrumentations/src/fetch.js +++ b/packages/datadog-instrumentations/src/fetch.js @@ -5,10 +5,10 @@ const { IS_SERVERLESS } = require('../../dd-trace/src/serverless') if (globalThis.fetch) { const globalFetch = globalThis.fetch - let fetch = (input, init) => { + let wrappedFetch = (input, init) => { wrapRealFetch() - return fetch(input, init) + return wrappedFetch(input, init) } function wrapRealFetch () { @@ -20,14 +20,14 @@ if (globalThis.fetch) { channel('dd-trace:instrumentation:load').publish({ name: 'global:fetch' }) }) - fetch = wrapFetch(globalFetch) + wrappedFetch = wrapFetch(globalFetch) } if (!IS_SERVERLESS) { wrapRealFetch() } - globalThis.fetch = function value (input, init) { - return fetch(input, init) + globalThis.fetch = function fetch (input, init) { + return wrappedFetch(input, init) } } diff --git a/packages/datadog-instrumentations/src/graphql.js b/packages/datadog-instrumentations/src/graphql.js index c48dd2f60a5..11423b4c41b 100644 --- a/packages/datadog-instrumentations/src/graphql.js +++ b/packages/datadog-instrumentations/src/graphql.js @@ -169,7 +169,7 @@ function wrapExecute (execute) { const ctx = { operation, args, - docSource: documentSources.get(document), + docSource: source, source, fields: Object.create(null), abortController: new AbortController(), @@ -257,13 +257,17 @@ function callInAsyncScope (fn, thisArg, args, abortController, cb) { } function pathToArray (path) { - const flattened = [] - let curr = path - while (curr) { - flattened.push(curr.key) - curr = curr.prev + let length = 0 + for (let curr = path; curr; curr = curr.prev) { + length += 1 } - return flattened.reverse() + + const flattened = new Array(length) + let index = length + for (let curr = path; curr; curr = curr.prev) { + flattened[--index] = curr.key + } + return flattened } function assertField (rootCtx, info, args) { @@ -295,9 +299,7 @@ function wrapFields (type) { patchedTypes.add(type) - for (const key of Object.keys(type._fields)) { - const field = type._fields[key] - + for (const field of Object.values(type._fields)) { wrapFieldResolve(field) wrapFieldType(field) } @@ -321,8 +323,7 @@ function wrapFieldType (field) { } function finishResolvers ({ fields }) { - for (const key of Object.keys(fields).reverse()) { - const field = fields[key] + for (const field of Object.values(fields)) { field.ctx.finishTime = field.finishTime field.ctx.field = field if (field.error) { diff --git a/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js b/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js index 7f12ad8022d..11d4f0b88fa 100644 --- a/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +++ b/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js @@ -19,7 +19,7 @@ const { channel } = require('./instrument') * * @param {string} prefix * @param {object} [options] - * @param {boolean} [options.captureResult=false] set `ctx.result` to the callback's first + * @param {boolean} [options.captureResult] set `ctx.result` to the callback's first * non-error argument before publishing `:finish`. Plugins that tag spans from the call's * return value (e.g. the DNS lookup plugin) rely on this. * @returns {(buildContext: (thisArg: unknown, args: IArguments) => object | undefined) => diff --git a/packages/datadog-instrumentations/src/helpers/hook.js b/packages/datadog-instrumentations/src/helpers/hook.js index b5db3a487ff..36a3fb17332 100644 --- a/packages/datadog-instrumentations/src/helpers/hook.js +++ b/packages/datadog-instrumentations/src/helpers/hook.js @@ -16,7 +16,10 @@ function getVersion (moduleBaseDir) { return requirePackageJson(moduleBaseDir, /** @type {import('module').Module} */ (module)).version } - return process.version + // In a packaged Electron binary, built-in modules (like 'electron', 'electron/main') have no + // moduleBaseDir. Use the Electron version for version checks when available, otherwise fall back + // to the Node.js version. + return process.versions?.electron ?? process.version } /** diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 3f67d768d63..e67bdafc922 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -68,6 +68,7 @@ module.exports = { cypress: () => require('../cypress'), 'dd-trace-api': () => require('../dd-trace-api'), elasticsearch: () => require('../elasticsearch'), + electron: () => require('../electron'), express: () => require('../express'), 'express-mongo-sanitize': () => require('../express-mongo-sanitize'), 'express-session': () => require('../express-session'), diff --git a/packages/datadog-instrumentations/src/helpers/instrument.js b/packages/datadog-instrumentations/src/helpers/instrument.js index 2695807757c..332586f0212 100644 --- a/packages/datadog-instrumentations/src/helpers/instrument.js +++ b/packages/datadog-instrumentations/src/helpers/instrument.js @@ -55,9 +55,9 @@ exports.getHooks = function getHooks (names) { * @param {object} args * @param {string} args.name module name * @param {string[]} [args.versions] array of semver range strings - * @param {string} [args.file='index.js'] path to file within package to instrument + * @param {string} [args.file] path to file within package to instrument. Defaults to 'index.js'. * @param {string} [args.filePattern] pattern to match files within package to instrument - * @param {boolean} [args.patchDefault=true] whether to patch the default export + * @param {boolean} [args.patchDefault] whether to patch the default export. Defaults to true. * @param {(moduleExports: unknown, version: string, isIitm?: boolean) => unknown} [hook] Patches module exports */ exports.addHook = function addHook ({ name, versions, file, filePattern, patchDefault }, hook) { diff --git a/packages/datadog-instrumentations/src/helpers/kafka.js b/packages/datadog-instrumentations/src/helpers/kafka.js new file mode 100644 index 00000000000..1a1f7390773 --- /dev/null +++ b/packages/datadog-instrumentations/src/helpers/kafka.js @@ -0,0 +1,41 @@ +'use strict' + +// Side-table mapping a kafkajs producer/consumer to the cluster captured at +// creation time. The boundary uses it to read `cluster.brokerPool` lazily on +// first send/consume instead of opening a parallel admin connection. A +// WeakMap keeps the kafkajs object itself untouched: no Symbol-keyed +// property to leak through `Reflect.ownKeys`, no string-keyed underscore for +// user serializers to pick up, and the entry drops as soon as the producer +// is GC'd. +const clientToCluster = new WeakMap() + +/** + * Shallow-clone each message and its headers so the boundary, kafkajs, and + * the user never share the same nested objects. With `ensureHeaders` true + * (header injection enabled) messages without `headers` get an empty object + * the producer plugin can inject into; with it false (broker rejected + * headers) the absence of `headers` is preserved so brokers that fail on any + * header field can recover. + * + * @param {Array} messages + * @param {boolean} ensureHeaders + */ +function cloneMessages (messages, ensureHeaders) { + const result = new Array(messages.length) + for (let i = 0; i < messages.length; i++) { + const message = messages[i] + if (message === null || typeof message !== 'object') { + result[i] = message + } else if (message.headers) { + result[i] = { ...message, headers: { ...message.headers } } + } else { + result[i] = ensureHeaders ? { ...message, headers: {} } : { ...message } + } + } + return result +} + +module.exports = { + clientToCluster, + cloneMessages, +} diff --git a/packages/datadog-instrumentations/src/ioredis.js b/packages/datadog-instrumentations/src/ioredis.js index e2057950d76..789e942c6b5 100644 --- a/packages/datadog-instrumentations/src/ioredis.js +++ b/packages/datadog-instrumentations/src/ioredis.js @@ -10,6 +10,8 @@ const startCh = channel('apm:ioredis:command:start') const finishCh = channel('apm:ioredis:command:finish') const errorCh = channel('apm:ioredis:command:error') +const connectionOptionsCache = new WeakMap() + function wrapRedis (Redis) { shimmer.wrap(Redis.prototype, 'sendCommand', sendCommand => function (command, stream) { if (!startCh.hasSubscribers) return sendCommand.apply(this, arguments) @@ -17,21 +19,23 @@ function wrapRedis (Redis) { if (!command || !command.promise) return sendCommand.apply(this, arguments) const options = this.options || {} - const connectionName = options.connectionName - const db = options.db - const connectionOptions = { host: options.host, port: options.port } - - const ctx = { db, command: command.name, args: command.args, connectionOptions, connectionName } + let connectionOptions = connectionOptionsCache.get(this) + if (connectionOptions === undefined) { + connectionOptions = { host: options.host, port: options.port } + connectionOptionsCache.set(this, connectionOptions) + } + + const ctx = { + db: options.db, + command: command.name, + args: command.args, + connectionOptions, + connectionName: options.connectionName, + } return startCh.runStores(ctx, () => { command.promise.then(() => finish(finishCh, errorCh, ctx), err => finish(finishCh, errorCh, ctx, err)) - try { - return sendCommand.apply(this, arguments) - } catch (err) { - errorCh.publish(err) - - throw err - } + return sendCommand.apply(this, arguments) }) }) return Redis diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 2fcde1ad59c..806f9afa3ff 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -3,10 +3,13 @@ // Capture real timers at module load time, before any test can install fake timers. const realSetTimeout = setTimeout +const { readFileSync } = require('node:fs') +const { builtinModules } = require('node:module') const path = require('path') const satisfies = require('../../../vendor/dist/semifies') const { DD_MAJOR } = require('../../../version') const shimmer = require('../../datadog-shimmer') +const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper') const log = require('../../dd-trace/src/log') const { getCoveredFilenamesFromCoverage, @@ -74,6 +77,7 @@ const CHILD_MESSAGE_CALL = 1 // Maximum time we'll wait for the tracer to flush const FLUSH_TIMEOUT = 10_000 +const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID') // https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54 const RETRY_TIMES = Symbol.for('RETRY_TIMES') @@ -101,6 +105,8 @@ let testManagementTests = {} let testManagementAttemptToFixRetries = 0 let isImpactedTestsEnabled = false let modifiedFiles = {} +let activeTestSuiteAbsolutePath +let isConsoleErrorWrapped = false const testContexts = new WeakMap() const originalTestFns = new WeakMap() @@ -124,7 +130,12 @@ const efdNewTestCandidates = new Set() // Tests that are genuinely new (not in known tests list). const newTests = new Set() const testSuiteAbsolutePathsWithFastCheck = new Set() +const testSuiteFastCheckUsage = new Map() const testSuiteJestObjects = new Map() +const wrappedJestGlobals = new WeakSet() +const wrappedJestObjects = new WeakSet() +const wrappedWorkerInitializers = new WeakSet() +const publishedRuntimeReferenceErrors = new WeakMap() const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200 const ATR_RETRY_SUPPRESSION_FLAG = '_ddDisableAtrRetry' @@ -303,6 +314,26 @@ function getAttemptToFixExecutionsFromJestResults (result) { return executions } +function wrapConsoleErrorForJestReferenceErrors () { + if (isConsoleErrorWrapped) return + + isConsoleErrorWrapped = true + // eslint-disable-next-line no-console + const originalConsoleError = console.error + // eslint-disable-next-line no-console + console.error = function () { + const [message] = arguments + if ( + typeof message === 'string' && + message.includes('Jest environment has been torn down') && + activeTestSuiteAbsolutePath + ) { + publishRuntimeReferenceError({ _testPath: activeTestSuiteAbsolutePath }, message) + } + return originalConsoleError.apply(this, arguments) + } +} + function getWrappedEnvironment (BaseEnvironment, jestVersion) { return class DatadogEnvironment extends BaseEnvironment { constructor (config, context) { @@ -314,6 +345,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { this.global._ddtrace = global._ddtrace this.hasSnapshotTests = undefined this.testSuiteAbsolutePath = context.testPath + activeTestSuiteAbsolutePath = this.testSuiteAbsolutePath + wrapConsoleErrorForJestReferenceErrors() + this.globalConfig = config.globalConfig this.displayName = config.projectConfig?.displayName?.name || config.displayName this.testEnvironmentOptions = getTestEnvironmentOptions(config) @@ -423,6 +457,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { */ resetMockState () { try { + if (this.moduleMocker?.clearAllMocks) { + this.moduleMocker.clearAllMocks() + return + } const jestObject = testSuiteJestObjects.get(this.testSuiteAbsolutePath) if (jestObject?.clearAllMocks) { jestObject.clearAllMocks() @@ -504,7 +542,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } getShouldStripSeedFromTestName () { - return testSuiteAbsolutePathsWithFastCheck.has(this.testSuiteAbsolutePath) + return doesTestSuiteUseFastCheck(this.testSuiteAbsolutePath) } // At the `add_test` event we don't have the test object yet, so we can't use it @@ -843,8 +881,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { const willBeRetriedByFailedTestReplay = numRetries > 0 && numTestExecutions - 1 < numRetries const mightHitBreakpoint = this.isDiEnabled && numTestExecutions >= 2 - // For quarantined tests, suppress errors so Jest doesn't count them as failures. - // This prevents --bail from stopping the test run on quarantined test failures. + // For quarantined tests, track failures so the session can be marked as passing later, + // and suppress errors so Jest does not mark the test suite as failing. // The actual status ('fail') is already captured above for dd-trace reporting. // Only suppress on the final execution — not when ATR/EFD/ATF will retry the test. if (!event.test?.[ATR_RETRY_SUPPRESSION_FLAG] && !willBeRetriedByFailedTestReplay) { @@ -1055,7 +1093,19 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } } - return super.teardown() + const clearActiveTestSuite = () => { + realSetTimeout(() => { + if (activeTestSuiteAbsolutePath === this.testSuiteAbsolutePath) { + activeTestSuiteAbsolutePath = undefined + } + }, 0) + } + const result = super.teardown() + if (result?.then) { + return result.finally(clearActiveTestSuite) + } + clearActiveTestSuite() + return result } } } @@ -1492,7 +1542,8 @@ function getCliWrapper (isNewJestVersion) { const timeoutPromise = new Promise((resolve) => { timeoutId = realSetTimeout(() => { resolve('timeout') - }, FLUSH_TIMEOUT).unref() + }, FLUSH_TIMEOUT) + timeoutId.unref?.() }) testSessionFinishCh.publish({ @@ -1557,6 +1608,33 @@ function coverageReporterWrapper (coverageReporter) { return coverageReporter } +function shouldWaitForTestSuiteFinish (environment) { + return isJestWorker && environment.globalConfig?.workerIdleMemoryLimit !== undefined +} + +function publishTestSuiteFinish (payload, waitForFinish) { + if (!testSuiteFinishCh.hasSubscribers) return + + if (!waitForFinish) { + testSuiteFinishCh.publish(payload) + return + } + + return new Promise(resolve => { + testSuiteFinishCh.publish({ + ...payload, + waitForFinish, + onDone: resolve, + }) + }) +} + +function cleanupTestSuiteState (testSuiteAbsolutePath) { + testSuiteMockedFiles.delete(testSuiteAbsolutePath) + testSuiteFastCheckUsage.delete(testSuiteAbsolutePath) + testSuiteJestObjects.delete(testSuiteAbsolutePath) +} + addHook({ name: '@jest/core', file: 'build/TestScheduler.js', @@ -1675,7 +1753,7 @@ function jestAdapterWrapper (jestAdapter, jestVersion) { const getFilesWithPath = (files) => files.map(file => getTestSuitePath(file, root)) const coverageFiles = getFilesWithPath(getCoveredFilenamesFromCoverage(environment.global.__coverage__)) - const mockedFiles = getFilesWithPath(testSuiteMockedFiles.get(environment.testSuiteAbsolutePath) || []) + const mockedFiles = getFilesWithPath(getMockedFiles(environment.testSuiteAbsolutePath)) testSuiteCodeCoverageCh.publish({ coverageFiles, @@ -1684,19 +1762,51 @@ function jestAdapterWrapper (jestAdapter, jestVersion) { testSuiteAbsolutePath: environment.testSuiteAbsolutePath, }) } - testSuiteFinishCh.publish({ status, errorMessage, testSuiteAbsolutePath: environment.testSuiteAbsolutePath }) + const waitForFinish = shouldWaitForTestSuiteFinish(environment) + const finishPayload = { + status, + errorMessage, + testSuiteAbsolutePath: environment.testSuiteAbsolutePath, + } + if (waitForFinish) { + const finishPromise = publishTestSuiteFinish(finishPayload, waitForFinish) + if (finishPromise) { + return finishPromise.then(() => { + // Cleanup per-suite state to avoid memory leaks + cleanupTestSuiteState(environment.testSuiteAbsolutePath) + + return suiteResults + }) + } + } + publishTestSuiteFinish(finishPayload, waitForFinish) // Cleanup per-suite state to avoid memory leaks - testSuiteMockedFiles.delete(environment.testSuiteAbsolutePath) - testSuiteJestObjects.delete(environment.testSuiteAbsolutePath) + cleanupTestSuiteState(environment.testSuiteAbsolutePath) return suiteResults }).catch(error => { - testSuiteFinishCh.publish({ status: 'fail', error, testSuiteAbsolutePath: environment.testSuiteAbsolutePath }) + const waitForFinish = shouldWaitForTestSuiteFinish(environment) + const finishPayload = { + status: 'fail', + error, + testSuiteAbsolutePath: environment.testSuiteAbsolutePath, + } + if (waitForFinish) { + const finishPromise = publishTestSuiteFinish(finishPayload, waitForFinish) + if (finishPromise) { + return finishPromise.then(() => { + // Cleanup per-suite state to avoid memory leaks + cleanupTestSuiteState(environment.testSuiteAbsolutePath) + + throw error + }) + } + } + publishTestSuiteFinish(finishPayload, waitForFinish) // Cleanup per-suite state to avoid memory leaks - testSuiteMockedFiles.delete(environment.testSuiteAbsolutePath) - testSuiteJestObjects.delete(environment.testSuiteAbsolutePath) + cleanupTestSuiteState(environment.testSuiteAbsolutePath) throw error }) @@ -1794,6 +1904,7 @@ const DD_TEST_ENVIRONMENT_OPTION_KEYS = [ '_ddRepositoryRoot', '_ddIsFlakyTestRetriesEnabled', '_ddFlakyTestRetriesCount', + '_ddItrSkippingEnabledTags', '_ddIsDiEnabled', '_ddIsKnownTestsEnabled', '_ddIsTestManagementTestsEnabled', @@ -1904,39 +2015,195 @@ const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = new Set([ 'winston', ]) +function recordMockedFile (suiteFilePath, moduleName) { + if (!suiteFilePath || typeof moduleName !== 'string') return + + const existingMockedFiles = testSuiteMockedFiles.get(suiteFilePath) || [] + const suiteDir = path.dirname(suiteFilePath) + const mockPath = path.resolve(suiteDir, moduleName) + existingMockedFiles.push(mockPath) + testSuiteMockedFiles.set(suiteFilePath, existingMockedFiles) +} + +const JEST_STATIC_MOCK_CALL_RE = /\bjest\.(?:mock|doMock|unstable_mockModule)\(\s*(['"`])([^'"`]+)\1/g + +function getStaticMockedFiles (suiteFilePath) { + if (!suiteFilePath) return [] + + const mockedFiles = [] + try { + const source = readFileSync(suiteFilePath, 'utf8') + let match + JEST_STATIC_MOCK_CALL_RE.lastIndex = 0 + while ((match = JEST_STATIC_MOCK_CALL_RE.exec(source)) !== null) { + mockedFiles.push(path.resolve(path.dirname(suiteFilePath), match[2])) + } + } catch { + // ignore errors + } + + return mockedFiles +} + +function getMockedFiles (suiteFilePath) { + const mockedFiles = testSuiteMockedFiles.get(suiteFilePath) + if (mockedFiles?.length) { + return mockedFiles + } + return getStaticMockedFiles(suiteFilePath) +} + +function wrapJestObject (jestObject, suiteFilePath) { + if (!jestObject || !suiteFilePath || wrappedJestObjects.has(jestObject)) return + + testSuiteJestObjects.set(suiteFilePath, jestObject) + wrappedJestObjects.add(jestObject) + + shimmer.wrap(jestObject, 'mock', mock => function (moduleName) { + // If the library is mocked with `jest.mock`, we don't want to bypass jest's own require engine + if (LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) { + LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName) + } + recordMockedFile(suiteFilePath, moduleName) + return mock.apply(this, arguments) + }) +} + +function wrapJestGlobalsForRuntime (runtime) { + const jestGlobals = runtime?.jestGlobals + if (!jestGlobals || wrappedJestGlobals.has(jestGlobals) || typeof jestGlobals.jestObjectFor !== 'function') { + return + } + + wrappedJestGlobals.add(jestGlobals) + shimmer.wrap(jestGlobals, 'jestObjectFor', jestObjectFor => function (from) { + const jestObject = jestObjectFor.apply(this, arguments) + wrapJestObject(jestObject, from) + return jestObject + }) +} + +function recordFastCheckUsage (runtime, from, moduleName) { + if (moduleName !== '@fast-check/jest') return + + if (from) { + testSuiteAbsolutePathsWithFastCheck.add(from) + testSuiteFastCheckUsage.set(from, true) + } + if (runtime?._testPath) { + testSuiteAbsolutePathsWithFastCheck.add(runtime._testPath) + testSuiteFastCheckUsage.set(runtime._testPath, true) + } +} + +function doesTestSuiteUseFastCheck (testSuiteAbsolutePath) { + if (!testSuiteAbsolutePath) return false + if (testSuiteFastCheckUsage.has(testSuiteAbsolutePath)) { + return testSuiteFastCheckUsage.get(testSuiteAbsolutePath) + } + + try { + const usesFastCheck = readFileSync(testSuiteAbsolutePath, 'utf8').includes('@fast-check/jest') + testSuiteFastCheckUsage.set(testSuiteAbsolutePath, usesFastCheck) + if (usesFastCheck) { + testSuiteAbsolutePathsWithFastCheck.add(testSuiteAbsolutePath) + } + return usesFastCheck + } catch { + testSuiteFastCheckUsage.set(testSuiteAbsolutePath, false) + return false + } +} + +function getLastLoggedReferenceError (runtime) { + const loggedReferenceErrors = runtime?.loggedReferenceErrors + if (!loggedReferenceErrors?.size) return + return [...loggedReferenceErrors].pop() +} + +function publishRuntimeReferenceError (runtime, errorMessage) { + if (!errorMessage || !runtime?._testPath) return + + let publishedErrors = publishedRuntimeReferenceErrors.get(runtime) + if (!publishedErrors) { + publishedErrors = new Set() + publishedRuntimeReferenceErrors.set(runtime, publishedErrors) + } + if (publishedErrors.has(errorMessage)) return + + publishedErrors.add(errorMessage) + testSuiteErrorCh.publish({ + errorMessage, + testSuiteAbsolutePath: runtime._testPath, + }) +} + +function isBetweenTestsReferenceError (error) { + return error?.name === 'ReferenceError' && + typeof error.message === 'string' && + error.message.includes('outside of the scope of the test code') +} + +function reportBetweenTestsReferenceError (runtime, moduleName, originalErrorMessage) { + if (typeof moduleName !== 'string') return false + + const fallbackErrorMessage = moduleName.startsWith('node:') || builtinModules.includes(moduleName) + ? 'You are trying to access a Node.js module outside of the scope of the test code.' + : 'You are trying to `require` a file after the Jest environment has been torn down.' + const errorMessage = originalErrorMessage || fallbackErrorMessage + + if (typeof runtime._logFormattedReferenceError === 'function') { + runtime._logFormattedReferenceError(errorMessage) + } + publishRuntimeReferenceError(runtime, getLastLoggedReferenceError(runtime) || errorMessage) + process.exitCode = 1 + return true +} + +function requireOutsideJestRequireEngine (runtime, moduleName) { + if (typeof runtime._requireCoreModule === 'function') { + return runtime._requireCoreModule(moduleName) + } + return require(moduleName) +} + +function formatDefaultStackTrace (error, structuredStackTrace) { + const errorString = Error.prototype.toString.call(error) + if (structuredStackTrace.length === 0) return errorString + + return `${errorString}\n at ${structuredStackTrace.join('\n at ')}` +} + addHook({ name: 'jest-runtime', versions: [MINIMUM_JEST_VERSION], }, (runtimePackage) => { const Runtime = runtimePackage.default ?? runtimePackage - shimmer.wrap(Runtime.prototype, '_createJestObjectFor', _createJestObjectFor => function (from) { - const result = _createJestObjectFor.apply(this, arguments) - const suiteFilePath = this._testPath || from + if (typeof Runtime.prototype._createJestObjectFor === 'function') { + shimmer.wrap(Runtime.prototype, '_createJestObjectFor', _createJestObjectFor => function (from) { + const result = _createJestObjectFor.apply(this, arguments) + const suiteFilePath = this._testPath || from - // Store the jest object so we can access it later for resetting mock state - if (suiteFilePath) { - testSuiteJestObjects.set(suiteFilePath, result) - } + wrapJestObject(result, suiteFilePath) + return result + }) + } - shimmer.wrap(result, 'mock', mock => function (moduleName) { - // If the library is mocked with `jest.mock`, we don't want to bypass jest's own require engine - if (LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) { - LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName) - } - if (suiteFilePath) { - const existingMockedFiles = testSuiteMockedFiles.get(suiteFilePath) || [] - const suiteDir = path.dirname(suiteFilePath) - const mockPath = path.resolve(suiteDir, moduleName) - existingMockedFiles.push(mockPath) - testSuiteMockedFiles.set(suiteFilePath, existingMockedFiles) + shimmer.wrap(Runtime.prototype, 'requireModule', requireModule => function (from, moduleName) { + wrapJestGlobalsForRuntime(this) + try { + return requireModule.apply(this, arguments) + } catch (error) { + if (isBetweenTestsReferenceError(error)) { + reportBetweenTestsReferenceError(this, moduleName, error.message) } - return mock.apply(this, arguments) - }) - return result + throw error + } }) shimmer.wrap(Runtime.prototype, 'requireModuleOrMock', requireModuleOrMock => function (from, moduleName) { + wrapJestGlobalsForRuntime(this) // `requireModuleOrMock` may log errors to the console. If we don't remove ourselves // from the stack trace, the user might see a useless stack trace rather than the error // that `jest` tries to show. @@ -1945,32 +2212,33 @@ addHook({ const filteredStackTrace = structuredStackTrace .filter(callSite => !callSite.getFileName()?.includes('datadog-instrumentations/src/jest.js')) - return originalPrepareStackTrace(error, filteredStackTrace) + if (typeof originalPrepareStackTrace === 'function') { + return originalPrepareStackTrace(error, filteredStackTrace) + } + return formatDefaultStackTrace(error, filteredStackTrace) } try { // TODO: do this for every library that we instrument if (LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) { // To bypass jest's own require engine - return this._requireCoreModule(moduleName) + return requireOutsideJestRequireEngine(this, moduleName) } // This means that `@fast-check/jest` is used in the test file. - if (moduleName === '@fast-check/jest') { - testSuiteAbsolutePathsWithFastCheck.add(this._testPath) + recordFastCheckUsage(this, from, moduleName) + let returnedValue + try { + returnedValue = requireModuleOrMock.apply(this, arguments) + } catch (error) { + if (isBetweenTestsReferenceError(error)) { + reportBetweenTestsReferenceError(this, moduleName, error.message) + } + throw error } - const returnedValue = requireModuleOrMock.apply(this, arguments) if (process.exitCode === 1) { - if (this.loggedReferenceErrors?.size > 0) { - const errorMessage = [...this.loggedReferenceErrors][0] - testSuiteErrorCh.publish({ - errorMessage, - testSuiteAbsolutePath: this._testPath, - }) - } else { - testSuiteErrorCh.publish({ - errorMessage: 'An error occurred while importing a module', - testSuiteAbsolutePath: this._testPath, - }) - } + publishRuntimeReferenceError( + this, + getLastLoggedReferenceError(this) || 'An error occurred while importing a module' + ) } return returnedValue } finally { @@ -1979,6 +2247,27 @@ addHook({ } }) + if (Runtime.prototype._logFormattedReferenceError) { + shimmer.wrap(Runtime.prototype, '_logFormattedReferenceError', logFormattedReferenceError => function () { + // eslint-disable-next-line no-console + const originalConsoleError = console.error + let loggedReferenceError + // eslint-disable-next-line no-console + console.error = function () { + loggedReferenceError = arguments[0] + return originalConsoleError.apply(this, arguments) + } + try { + const result = logFormattedReferenceError.apply(this, arguments) + publishRuntimeReferenceError(this, getLastLoggedReferenceError(this) || loggedReferenceError) + return result + } finally { + // eslint-disable-next-line no-console + console.error = originalConsoleError + } + }) + } + return runtimePackage }) @@ -2065,11 +2354,23 @@ function wrapWorkerChannel (worker) { shimmer.wrap(workerChannel, worker._child ? 'send' : 'postMessage', sendWrapper) } +function wrapWorkerInitializer (worker) { + if (wrappedWorkerInitializers.has(worker) || typeof worker.initialize !== 'function') return + + wrappedWorkerInitializers.add(worker) + shimmer.wrap(worker, 'initialize', initialize => function () { + const result = initialize.apply(this, arguments) + wrapWorkerChannel(this) + return result + }) +} + function wrapWorker (worker) { // ChildProcessWorker uses _child (child_process), ExperimentalWorker uses _worker (worker_threads) const workerChannel = worker._child || worker._worker if (!workerChannel) return + wrapWorkerInitializer(worker) wrapWorkerChannel(worker) shimmer.wrap(worker, '_onMessage', onMessageWrapper) workerChannel.removeAllListeners('message') diff --git a/packages/datadog-instrumentations/src/kafkajs.js b/packages/datadog-instrumentations/src/kafkajs.js index 82f9bd55976..8a0fb38703c 100644 --- a/packages/datadog-instrumentations/src/kafkajs.js +++ b/packages/datadog-instrumentations/src/kafkajs.js @@ -7,6 +7,10 @@ const { channel, addHook, } = require('./helpers/instrument') +const { + clientToCluster, + cloneMessages, +} = require('./helpers/kafka') const producerStartCh = channel('apm:kafkajs:produce:start') const producerCommitCh = channel('apm:kafkajs:produce:commit') @@ -24,6 +28,26 @@ const batchConsumerErrorCh = channel('apm:kafkajs:consume-batch:error') const disabledHeaderWeakSet = new WeakSet() +addHook({ name: 'kafkajs', file: 'src/producer/index.js', versions: ['>=1.4'] }, (createProducer) => + shimmer.wrapFunction(createProducer, original => function wrappedCreateProducer (params) { + const producer = original(params) + if (params?.cluster) { + clientToCluster.set(producer, params.cluster) + } + return producer + }) +) + +addHook({ name: 'kafkajs', file: 'src/consumer/index.js', versions: ['>=1.4'] }, (createConsumer) => + shimmer.wrapFunction(createConsumer, original => function wrappedCreateConsumer (params) { + const consumer = original(params) + if (params?.cluster) { + clientToCluster.set(consumer, params.cluster) + } + return consumer + }) +) + addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => { class Kafka extends BaseKafka { constructor (options) { @@ -34,79 +58,95 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf } } - shimmer.wrap(Kafka.prototype, 'producer', createProducer => function (...args) { - const producer = createProducer.apply(this, args) - const send = producer.send + shimmer.wrap(Kafka.prototype, 'producer', createProducer => function () { + const producer = createProducer.apply(this, arguments) + const originalSend = producer.send const bootstrapServers = this._brokers - - const kafkaClusterIdPromise = getKafkaClusterId(this) + const cluster = clientToCluster.get(producer) producer.send = function (...args) { - const wrappedSend = (clusterId) => { - const { topic, messages = [] } = args[0] - - const ctx = { - bootstrapServers, - clusterId, - disableHeaderInjection: disabledHeaderWeakSet.has(producer), - messages, - topic, - } + if (!producerStartCh.hasSubscribers) { + return originalSend.apply(this, args) + } - for (const message of messages) { - if (message !== null && typeof message === 'object' && !ctx.disableHeaderInjection) { - message.headers = message.headers || {} - } - } + // Fast path: kafkajs has fetched metadata, so clusterId is already on + // the broker pool. + const metadata = cluster?.brokerPool?.metadata + if (metadata) { + return runSend.call(this, args, metadata.clusterId) + } - return producerStartCh.runStores(ctx, () => { - try { - const result = send.apply(this, args) - result.then( - (res) => { - ctx.result = res - producerFinishCh.publish(ctx) - producerCommitCh.publish(ctx) - }, - (err) => { - ctx.error = err - if (err) { - // Fixes bug where we would inject message headers for kafka brokers that don't support headers - // (version <0.11). On the error, we disable header injection. - // Unfortunately the error name / type is not more specific. - // This approach is implemented by other tracers as well. - if (err.name === 'KafkaJSProtocolError' && err.type === 'UNKNOWN') { - disabledHeaderWeakSet.add(producer) - log.error( - // eslint-disable-next-line @stylistic/max-len - 'Kafka Broker responded with UNKNOWN_SERVER_ERROR (-1). Please look at broker logs for more information. Tracer message header injection for Kafka is disabled.' - ) - } - producerErrorCh.publish(err) - } - producerFinishCh.publish(ctx) - }) + // Slow path, taken at most once per producer connect cycle. Prime the + // metadata fetch kafkajs's send would do internally a few stack frames + // later. `sharedPromiseTo` collapses our call and kafkajs's call into a + // single round trip, so total latency is unchanged. + if (typeof cluster?.refreshMetadataIfNecessary !== 'function') { + return runSend.call(this, args) + } + return cluster.refreshMetadataIfNecessary().then( + () => runSend.call(this, args, cluster.brokerPool?.metadata?.clusterId), + () => runSend.call(this, args) + ) + } - return result - } catch (e) { - ctx.error = e - producerErrorCh.publish(ctx) - producerFinishCh.publish(ctx) - throw e - } - }) + function runSend (args, clusterId) { + const arg0 = args[0] + const topic = arg0?.topic + const inputMessages = Array.isArray(arg0?.messages) ? arg0.messages : [] + const disableHeaderInjection = disabledHeaderWeakSet.has(producer) + + // Hand kafkajs and the plugin a shallow clone so injection writes to + // tracer-owned objects instead of the caller's. With injection + // disabled the clone must not seed `headers: {}` either: brokers that + // reject any header field cannot recover otherwise. + let messages = inputMessages + if (inputMessages.length > 0) { + messages = cloneMessages(inputMessages, !disableHeaderInjection) + args[0] = { ...arg0, messages } } - if (isPromise(kafkaClusterIdPromise)) { - // promise is not resolved - return kafkaClusterIdPromise.then((clusterId) => { - return wrappedSend(clusterId) - }) + const ctx = { + bootstrapServers, + clusterId, + disableHeaderInjection, + messages, + topic, } - // promise is already resolved - return wrappedSend(kafkaClusterIdPromise) + return producerStartCh.runStores(ctx, () => { + try { + const result = originalSend.apply(this, args) + result.then( + (res) => { + ctx.result = res + producerFinishCh.publish(ctx) + producerCommitCh.publish(ctx) + }, + (error) => { + ctx.error = error + if (error) { + if (error.name === 'KafkaJSProtocolError' && error.type === 'UNKNOWN') { + disabledHeaderWeakSet.add(producer) + log.error( + // eslint-disable-next-line @stylistic/max-len + 'Kafka Broker responded with UNKNOWN_SERVER_ERROR (-1). Please look at broker logs for more information. Tracer message header injection for Kafka is disabled.' + ) + } + producerErrorCh.publish(error) + } + producerFinishCh.publish(ctx) + } + ) + return result + } catch (error) { + ctx.error = error + producerErrorCh.publish(ctx) + producerFinishCh.publish(ctx) + throw error + } + }) } + return producer }) @@ -115,24 +155,26 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf return createConsumer.apply(this, args) } - const kafkaClusterIdPromise = getKafkaClusterId(this) - let resolvedClusterId = null + const consumer = createConsumer.apply(this, arguments) + const cluster = clientToCluster.get(consumer) + const groupId = arguments[0].groupId + + const readClusterId = () => cluster?.brokerPool?.metadata?.clusterId - const eachMessageExtractor = (args, clusterId) => { + const eachMessageExtractor = (args) => { const { topic, partition, message } = args[0] - return { topic, partition, message, groupId, clusterId } + return { topic, partition, message, groupId, clusterId: readClusterId() } } - const eachBatchExtractor = (args, clusterId) => { + const eachBatchExtractor = (args) => { const { batch } = args[0] const { topic, partition, messages } = batch - return { topic, partition, messages, groupId, clusterId } + return { topic, partition, messages, groupId, clusterId: readClusterId() } } - const consumer = createConsumer.apply(this, args) - consumer.on(consumer.events.COMMIT_OFFSETS, (event) => { const { payload: { groupId: commitGroupId, topics } } = event + const clusterId = readClusterId() const commitList = [] for (const { topic, partitions } of topics) { for (const { partition, offset } of partitions) { @@ -141,7 +183,7 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf partition, offset, topic, - clusterId: resolvedClusterId, + clusterId, }) } } @@ -149,118 +191,67 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf }) const run = consumer.run - const groupId = args[0].groupId consumer.run = function ({ eachMessage, eachBatch, ...runArgs }) { - const wrapConsume = (clusterId) => { - // In kafkajs COMMIT_OFFSETS always happens in the context of one synchronous run - // So this will always reference a correct cluster id - resolvedClusterId = clusterId - return run({ - eachMessage: wrappedCallback( - eachMessage, - consumerStartCh, - consumerFinishCh, - consumerErrorCh, - eachMessageExtractor, - clusterId - ), - eachBatch: wrappedCallback( - eachBatch, - batchConsumerStartCh, - batchConsumerFinishCh, - batchConsumerErrorCh, - eachBatchExtractor, - clusterId - ), - ...runArgs, - }) - } - - if (isPromise(kafkaClusterIdPromise)) { - // promise is not resolved - return kafkaClusterIdPromise.then((clusterId) => { - return wrapConsume(clusterId) - }) - } - - // promise is already resolved - return wrapConsume(kafkaClusterIdPromise) + return run({ + eachMessage: wrappedCallback( + eachMessage, + consumerStartCh, + consumerFinishCh, + consumerErrorCh, + eachMessageExtractor + ), + eachBatch: wrappedCallback( + eachBatch, + batchConsumerStartCh, + batchConsumerFinishCh, + batchConsumerErrorCh, + eachBatchExtractor + ), + ...runArgs, + }) } + return consumer }) + return Kafka }) -const wrappedCallback = (fn, startCh, finishCh, errorCh, extractArgs, clusterId) => { - return typeof fn === 'function' - ? function (...args) { - const extractedArgs = extractArgs(args, clusterId) - const ctx = { - extractedArgs, - } +const wrappedCallback = (fn, startCh, finishCh, errorCh, extractArgs) => { + if (typeof fn !== 'function') return fn + return function (...args) { + const ctx = { + extractedArgs: extractArgs(args), + } - return startCh.runStores(ctx, () => { - try { - const result = fn.apply(this, args) - if (result && typeof result.then === 'function') { - result.then( - (res) => { - ctx.result = res - finishCh.publish(ctx) - }, - (err) => { - ctx.error = err - if (err) { - errorCh.publish(ctx) - } - finishCh.publish(ctx) - }) - } else { - finishCh.publish(ctx) - } - return result - } catch (e) { - ctx.error = e - errorCh.publish(ctx) + return startCh.runStores(ctx, () => { + try { + const result = fn.apply(this, args) + if (result && typeof result.then === 'function') { + result.then( + (res) => { + ctx.result = res + finishCh.publish(ctx) + }, + (error) => { + ctx.error = error + if (error) { + errorCh.publish(ctx) + } + finishCh.publish(ctx) + } + ) + } else { finishCh.publish(ctx) - throw e } - }) - } - : fn -} - -const getKafkaClusterId = (kafka) => { - if (kafka._ddKafkaClusterId) { - return kafka._ddKafkaClusterId - } - - if (!kafka.admin) { - return null - } - - const admin = kafka.admin() - - if (!admin.describeCluster) { - return null - } - - return admin.connect() - .then(() => { - return admin.describeCluster() - }) - .then((clusterInfo) => { - const clusterId = clusterInfo?.clusterId - kafka._ddKafkaClusterId = clusterId - admin.disconnect() - return clusterId - }) - .catch((error) => { - throw error + return result + } catch (error) { + ctx.error = error + errorCh.publish(ctx) + finishCh.publish(ctx) + throw error + } }) -} - -function isPromise (obj) { - return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' + } } diff --git a/packages/datadog-instrumentations/src/mocha/main.js b/packages/datadog-instrumentations/src/mocha/main.js index c46f82a2586..1a9bc784d82 100644 --- a/packages/datadog-instrumentations/src/mocha/main.js +++ b/packages/datadog-instrumentations/src/mocha/main.js @@ -469,6 +469,10 @@ addHook({ } const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite) + // Root-level tests (direct children of root, no describe wrapper) keyed by file. + // Populated during the root 'suite' event so the normal finish path can include them + // in mixed-file status calculation. + const rootTestsByFile = new Map() this.once('start', getOnStartHandler(frameworkVersion)) @@ -489,6 +493,30 @@ addHook({ this.on('suite', function (suite) { if (suite.root || !suite.tests.length) { + // This branch can be triggered when we have top level it(...) inside test files. + // In that case, they all (even if they are from different files) are going to be + // children of the root suite. + // Note: We could have suites that contain top level it(...) and also it(...) nested + // inside describe(...) ("mixed case"). Duplication is avoided by the context guard + // below. Since 'suite' fires for root first, in the mixed case the ctx is created + // here and the describe-based handler finds it already set. + if (suite.root && suite.tests.length > 0) { + const files = new Set(suite.tests.map(test => test.file).filter(Boolean)) + for (const file of files) { + rootTestsByFile.set(file, suite.tests.filter(t => t.file === file)) + if (testFileToSuiteCtx.get(file)) continue + const isUnskippable = unskippableSuites.includes(file) + isForcedToRun = isUnskippable && suitesToSkip.includes(getTestSuitePath(file, process.cwd())) + const ctx = { + testSuiteAbsolutePath: file, + isUnskippable, + isForcedToRun, + itrCorrelationId, + } + testFileToSuiteCtx.set(file, ctx) + testSuiteStartCh.runStores(ctx, () => {}) + } + } return } let ctx = testFileToSuiteCtx.get(suite.file) @@ -508,6 +536,44 @@ addHook({ this.on('suite end', function (suite) { if (suite.root) { + // Symmetric to the suite start fix + const fileToTests = new Map() + for (const test of suite.tests) { + if (!test.file) continue + if (!fileToTests.has(test.file)) fileToTests.set(test.file, []) + fileToTests.get(test.file).push(test) + } + for (const [file, tests] of fileToTests) { + // Mixed case: if a file appears in suitesByTestFile (pre-populated before the run), + // its numSuitesByTestFile counter hits zero when its last describe-based suite ends + // and the normal path below fires testSuiteFinishCh. Since root is last when + // 'suite end' fires, any such file has already been handled — skipping it here + // avoids duplication. + if (suitesByTestFile[file]) continue + let status = 'pass' + if (tests.every(test => test.isPending())) { + status = 'skip' + } else { + for (const test of tests) { + if (test.state === 'failed' || test.timedOut) { + status = 'fail' + break + } + } + } + if (global.__coverage__) { + const coverageFiles = getCoveredFilenamesFromCoverage(global.__coverage__) + testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file }) + mergeCoverage(global.__coverage__, originalCoverageMap) + resetCoverage(global.__coverage__) + } + const ctx = testFileToSuiteCtx.get(file) + if (ctx) { + testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {}) + } else { + log.warn('No ctx found for suite', file) + } + } return } const suitesInTestFile = suitesByTestFile[suite.file] @@ -517,8 +583,9 @@ addHook({ return } + const rootTests = rootTestsByFile.get(suite.file) || [] let status = 'pass' - if (suitesInTestFile.every(suite => suite.pending)) { + if (suitesInTestFile.every(suite => suite.pending) && rootTests.every(test => test.isPending())) { status = 'skip' } else { // has to check every test in the test file @@ -530,6 +597,11 @@ addHook({ } }) }) + for (const test of rootTests) { + if (test.state === 'failed' || test.timedOut) { + status = 'fail' + } + } } if (global.__coverage__) { diff --git a/packages/datadog-instrumentations/src/mongodb-core.js b/packages/datadog-instrumentations/src/mongodb-core.js index 2023c6190d6..66416b7343a 100644 --- a/packages/datadog-instrumentations/src/mongodb-core.js +++ b/packages/datadog-instrumentations/src/mongodb-core.js @@ -11,6 +11,14 @@ const startCh = channel('apm:mongodb:query:start') const finishCh = channel('apm:mongodb:query:finish') const errorCh = channel('apm:mongodb:query:error') +// Per-Connection cached topology shape (mongodb >= 4). The Connection's `address` is immutable +// for the lifetime of the connection, so we synthesize the `{ s: { options } }` envelope the +// plugin expects only once per connection. A WeakMap keeps the cache off the foreign Connection +// instance — no extra own-key visible to `Reflect.ownKeys`, `Object.freeze`, or another tracer's +// instrumentation walking the connection. +/** @type {WeakMap} */ +const topologyCache = new WeakMap() + addHook({ name: 'mongodb-core', versions: ['2 - 3.1.9'] }, Server => { const serverProto = Server.Server.prototype shimmer.wrap(serverProto, 'command', command => wrapCommand(command, 'command')) @@ -88,21 +96,36 @@ function wrapUnifiedCommand (command, operation, name) { } function wrapConnectionCommand (command, operation, name, instrumentFn = instrument) { + const opts = { name } return function (ns, ops) { if (!startCh.hasSubscribers) { return command.apply(this, arguments) } - const hostParts = typeof this.address === 'string' ? this.address.split(':') : '' - const options = hostParts.length === 2 - ? { host: hostParts[0], port: hostParts[1] } - : {} // no port means the address is a random UUID so no host either - const topology = { s: { options } } - - ns = `${ns.db}.${ns.collection}` - return instrumentFn(operation, command, this, arguments, topology, ns, ops, { name }) + let topology = topologyCache.get(this) + if (topology === undefined) { + topology = synthesizeTopology(this.address) + topologyCache.set(this, topology) + } + return instrumentFn(operation, command, this, arguments, topology, `${ns.db}.${ns.collection}`, ops, opts) } } +/** + * @param {string} address + * @returns {{ s: { options: { host?: string, port?: string } } }} + */ +function synthesizeTopology (address) { + if (typeof address === 'string') { + const colon = address.indexOf(':') + // Match the previous `.split(':')` length-2 check: exactly one colon with non-empty parts on both sides. + if (colon > 0 && colon < address.length - 1 && !address.includes(':', colon + 1)) { + return { s: { options: { host: address.slice(0, colon), port: address.slice(colon + 1) } } } + } + } + // No port means the address is a random UUID, an IPv6 form, or otherwise unparseable, so no host either. + return { s: { options: {} } } +} + function wrapQuery (query, operation, name) { return function (...args) { if (!startCh.hasSubscribers) { @@ -171,6 +194,8 @@ function instrument (operation, command, instance, args, server, ns, ops, option }) } +module.exports = { synthesizeTopology } + function instrumentPromise (operation, command, instance, args, server, ns, ops, options = {}) { const name = options.name || (ops && Object.keys(ops)[0]) diff --git a/packages/datadog-instrumentations/src/pg.js b/packages/datadog-instrumentations/src/pg.js index 98b89791521..91aafdb5324 100644 --- a/packages/datadog-instrumentations/src/pg.js +++ b/packages/datadog-instrumentations/src/pg.js @@ -15,6 +15,10 @@ const errorCh = channel('apm:pg:query:error') const startPoolQueryCh = channel('datadog:pg:pool:query:start') const finishPoolQueryCh = channel('datadog:pg:pool:query:finish') +// Drivers like pg-promise reuse the same prepared-statement query object across executions; cache +// the un-injected `text` so the wrap doesn't capture a previous DBM injection as the new original. +const originalTextCache = new WeakMap() + addHook({ name: 'pg', versions: ['>=8.0.3'], file: 'lib/native/client.js' }, Client => { shimmer.wrap(Client.prototype, 'query', query => wrapQuery(query)) return Client @@ -43,20 +47,14 @@ function wrapQuery (query) { : { text: args[0] } const textPropObj = pgQuery.cursor ?? pgQuery - const textProp = Object.getOwnPropertyDescriptor(textPropObj, 'text') const stream = typeof textPropObj.read === 'function' - // Only alter `text` property if safe to do so. Initially, it's a property, not a getter. - let originalText - if (!textProp || textProp.configurable) { + let originalText = originalTextCache.get(textPropObj) + if (originalText === undefined) { originalText = textPropObj.text - - Object.defineProperty(textPropObj, 'text', { - get () { - return this?.__ddInjectableQuery || originalText - }, - }) + originalTextCache.set(textPropObj, originalText) } + const abortController = new AbortController() const ctx = { params: this.connectionParameters, @@ -109,6 +107,22 @@ function wrapQuery (query) { return Promise.reject(error) } + const injected = ctx.injected + if (injected !== undefined) { + // Skip the per-read getter trampoline when `text` is a configurable, writable data + // property (the pg / pg-cursor common shape). Accessor descriptors and read-only data + // still go through `defineProperty(get)` so `get text ()` query objects keep working. + const textProp = Object.getOwnPropertyDescriptor(textPropObj, 'text') + if (textProp?.configurable === true && textProp.writable === true) { + textPropObj.text = injected + } else if (textProp === undefined || textProp.configurable === true) { + Object.defineProperty(textPropObj, 'text', { + configurable: true, + get () { return injected }, + }) + } + } + args[0] = pgQuery const retval = query.apply(this, args) diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index 7f3b3d3aa99..ae02deab234 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -3,6 +3,7 @@ // Capture real timers at module load time, before any test can install fake timers. const realSetTimeout = setTimeout +const { performance } = require('node:perf_hooks') const satisfies = require('../../../vendor/dist/semifies') const shimmer = require('../../datadog-shimmer') @@ -12,6 +13,8 @@ const { PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE, getIsFaultyEarlyFlakeDetection, DYNAMIC_NAME_RE, + getEfdRetryCount, + getMaxEfdRetryCount, recordAttemptToFixExecution, logAttemptToFixTestExecution, logTestOptimizationSummary, @@ -68,6 +71,7 @@ let remainingTestsByFile = {} let isKnownTestsEnabled = false let isEarlyFlakeDetectionEnabled = false let earlyFlakeDetectionNumRetries = 0 +let earlyFlakeDetectionSlowTestRetries = {} let isEarlyFlakeDetectionFaulty = false let earlyFlakeDetectionFaultyThreshold = 0 let isFlakyTestRetriesEnabled = false @@ -83,10 +87,19 @@ let testsReportedInGenerateSummary = new Set() const newTestsWithDynamicNames = new Set() const attemptToFixExecutions = new Map() const loggedAttemptToFixTests = new Set() +const efdManagedTestKeys = new Set() +const efdRetryCountByTestKey = new Map() +const efdRetryCountRequestsByTestKey = new Map() +const efdRetryTestsById = new Map() +const efdScheduledOriginalTestKeys = new Set() +const efdStartedOriginalTestKeys = new Set() +const efdSlowAbortedTests = new Set() let rootDir = '' let sessionProjects = [] const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5 +const EFD_RETRY_COUNT_REQUEST = 'ddEfdRetryCountRequest' +const EFD_RETRY_COUNT_RESPONSE = 'ddEfdRetryCountResponse' function isValidKnownTests (receivedKnownTests) { return !!receivedKnownTests.playwright @@ -97,6 +110,222 @@ function getTestFullyQualifiedName (test) { return `${test._requireFile} ${fullname}` } +/** + * @param {object} test + * @returns {string|undefined} + */ +function getTestProjectKey (test) { + const { _projectIndex, _projectId } = test + if (_projectIndex !== undefined) { + return `index:${_projectIndex}` + } + if (_projectId !== undefined) { + return `id:${_projectId}` + } + + const projectSuite = getSuiteType(test, 'project') + const projectName = projectSuite?._fullProject?.project?.name || + projectSuite?._fullProject?.name || + projectSuite?.title + if (projectName) { + return `name:${projectName}` + } +} + +/** + * @param {object} test + * @returns {number|undefined} + */ +function getTestEfdRepeatEachIndex (test) { + if (Object.hasOwn(test, '_ddEfdOriginalRepeatEachIndex')) { + return test._ddEfdOriginalRepeatEachIndex + } + return test.repeatEachIndex +} + +/** + * @param {object} test + * @returns {string|undefined} + */ +function getTestRepeatEachKey (test) { + const repeatEachIndex = getTestEfdRepeatEachIndex(test) + if (repeatEachIndex !== undefined) { + return `repeat:${repeatEachIndex}` + } +} + +/** + * @param {object} test + * @returns {string} + */ +function getTestEfdKey (test) { + const projectKey = getTestProjectKey(test) + const repeatEachKey = getTestRepeatEachKey(test) + const testFqn = getTestFullyQualifiedName(test) + return [projectKey, repeatEachKey, testFqn].filter(Boolean).join(' ') +} + +function getConfiguredEfdRetryCount () { + if (!earlyFlakeDetectionSlowTestRetries || !Object.keys(earlyFlakeDetectionSlowTestRetries).length) { + return earlyFlakeDetectionNumRetries + } + return getMaxEfdRetryCount(earlyFlakeDetectionSlowTestRetries) +} + +function markEfdManagedTest (test) { + test._ddIsEfdManagedTest = true + test._ddEfdSlowTestRetries = earlyFlakeDetectionSlowTestRetries + efdManagedTestKeys.add(getTestEfdKey(test)) +} + +function markEfdRetryTest (test, retryIndex, originalTest) { + test._ddIsEfdRetry = true + test._ddEfdRetryIndex = retryIndex + if (originalTest) { + test._ddEfdOriginalRepeatEachIndex = getTestEfdRepeatEachIndex(originalTest) + } +} + +function registerEfdRetryTest (test) { + if (!test._ddIsEfdRetry) { + return + } + + efdRetryTestsById.set(test.id, { + retryIndex: test._ddEfdRetryIndex, + testEfdKey: getTestEfdKey(test), + }) +} + +function getTestEfdSlowTestRetries (test) { + return test._ddEfdSlowTestRetries || earlyFlakeDetectionSlowTestRetries +} + +function isTestEfdManaged (test) { + return !!test._ddIsEfdManagedTest || ( + (test._ddIsNew || test._ddIsModified) && + !test._ddIsAttemptToFix && + isEarlyFlakeDetectionEnabled + ) +} + +function getFileSuiteRepeatEachIndex (fileSuite) { + const test = fileSuite.allTests()[0] + return test ? getTestEfdRepeatEachIndex(test) || 0 : 0 +} + +function getEfdRetryRepeatEachIndex (fileSuite, projectSuite, retryIndex, retryCount) { + const nativeRepeatEach = projectSuite._fullProject?.project?.repeatEach || 1 + const originalRepeatEachIndex = getFileSuiteRepeatEachIndex(fileSuite) + return nativeRepeatEach + (originalRepeatEachIndex * retryCount) + retryIndex - 1 +} + +function getEfdRetryCountForTest (test) { + return efdRetryCountByTestKey.get(getTestEfdKey(test)) ?? getConfiguredEfdRetryCount() +} + +function setEfdRetryCountForTest (test, retryCount) { + const testEfdKey = getTestEfdKey(test) + efdRetryCountByTestKey.set(testEfdKey, retryCount) + + const requests = efdRetryCountRequestsByTestKey.get(testEfdKey) + if (requests) { + efdRetryCountRequestsByTestKey.delete(testEfdKey) + for (const resolveRequest of requests) { + resolveRequest(retryCount) + } + } +} + +function sendEfdRetryCountToWorker (workerProcess, testId, retryIndex, retryCount) { + workerProcess.send({ + type: EFD_RETRY_COUNT_RESPONSE, + testId, + isEfdRetry: retryIndex !== undefined, + retryIndex, + retryCount, + }) +} + +function sendEfdRetryCountToWorkerWhenAvailable (workerProcess, testId) { + const efdRetryTest = efdRetryTestsById.get(testId) + if (!efdRetryTest) { + sendEfdRetryCountToWorker(workerProcess, testId) + return + } + + const { retryIndex, testEfdKey } = efdRetryTest + + if (!testEfdKey || !efdManagedTestKeys.has(testEfdKey)) { + sendEfdRetryCountToWorker(workerProcess, testId) + return + } + + const retryCount = efdRetryCountByTestKey.get(testEfdKey) + if (retryCount !== undefined) { + sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, retryCount) + return + } + + if (!efdStartedOriginalTestKeys.has(testEfdKey) && !efdScheduledOriginalTestKeys.has(testEfdKey)) { + sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, 0) + return + } + + if (!efdRetryCountRequestsByTestKey.has(testEfdKey)) { + efdRetryCountRequestsByTestKey.set(testEfdKey, []) + } + efdRetryCountRequestsByTestKey.get(testEfdKey).push((retryCount) => { + sendEfdRetryCountToWorker(workerProcess, testId, retryIndex, retryCount) + }) +} + +/** + * @param {object} test + * @returns {boolean} + */ +function shouldRequestEfdRetryCount (test) { + // The main process remains the source of truth. repeatEachIndex is only used as + // a cheap worker-side filter so first executions do not block on coordination. + return test._ddIsEfdRetry || test.repeatEachIndex > 0 +} + +function waitForEfdRetryCount (test) { + if (!process.send || !shouldRequestEfdRetryCount(test)) { + return Promise.resolve() + } + + const testEfdKey = getTestEfdKey(test) + return new Promise(resolve => { + const messageHandler = (message) => { + if (message?.type === EFD_RETRY_COUNT_RESPONSE && message.testId === test.id) { + if (message.isEfdRetry) { + test._ddIsEfdRetry = true + test._ddEfdRetryIndex = message.retryIndex + test._ddEfdRetryCount = message.retryCount + efdRetryCountByTestKey.set(testEfdKey, message.retryCount) + } + process.removeListener('message', messageHandler) + resolve() + } + } + + process.on('message', messageHandler) + process.send({ + type: EFD_RETRY_COUNT_REQUEST, + testId: test.id, + }) + }) +} + +function shouldSkipEfdRetry (test) { + if (!test._ddIsEfdRetry) { + return false + } + const retryCount = test._ddEfdRetryCount ?? efdRetryCountByTestKey.get(getTestEfdKey(test)) + return retryCount !== undefined && test._ddEfdRetryIndex > retryCount +} + function getTestProperties (test) { const testName = getTestFullname(test) const testSuite = getTestSuitePath(test._requireFile, rootDir) @@ -125,14 +354,17 @@ function getSuiteType (test, type) { } // Copy of Suite#_deepClone but with a function to filter tests -function deepCloneSuite (suite, filterTest, tags = []) { +function deepCloneSuite (suite, filterTest, tags = [], configureCopiedTest) { const copy = suite._clone() for (const entry of suite._entries) { if (entry.constructor.name === 'Suite') { - copy._addSuite(deepCloneSuite(entry, filterTest, tags)) + copy._addSuite(deepCloneSuite(entry, filterTest, tags, configureCopiedTest)) } else { if (filterTest(entry)) { const copiedTest = entry._clone() + if (configureCopiedTest) { + configureCopiedTest(copiedTest, entry) + } for (const tag of tags) { const resolvedTag = typeof tag === 'function' ? tag(entry) : tag @@ -303,6 +535,7 @@ function getFinalStatus ({ isAttemptToFix, hasFailedAllRetries, hasFailedAttemptToFixRetries, + hasPassedAnyEfdAttempt, testStatus, }) { if (!isFinalExecution) { @@ -311,9 +544,12 @@ function getFinalStatus ({ if (isDisabled || isQuarantined || testStatus === 'skip') { return 'skip' } - if (isAtrRetry || isEfdManagedTest) { + if (isAtrRetry) { return hasFailedAllRetries ? 'fail' : 'pass' } + if (isEfdManagedTest) { + return hasPassedAnyEfdAttempt ? 'pass' : 'fail' + } if (isAttemptToFix) { return hasFailedAttemptToFixRetries ? 'fail' : 'pass' } @@ -350,6 +586,14 @@ function testBeginHandler (test, browserName, shouldCreateTestSpan) { if (_type === 'beforeAll' || _type === 'afterAll') { return } + if (shouldSkipEfdRetry(test)) { + test._ddShouldSkipEfdRetry = true + return + } + test._ddStartTime = performance.now() + if (isTestEfdManaged(test) && !test._ddIsEfdRetry) { + efdStartedOriginalTestKeys.add(getTestEfdKey(test)) + } // this means that a skipped test is being handled if (!remainingTestsByFile[testSuiteAbsolutePath].length) { return @@ -391,6 +635,45 @@ function testBeginHandler (test, browserName, shouldCreateTestSpan) { } } +function finishTestSuiteIfDone (testSuiteAbsolutePath, projects) { + if (!shouldFinishTestSuite(testSuiteAbsolutePath)) { + return + } + + const skippedTests = remainingTestsByFile[testSuiteAbsolutePath] + .filter(test => test.expectedStatus === 'skipped') + + for (const test of skippedTests) { + const browserName = getBrowserNameFromProjects(projects, test) + testSkipCh.publish({ + testName: getTestFullname(test), + testSuiteAbsolutePath, + testSourceFileAbsolutePath: test.location.file, + testSourceLine: test.location.line, + browserName, + isNew: test._ddIsNew, + isDisabled: test._ddIsDisabled, + isModified: test._ddIsModified, + isQuarantined: test._ddIsQuarantined, + }) + } + remainingTestsByFile[testSuiteAbsolutePath] = [] + + const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath) + let testSuiteStatus = 'pass' + if (testStatuses?.includes('fail')) { + testSuiteStatus = 'fail' + } else if (testStatuses?.every(status => status === 'skip')) { + testSuiteStatus = 'skip' + } + + const suiteError = getTestSuiteError(testSuiteAbsolutePath) + const testSuiteCtx = testSuiteToCtx.get(testSuiteAbsolutePath) + if (testSuiteCtx) { + testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError, ...testSuiteCtx.currentStore }) + } +} + function testEndHandler ({ test, annotations, @@ -420,11 +703,21 @@ function testEndHandler ({ return } + if (test._ddShouldSkipEfdRetry || shouldSkipEfdRetry(test)) { + test._ddShouldSkipEfdRetry = true + remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath] + .filter(currentTest => currentTest !== test) + finishTestSuiteIfDone(testSuiteAbsolutePath, projects) + return + } + + const isEfdManagedTest = isTestEfdManaged(test) const testFqn = getTestFullyQualifiedName(test) - const testStatuses = testsToTestStatuses.get(testFqn) || [] + const testStatusKey = isEfdManagedTest ? getTestEfdKey(test) : testFqn + const testStatuses = testsToTestStatuses.get(testStatusKey) || [] if (testStatuses.length === 0) { - testsToTestStatuses.set(testFqn, [testStatus]) + testsToTestStatuses.set(testStatusKey, [testStatus]) if (test._ddIsNew && DYNAMIC_NAME_RE.test(getTestFullname(test))) { newTestsWithDynamicNames.add(`${getTestSuitePath(test._requireFile, rootDir)} › ${getTestFullname(test)}`) } @@ -432,6 +725,17 @@ function testEndHandler ({ testStatuses.push(testStatus) } + const testEfdKey = getTestEfdKey(test) + if (isEfdManagedTest && !test._ddIsEfdRetry && !efdRetryCountByTestKey.has(testEfdKey)) { + const testResult = results.at(-1) + const duration = testResult?.duration > 0 ? testResult.duration : performance.now() - test._ddStartTime + const retryCount = getEfdRetryCount(duration, getTestEfdSlowTestRetries(test)) + setEfdRetryCountForTest(test, retryCount) + if (retryCount === 0) { + efdSlowAbortedTests.add(testEfdKey) + } + } + const testProperties = getTestProperties(test) if (testProperties.attemptToFix) { @@ -460,9 +764,10 @@ function testEndHandler ({ } // Check if all EFD retries failed - if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 && + const efdRetryCount = getEfdRetryCountForTest(test) + if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 && (test._ddIsNew || test._ddIsModified) && - test._ddIsEfdRetry && + isEarlyFlakeDetectionEnabled && testStatuses.every(status => status === 'fail')) { test._ddHasFailedAllRetries = true } @@ -480,9 +785,6 @@ function testEndHandler ({ if (shouldCreateTestSpan) { const testResult = results.at(-1) const testCtx = testToCtx.get(test) - const isEfdManagedTest = (test._ddIsNew || test._ddIsModified) && - !test._ddIsAttemptToFix && - isEarlyFlakeDetectionEnabled const isAtrRetry = testResult?.retry > 0 && isFlakyTestRetriesEnabled && !test._ddIsAttemptToFix && @@ -497,6 +799,7 @@ function testEndHandler ({ isAttemptToFix: test._ddIsAttemptToFix, hasFailedAllRetries: test._ddHasFailedAllRetries, hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries, + hasPassedAnyEfdAttempt: testStatuses.includes('pass'), testStatus, }) @@ -520,6 +823,7 @@ function testEndHandler ({ isAtrRetry, isModified: test._ddIsModified, finalStatus, + earlyFlakeAbortReason: efdSlowAbortedTests.has(testEfdKey) ? 'slow' : undefined, ...testCtx.currentStore, }) } @@ -540,38 +844,7 @@ function testEndHandler ({ .filter(currentTest => currentTest !== test) } - if (shouldFinishTestSuite(testSuiteAbsolutePath)) { - const skippedTests = remainingTestsByFile[testSuiteAbsolutePath] - .filter(test => test.expectedStatus === 'skipped') - - for (const test of skippedTests) { - const browserName = getBrowserNameFromProjects(projects, test) - testSkipCh.publish({ - testName: getTestFullname(test), - testSuiteAbsolutePath, - testSourceFileAbsolutePath: test.location.file, - testSourceLine: test.location.line, - browserName, - isNew: test._ddIsNew, - isDisabled: test._ddIsDisabled, - isModified: test._ddIsModified, - isQuarantined: test._ddIsQuarantined, - }) - } - remainingTestsByFile[testSuiteAbsolutePath] = [] - - const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath) - let testSuiteStatus = 'pass' - if (testStatuses.includes('fail')) { - testSuiteStatus = 'fail' - } else if (testStatuses.every(status => status === 'skip')) { - testSuiteStatus = 'skip' - } - - const suiteError = getTestSuiteError(testSuiteAbsolutePath) - const testSuiteCtx = testSuiteToCtx.get(testSuiteAbsolutePath) - testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError, ...testSuiteCtx.currentStore }) - } + finishTestSuiteIfDone(testSuiteAbsolutePath, projects) } function dispatcherRunWrapper (run) { @@ -581,6 +854,39 @@ function dispatcherRunWrapper (run) { } } +function deferEfdRetryGroups (testGroups) { + const groupsWithOriginalTests = [] + const efdRetryOnlyGroups = [] + + for (const group of testGroups) { + const originalTests = [] + const efdRetryTests = [] + + for (const test of group.tests) { + if (test._ddIsEfdRetry) { + efdRetryTests.push(test) + } else { + originalTests.push(test) + if (isTestEfdManaged(test)) { + efdScheduledOriginalTestKeys.add(getTestEfdKey(test)) + } + } + } + + if (efdRetryTests.length && originalTests.length) { + group.tests = [...originalTests, ...efdRetryTests] + } + + if (originalTests.length) { + groupsWithOriginalTests.push(group) + } else { + efdRetryOnlyGroups.push(group) + } + } + + return [...groupsWithOriginalTests, ...efdRetryOnlyGroups] +} + function dispatcherRunWrapperNew (run) { return function (testGroups) { // Filter out disabled tests from testGroups before they get scheduled, @@ -593,12 +899,17 @@ function dispatcherRunWrapperNew (run) { testGroups = testGroups.filter(group => group.tests.length > 0) } + if (isEarlyFlakeDetectionEnabled) { + testGroups = deferEfdRetryGroups(testGroups) + } + if (!this._allTests) { // Removed in https://github.com/microsoft/playwright/commit/1e52c37b254a441cccf332520f60225a5acc14c7 // Not available from >=1.44.0 this._ddAllTests = testGroups.flatMap(g => g.tests) } remainingTestsByFile = getTestsBySuiteFromTestGroups(testGroups) + arguments[0] = testGroups return run.apply(this, arguments) } } @@ -638,7 +949,6 @@ function dispatcherHook (dispatcherExport) { ) } }) - return worker }) return dispatcherExport @@ -690,14 +1000,11 @@ function dispatcherHookNew (dispatcherExport, runWrapper) { // above) and mark the execution final once the count reaches the expected total. // This mirrors how ATF finality is detected and centralizes the decision in the // main process, so workers only need to act on the _ddIsFinalExecution flag. - const isEfdManagedTest = (test._ddIsNew || test._ddIsModified) && - !test._ddIsAttemptToFix && - isEarlyFlakeDetectionEnabled + const isEfdManagedTest = isTestEfdManaged(test) let isFinalExecution if (isEfdManagedTest) { - const testFqn = getTestFullyQualifiedName(test) - const efdTestStatuses = testsToTestStatuses.get(testFqn) || [] - isFinalExecution = efdTestStatuses.length === earlyFlakeDetectionNumRetries + 1 + const efdTestStatuses = testsToTestStatuses.get(getTestEfdKey(test)) || [] + isFinalExecution = efdTestStatuses.length === getEfdRetryCountForTest(test) + 1 } else if (test._ddIsAttemptToFix) { isFinalExecution = !!(test._ddHasPassedAttemptToFixRetries || test._ddHasFailedAttemptToFixRetries) } else { @@ -722,10 +1029,11 @@ function dispatcherHookNew (dispatcherExport, runWrapper) { _ddIsModified: test._ddIsModified, _ddIsFinalExecution: isFinalExecution, _ddIsEfdManagedTest: isEfdManagedTest, + _ddEarlyFlakeAbortReason: efdSlowAbortedTests.has(getTestEfdKey(test)) ? 'slow' : undefined, + _ddHasPassedAnyEfdAttempt: (testsToTestStatuses.get(getTestEfdKey(test)) || []).includes('pass'), }, }) }) - return worker }) return dispatcherExport @@ -751,6 +1059,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) { isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries + earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {} earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount @@ -900,6 +1209,13 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) { remainingTestsByFile = {} quarantinedButNotAttemptToFixFqns = new Set() testsReportedInGenerateSummary = new Set() + efdManagedTestKeys.clear() + efdRetryCountByTestKey.clear() + efdRetryCountRequestsByTestKey.clear() + efdRetryTestsById.clear() + efdScheduledOriginalTestKeys.clear() + efdStartedOriginalTestKeys.clear() + efdSlowAbortedTests.clear() // TODO: we can trick playwright into thinking the session passed by returning // 'passed' here. We might be able to use this for both EFD and Test Management tests. @@ -1001,12 +1317,29 @@ addHook({ * - we execute `applyRepeatEachIndex` for each of these cloned file suites * - we add the cloned file suites to the project suite */ -function applyRetriesToTests (fileSuitesWithTestsToRetry, filterTest, tagsToApply, numRetries) { +function applyRetriesToTests ( + fileSuitesWithTestsToRetry, + filterTest, + tagsToApply, + numRetries, + configureCopiedTest, + getRetryRepeatEachIndex +) { for (const [fileSuite, projectSuite] of fileSuitesWithTestsToRetry.entries()) { for (let repeatEachIndex = 1; repeatEachIndex <= numRetries; repeatEachIndex++) { - const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply) - applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1) + const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply, (copiedTest, originalTest) => { + if (configureCopiedTest) { + configureCopiedTest(copiedTest, originalTest, repeatEachIndex) + } + }) + const retryRepeatEachIndex = getRetryRepeatEachIndex + ? getRetryRepeatEachIndex(fileSuite, projectSuite, repeatEachIndex, numRetries) + : repeatEachIndex + 1 + applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, retryRepeatEachIndex) projectSuite._addSuite(copyFileSuite) + for (const copiedTest of copyFileSuite.allTests()) { + registerEfdRetryTest(copiedTest) + } } } } @@ -1091,6 +1424,7 @@ addHook({ for (const impactedTest of impactedTests) { impactedTest._ddIsModified = true if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') { + markEfdManagedTest(impactedTest) const fileSuite = getSuiteType(impactedTest, 'file') if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) { fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project')) @@ -1106,7 +1440,12 @@ addHook({ '_ddIsEfdRetry', (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null), ], - earlyFlakeDetectionNumRetries + getConfiguredEfdRetryCount(), + (copiedTest, originalTest, retryIndex) => { + markEfdRetryTest(copiedTest, retryIndex, originalTest) + markEfdManagedTest(copiedTest) + }, + getEfdRetryRepeatEachIndex ) } @@ -1130,6 +1469,7 @@ addHook({ if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) { // Prevent ATR or `--retries` from retrying new tests if EFD is enabled newTest.retries = 0 + markEfdManagedTest(newTest) const fileSuite = getSuiteType(newTest, 'file') if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) { fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project')) @@ -1141,7 +1481,12 @@ addHook({ fileSuitesWithNewTestsToProjects, isNewTest, ['_ddIsNew', '_ddIsEfdRetry'], - earlyFlakeDetectionNumRetries + getConfiguredEfdRetryCount(), + (copiedTest, originalTest, retryIndex) => { + markEfdRetryTest(copiedTest, retryIndex, originalTest) + markEfdManagedTest(copiedTest) + }, + getEfdRetryRepeatEachIndex ) } } @@ -1177,6 +1522,10 @@ addHook({ // We add a new listener to `this.process`, which is represents the worker this.process.on('message', (message) => { + if (message?.type === EFD_RETRY_COUNT_REQUEST) { + sendEfdRetryCountToWorkerWhenAvailable(this.process, message.testId) + return + } // These messages are [code, payload]. The payload is test data if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) { workerReportCh.publish(message[1]) @@ -1235,9 +1584,15 @@ addHook({ const stepInfoByStepId = {} shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) { + await waitForEfdRetryCount(test) + if (shouldSkipEfdRetry(test)) { + test._ddShouldSkipEfdRetry = true + test.expectedStatus = 'skipped' + } if (test.expectedStatus === 'skipped') { return _runTest.apply(this, arguments) } + test._ddStartTime = performance.now() steps = [] const { @@ -1320,6 +1675,21 @@ addHook({ await res const { status, error, annotations, retry, testId } = testInfo + const testEfdKey = getTestEfdKey(test) + const isEfdManagedTest = isTestEfdManaged(test) + if (isEfdManagedTest && !test._ddIsEfdRetry && !efdRetryCountByTestKey.has(testEfdKey)) { + const duration = test.results?.at(-1)?.duration > 0 + ? test.results.at(-1).duration + : performance.now() - test._ddStartTime + const retryCount = getEfdRetryCount( + duration, + getTestEfdSlowTestRetries(test) + ) + setEfdRetryCountForTest(test, retryCount) + if (retryCount === 0) { + efdSlowAbortedTests.add(testEfdKey) + } + } // testInfo.errors could be better than "error", // which will only include timeout error (even though the test failed because of a different error) @@ -1365,6 +1735,7 @@ addHook({ isAttemptToFix: test._ddIsAttemptToFix, hasFailedAllRetries: test._ddHasFailedAllRetries, hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries, + hasPassedAnyEfdAttempt: test._ddHasPassedAnyEfdAttempt, testStatus: STATUS_TO_TEST_STATUS[status], }) @@ -1388,6 +1759,7 @@ addHook({ isModified: test._ddIsModified, onDone, finalStatus, + earlyFlakeAbortReason: test._ddEarlyFlakeAbortReason, ...testCtx.currentStore, }) diff --git a/packages/datadog-instrumentations/src/redis.js b/packages/datadog-instrumentations/src/redis.js index a37105bde77..93ba5612caf 100644 --- a/packages/datadog-instrumentations/src/redis.js +++ b/packages/datadog-instrumentations/src/redis.js @@ -13,6 +13,7 @@ const errorCh = channel('apm:redis:command:error') let createClientUrl let createClientName const instanceInfo = new WeakMap() +const connectionInfoCache = new WeakMap() function wrapAddCommand (addCommand) { return function (command) { @@ -20,10 +21,7 @@ function wrapAddCommand (addCommand) { return addCommand.apply(this, arguments) } - const name = command[0] - const args = command.slice(1) - - const ctx = getStartCtx(this, name, args) + const ctx = getStartCtx(this, command[0], command, 1) return startCh.runStores(ctx, () => { const res = addCommand.apply(this, arguments) @@ -136,22 +134,33 @@ addHook({ name: 'redis', versions: ['>=0.12 <2.6'] }, redis => { return redis }) -function getStartCtx (client, command, args) { - const { url, connectionName } = instanceInfo.get(client) || {} +function getStartCtx (client, command, args, argsStartIndex) { + let cached = connectionInfoCache.get(client) + if (cached === undefined) { + const info = instanceInfo.get(client) + cached = { + connectionOptions: + client.connection_options || client.connection_option || client.connectionOption || info?.url, + connectionName: info?.connectionName, + } + connectionInfoCache.set(client, cached) + } return { db: client.selected_db, command, args, - connectionOptions: client.connection_options || client.connection_option || client.connectionOption || url, - connectionName, + argsStartIndex, + connectionOptions: cached.connectionOptions, + connectionName: cached.connectionName, } } function wrapCallback (finishCh, errorCh, ctx, callback) { - return shimmer.wrapFunction(callback, callback => function (err) { + if (typeof callback !== 'function') return callback + return function (err) { return finish(finishCh, errorCh, ctx, err, callback, this, arguments) - }) + } } function finish (finishCh, errorCh, ctx, error, callback, thisArg, args) { diff --git a/packages/datadog-instrumentations/test/couchbase.spec.js b/packages/datadog-instrumentations/test/couchbase.spec.js new file mode 100644 index 00000000000..62bc4b80d9c --- /dev/null +++ b/packages/datadog-instrumentations/test/couchbase.spec.js @@ -0,0 +1,353 @@ +'use strict' + +const assert = require('node:assert/strict') +const EventEmitter = require('node:events') + +const { tracingChannel } = require('dc-polyfill') +const { afterEach, before, describe, it } = require('mocha') + +require('../src/couchbase') + +const COUCHBASE_HOOKS = globalThis[Symbol.for('_ddtrace_instrumentations')].couchbase + +const queryChannel = tracingChannel('apm:couchbase:query') +const upsertChannel = tracingChannel('apm:couchbase:upsert') + +// Mirror of the SDK's `StreamablePromise` in v3.2.x / v4.0-v4.4: lazy +// internal Promise that only constructs on first `.then`. No depromisify +// override on `.on` — that surfaced later (v4.5.0+, JSCBC-1301). The +// orchestrator wrap relies on calling `.then(...)` synchronously to +// trigger the lazy listener attachment before libcouchbase emits. +class FakeStreamablePromise extends EventEmitter { + #promise = null + #promiseifyFn + + constructor (promiseifyFn) { + super() + this.#promiseifyFn = promiseifyFn + } + + get promise () { + this.#promise ??= new Promise((resolve, reject) => this.#promiseifyFn(this, resolve, reject)) + return this.#promise + } + + then (onfulfilled, onrejected) { return this.promise.then(onfulfilled, onrejected) } + catch (onrejected) { return this.promise.catch(onrejected) } + finally (onfinally) { return this.promise.finally(onfinally) } +} + +// Mirror of the SDK's `StreamablePromise` in v4.5.0+. Eager listener +// attachment via a proxy `emitter` that bypasses the `.on` override, +// plus the JSCBC-1301 depromisify behaviour: external `.on()` / +// `.addListener()` calls remove the SDK's internal listeners and null +// `_promise`. Subsequent `.then` / `.catch` / `.finally` then throw +// `Cannot await a promise that is already registered for events`. This +// is the v4.5.0+ contract: API must be either awaited or used with +// events, never both. The orchestrator wrap therefore must NEVER call +// `.on()` on the SDK's return value. +class FakeStreamablePromiseV45 extends EventEmitter { + #promise = null + #promiseOns = [] + #depromisified = false + + constructor (promiseifyFn) { + super() + const eeOn = EventEmitter.prototype.on + this.#promise = new Promise((resolve, reject) => { + const proxy = { + on: (eventName, listener) => { + this.#promiseOns.push([eventName, listener]) + eeOn.call(this, eventName, listener) + }, + } + promiseifyFn(proxy, resolve, reject) + }) + } + + #depromisify () { + for (const [eventName, listener] of this.#promiseOns) this.off(eventName, listener) + this.#promiseOns = [] + this.#promise = null + this.#depromisified = true + } + + get promise () { + if (this.#depromisified) { + throw new Error('Cannot await a promise that is already registered for events') + } + return this.#promise + } + + on (eventName, listener) { + this.#depromisify() + return super.on(eventName, listener) + } + + addListener (eventName, listener) { + this.#depromisify() + return super.addListener(eventName, listener) + } + + then (onfulfilled, onrejected) { return this.promise.then(onfulfilled, onrejected) } + catch (onrejected) { return this.promise.catch(onrejected) } + finally (onfinally) { return this.promise.finally(onfinally) } +} + +function rowsPromiseifyFn (rowsToResultFn) { + return (emitter, resolve, reject) => { + let err + const rows = [] + let meta + emitter.on('row', r => rows.push(r)) + emitter.on('meta', m => { meta = m }) + emitter.on('error', e => { err = e }) + emitter.on('end', () => { + if (err) return reject(err) + resolve(rowsToResultFn(rows, meta)) + }) + } +} + +class FakeStreamableRowPromise extends FakeStreamablePromise { + constructor (rowsToResultFn) { super(rowsPromiseifyFn(rowsToResultFn)) } +} + +class FakeStreamableRowPromiseV45 extends FakeStreamablePromiseV45 { + constructor (rowsToResultFn) { super(rowsPromiseifyFn(rowsToResultFn)) } +} + +/** + * @param {string} file File suffix to match against the registered addHook entries. + * @returns {(mod: unknown) => unknown} + */ +function findHook (file) { + for (const entry of COUCHBASE_HOOKS) { + if (entry.file === file) return entry.hook + } + throw new Error(`couchbase hook for ${file} not registered`) +} + +const wrapClusterQuery = findHook('dist/cluster.js') +const wrapBucket = findHook('dist/bucket.js') +const wrapCollection = findHook('dist/collection.js') + +function makeFakeSdk ({ emit }) { + class FakeCluster { + constructor () { + this._connStr = 'couchbase://localhost' + } + + query (statement) { + const emitter = new FakeStreamableRowPromise(rows => ({ rows })) + emit(emitter) + return emitter + } + } + + class FakeCollection { + constructor (bucket, name) { + this._scope = { _bucket: { _name: bucket._name } } + this._name = name + } + + upsert (key, value, ...rest) { return this.#kv(emit, rest) } + insert (key, value, ...rest) { return this.#kv(emit, rest) } + replace (key, value, ...rest) { return this.#kv(emit, rest) } + + // Mirrors the real SDK's `PromiseHelper.wrapAsync(...)` (cluster.ts / + // collection.ts on every traced op): when a callback is provided, it + // synchronously calls `.then(...)` on the StreamablePromise to forward + // resolve / reject to the callback. That `.then` is what triggers the + // lazy listener attachment in `StreamablePromise.get promise` for v3.2.x + // and v4.0-v4.4, so an `'error'` listener exists by the time + // libcouchbase fires. + #kv (emit, rest) { + const callback = typeof rest.at(-1) === 'function' ? rest.at(-1) : undefined + const emitter = new FakeStreamableRowPromise(rows => ({ rows })) + emit(emitter) + if (callback) { + emitter.then( + (result) => callback(undefined, result), + (error) => callback(error, undefined) + ) + setImmediate(() => emitter.emit('end')) + } + return emitter + } + } + + FakeCollection.DEFAULT_NAME = '_default' + + class FakeBucket { + constructor (cluster, name) { + this._cluster = cluster + this._name = name + } + + collection (name) { return new FakeCollection(this, name) } + } + + wrapClusterQuery({ Cluster: FakeCluster }) + wrapBucket({ Bucket: FakeBucket }) + wrapCollection({ Collection: FakeCollection }) + + const cluster = new FakeCluster() + const bucket = new FakeBucket(cluster, 'datadog-test') + const collection = bucket.collection('_default') + return { cluster, bucket, collection } +} + +describe('couchbase instrumentation: v3.2.x StreamableRowPromise', () => { + /** @type {Array<() => void>} */ + const cleanups = [] + + before(() => { + assert.equal(Array.isArray(COUCHBASE_HOOKS), true, 'couchbase hooks must register on require') + }) + + afterEach(() => { + while (cleanups.length) cleanups.pop()() + }) + + function subscribe (ch, names = ['start', 'asyncStart', 'asyncEnd', 'error', 'end']) { + const events = [] + const handlers = {} + for (const name of names) { + handlers[name] = ctx => events.push({ name, ctx }) + } + ch.subscribe(handlers) + cleanups.push(() => ch.unsubscribe(handlers)) + return events + } + + it('does not crash when libcouchbase emits "error" on the StreamableRowPromise asynchronously', async () => { + const events = subscribe(queryChannel) + let pendingEmitter + const { cluster } = makeFakeSdk({ emit: e => { pendingEmitter = e } }) + + const promise = cluster.query('SELECT 1+1') + + await new Promise(resolve => setImmediate(resolve)) + + pendingEmitter.emit('error', new Error('libcouchbase: request canceled')) + + await new Promise(resolve => setImmediate(resolve)) + + assert.equal(events.some(e => e.name === 'start'), true, 'start event must fire') + assert.equal(typeof promise.on, 'function', 'wrap returns the original EventEmitter surface') + }) + + it('does not crash when the SDK emits "error" before any caller awaits the promise', async () => { + subscribe(queryChannel) + let pendingEmitter + const { cluster } = makeFakeSdk({ emit: e => { pendingEmitter = e } }) + + cluster.query('SELECT 1+1') + + await new Promise(resolve => setImmediate(resolve)) + + pendingEmitter.emit('error', new Error('libcouchbase: request canceled')) + + await new Promise(resolve => setImmediate(resolve)) + }) + + it('drives asyncStart/asyncEnd/error sub-channels for the promise path', async () => { + const events = subscribe(queryChannel) + let pendingEmitter + const { cluster } = makeFakeSdk({ emit: e => { pendingEmitter = e } }) + + const promise = cluster.query('SELECT 1+1').catch(() => {}) + + pendingEmitter.emit('error', new Error('libcouchbase: query failed')) + pendingEmitter.emit('end') + + await promise + + const eventNames = events.map(e => e.name) + assert.deepEqual(eventNames, ['start', 'end', 'error', 'asyncStart', 'asyncEnd']) + const errorEvent = events.find(e => e.name === 'error') + assert.equal(errorEvent.ctx.error.message, 'libcouchbase: query failed') + }) + + it('drives asyncStart/asyncEnd on a successful resolution', async () => { + const events = subscribe(queryChannel) + let pendingEmitter + const { cluster } = makeFakeSdk({ emit: e => { pendingEmitter = e } }) + + const rows = [{ a: 1 }, { a: 2 }] + const promise = cluster.query('SELECT *') + + for (const row of rows) pendingEmitter.emit('row', row) + pendingEmitter.emit('end') + + const result = await promise + + assert.deepEqual(result, { rows }) + const eventNames = events.map(e => e.name) + assert.deepEqual(eventNames, ['start', 'end', 'asyncStart', 'asyncEnd']) + }) + + it('publishes :error and rethrows when the wrapped operation throws synchronously', () => { + const events = subscribe(queryChannel) + + class ThrowingCluster { + constructor () { this._connStr = 'couchbase://localhost' } + query () { throw new Error('libcouchbase: invalid argument') } + } + + wrapClusterQuery({ Cluster: ThrowingCluster }) + const cluster = new ThrowingCluster() + + assert.throws(() => cluster.query('SELECT bad'), /invalid argument/) + + const eventNames = events.map(e => e.name) + assert.deepEqual(eventNames, ['start', 'error', 'end']) + const errorEvent = events.find(e => e.name === 'error') + assert.equal(errorEvent.ctx.error.message, 'libcouchbase: invalid argument') + }) + + it('does not call .on() on the SDK return value (v4.5.0+ depromisify regression guard)', async () => { + subscribe(queryChannel) + let pendingEmitter + + class V45Cluster { + constructor () { this._connStr = 'couchbase://localhost' } + + query (statement) { + const emitter = new FakeStreamableRowPromiseV45(rows => ({ rows })) + pendingEmitter = emitter + return emitter + } + } + + wrapClusterQuery({ Cluster: V45Cluster }) + const cluster = new V45Cluster() + + const result = cluster.query('SELECT 1') + + setImmediate(() => pendingEmitter.emit('end')) + + // If the wrap calls `.on()` on the return value, JSCBC-1301 + // depromisifies and `await result` throws. The contract has been part + // of the SDK since v4.5.0. + await result + }) + + it('drives the callback path through traceCallback without losing the EE surface', async () => { + const events = subscribe(upsertChannel) + let pendingEmitter + const { collection } = makeFakeSdk({ emit: e => { pendingEmitter = e } }) + + const result = collection.upsert('k', 'v', () => {}) + + assert.equal(typeof result.on, 'function', 'callback path must keep the EventEmitter surface') + + pendingEmitter.emit('error', new Error('libcouchbase: timeout')) + + await new Promise(resolve => setImmediate(resolve)) + + const startEvent = events.find(e => e.name === 'start') + assert.notEqual(startEvent, undefined, 'start must fire under the callback path') + }) +}) diff --git a/packages/datadog-instrumentations/test/electron/preload.spec.js b/packages/datadog-instrumentations/test/electron/preload.spec.js new file mode 100644 index 00000000000..2aad8b9d6fd --- /dev/null +++ b/packages/datadog-instrumentations/test/electron/preload.spec.js @@ -0,0 +1,118 @@ +'use strict' + +const assert = require('node:assert/strict') +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire') + +const BRIDGE_CHANNEL = 'datadog:bridge-send' +const CONFIG_CHANNEL = 'datadog:bridge-config' + +describe('electron/preload', () => { + let contextBridge + let ipcRenderer + + beforeEach(() => { + global.location = { hostname: 'test.example.com' } + global.window = {} + + ipcRenderer = { + sendSync: sinon.stub(), + send: sinon.stub(), + } + contextBridge = { + exposeInMainWorld: sinon.stub(), + } + }) + + afterEach(() => { + delete global.location + delete global.window + }) + + function loadPreload (config = null) { + ipcRenderer.sendSync.returns(config) + proxyquire('../../src/electron/preload', { + electron: { contextBridge, ipcRenderer, '@noCallThru': true }, + }) + return global.window.DatadogEventBridge + } + + it('requests config from main via the config channel', () => { + loadPreload() + sinon.assert.calledOnceWithExactly(ipcRenderer.sendSync, CONFIG_CHANNEL) + }) + + it('sets DatadogEventBridge on window', () => { + loadPreload() + assert.ok(global.window.DatadogEventBridge) + }) + + it('exposes DatadogEventBridge in the main world via contextBridge', () => { + const bridge = loadPreload() + sinon.assert.calledOnceWithExactly(contextBridge.exposeInMainWorld, 'DatadogEventBridge', bridge) + }) + + describe('getCapabilities()', () => { + it('returns an empty JSON array', () => { + const bridge = loadPreload() + assert.strictEqual(bridge.getCapabilities(), '[]') + }) + }) + + describe('getPrivacyLevel()', () => { + it('returns "mask" when no config is provided', () => { + const bridge = loadPreload(null) + assert.strictEqual(bridge.getPrivacyLevel(), 'mask') + }) + + it('returns "mask" when config does not include defaultPrivacyLevel', () => { + const bridge = loadPreload({ allowedWebViewHosts: [] }) + assert.strictEqual(bridge.getPrivacyLevel(), 'mask') + }) + + it('returns the configured privacy level', () => { + const bridge = loadPreload({ defaultPrivacyLevel: 'allow' }) + assert.strictEqual(bridge.getPrivacyLevel(), 'allow') + }) + }) + + describe('getAllowedWebViewHosts()', () => { + it('includes location.hostname when no config is provided', () => { + const bridge = loadPreload(null) + const hosts = JSON.parse(bridge.getAllowedWebViewHosts()) + assert.ok(hosts.includes('test.example.com')) + }) + + it('includes both location.hostname and configured hosts', () => { + const bridge = loadPreload({ allowedWebViewHosts: ['allowed.example.com'] }) + const hosts = JSON.parse(bridge.getAllowedWebViewHosts()) + assert.ok(hosts.includes('test.example.com')) + assert.ok(hosts.includes('allowed.example.com')) + }) + + it('deduplicates hosts when location.hostname is also in configured hosts', () => { + const bridge = loadPreload({ allowedWebViewHosts: ['test.example.com', 'other.example.com'] }) + const hosts = JSON.parse(bridge.getAllowedWebViewHosts()) + assert.strictEqual(hosts.filter(h => h === 'test.example.com').length, 1) + assert.ok(hosts.includes('other.example.com')) + }) + }) + + describe('send()', () => { + it('forwards the message to the main process via the bridge channel', () => { + const bridge = loadPreload() + bridge.send('{"type":"record"}') + sinon.assert.calledOnceWithExactly(ipcRenderer.send, BRIDGE_CHANNEL, '{"type":"record"}') + }) + }) + + describe('when contextIsolation is disabled', () => { + it('does not throw and still sets bridge on window', () => { + contextBridge.exposeInMainWorld.throws(new Error('contextIsolation is disabled')) + const bridge = loadPreload() + assert.ok(bridge) + assert.strictEqual(bridge.getCapabilities(), '[]') + }) + }) +}) diff --git a/packages/datadog-instrumentations/test/fetch.spec.js b/packages/datadog-instrumentations/test/fetch.spec.js new file mode 100644 index 00000000000..7f15e04f8ed --- /dev/null +++ b/packages/datadog-instrumentations/test/fetch.spec.js @@ -0,0 +1,25 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { before, describe, it } = require('mocha') + +describe('packages/datadog-instrumentations/src/fetch.js', () => { + describe('globalThis.fetch identity', () => { + let nameBefore + + before(() => { + nameBefore = globalThis.fetch.name + require('../src/fetch') + }) + + it('preserves the fetch function name after dd-trace loads', () => { + assert.equal(nameBefore, 'fetch') + assert.equal(globalThis.fetch.name, 'fetch') + }) + + it('forwards calls to the wrapped fetch without recursion', () => { + return assert.rejects(globalThis.fetch('not-a-real-protocol://x'), TypeError) + }) + }) +}) diff --git a/packages/datadog-instrumentations/test/helpers/kafka.spec.js b/packages/datadog-instrumentations/test/helpers/kafka.spec.js new file mode 100644 index 00000000000..93f4ecf4e02 --- /dev/null +++ b/packages/datadog-instrumentations/test/helpers/kafka.spec.js @@ -0,0 +1,48 @@ +'use strict' + +const assert = require('node:assert/strict') +const { describe, it } = require('mocha') + +const { cloneMessages } = require('../../src/helpers/kafka') + +describe('helpers/kafka', () => { + describe('cloneMessages', () => { + it('returns a fresh array; the input is not the output', () => { + const input = [{ key: 'k', value: 'v' }] + const output = cloneMessages(input, false) + assert.notStrictEqual(output, input) + assert.notStrictEqual(output[0], input[0]) + }) + + it('keeps the headers field absent when ensureHeaders is false and input has none', () => { + const input = [{ key: 'k', value: 'v' }] + const [cloned] = cloneMessages(input, false) + assert.strictEqual(Object.hasOwn(cloned, 'headers'), false) + assert.strictEqual(input[0].headers, undefined) + }) + + it('seeds an empty headers object when ensureHeaders is true and input has none', () => { + const input = [{ key: 'k', value: 'v' }] + const [cloned] = cloneMessages(input, true) + assert.deepStrictEqual(cloned.headers, {}) + assert.strictEqual(input[0].headers, undefined) + }) + + it('shallow-clones existing headers so caller mutations stay isolated', () => { + const headers = { foo: 'bar' } + const [withHeaders] = cloneMessages([{ key: 'k', value: 'v', headers }], false) + const [withInjection] = cloneMessages([{ key: 'k', value: 'v', headers }], true) + assert.notStrictEqual(withHeaders.headers, headers) + assert.notStrictEqual(withInjection.headers, headers) + assert.deepStrictEqual(withHeaders.headers, headers) + assert.deepStrictEqual(withInjection.headers, headers) + }) + + it('passes non-object entries through unchanged regardless of ensureHeaders', () => { + const sentinel = Symbol('not-a-message') + const input = [null, sentinel, 0, ''] + assert.deepStrictEqual(cloneMessages(input, false), [null, sentinel, 0, '']) + assert.deepStrictEqual(cloneMessages(input, true), [null, sentinel, 0, '']) + }) + }) +}) diff --git a/packages/datadog-plugin-apollo/src/gateway/request.js b/packages/datadog-plugin-apollo/src/gateway/request.js index c31695ed6d9..a5ed3ccc4f5 100644 --- a/packages/datadog-plugin-apollo/src/gateway/request.js +++ b/packages/datadog-plugin-apollo/src/gateway/request.js @@ -2,8 +2,7 @@ const { storage } = require('../../../datadog-core') const ApolloBasePlugin = require('../../../dd-trace/src/plugins/apollo') - -let tools +const { getSignature } = require('../../../datadog-plugin-graphql/src/utils') const OPERATION_DEFINITION = 'OperationDefinition' const FRAGMENT_DEFINITION = 'FragmentDefinition' @@ -100,23 +99,4 @@ function buildOperationContext (schema, operationDocument, operationName) { } } -function getSignature (document, operationName, operationType, calculate) { - if (calculate !== false && tools !== false) { - try { - try { - tools = tools || require('../../../datadog-plugin-graphql/src/tools') - } catch (e) { - tools = false - throw e - } - - return tools.defaultEngineReportingSignature(document, operationName) - } catch { - // safety net - } - } - - return [operationType, operationName].filter(Boolean).join(' ') -} - module.exports = ApolloGatewayRequestPlugin diff --git a/packages/datadog-plugin-aws-sdk/src/base.js b/packages/datadog-plugin-aws-sdk/src/base.js index bdfd4d50e66..6418c57b29a 100644 --- a/packages/datadog-plugin-aws-sdk/src/base.js +++ b/packages/datadog-plugin-aws-sdk/src/base.js @@ -3,9 +3,8 @@ const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const ClientPlugin = require('../../dd-trace/src/plugins/client') const { storage } = require('../../datadog-core') -const { isTrue } = require('../../dd-trace/src/util') const { tagsFromRequest, tagsFromResponse } = require('../../dd-trace/src/payload-tagging') -const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper') +const getConfig = require('../../dd-trace/src/config') const { IS_SERVERLESS } = require('../../dd-trace/src/serverless') const RESPONSE_SKIP_KEYS = new Set(['request', 'requestId', 'error', '$metadata']) @@ -245,16 +244,15 @@ class BaseAwsSdkPlugin extends ClientPlugin { if (!span || !response.request) return const params = response.request.params const operation = response.request.operation - const extraTags = this.generateTags(params, operation, response) || {} - const tags = { - 'aws.response.request_id': response.requestId, - 'resource.name': operation, - 'span.kind': 'client', - ...extraTags, - } + // `'span.kind': 'client'` is already set by the start-meta; SQS overrides via `generateTags`. + span.setTag('aws.response.request_id', response.requestId) + span.setTag('resource.name', operation) - span.addTags(tags) + const extraTags = this.generateTags(params, operation, response) + if (extraTags) { + span.addTags(extraTags) + } if (this.constructor.isPayloadReporter && this.cloudTaggingConfig.response) { const maxDepth = this.cloudTaggingConfig.maxDepth @@ -314,29 +312,25 @@ function normalizeConfig (config, serviceIdentifier) { const hooks = getHooks(config) let specificConfig = config[serviceIdentifier] - switch (typeof specificConfig) { - case 'undefined': - specificConfig = {} - break - case 'boolean': - specificConfig = { enabled: specificConfig } - break + if (typeof specificConfig === 'boolean') { + specificConfig = { enabled: specificConfig } } - // check if AWS batch propagation or AWS_[SERVICE] batch propagation is enabled via env variable + // Check if AWS batch propagation or AWS_[SERVICE] batch propagation is enabled via env variable + const tracerConfig = getConfig() const serviceId = serviceIdentifier.toUpperCase() - const batchPropagationEnabled = isTrue( - specificConfig.batchPropagationEnabled ?? - getValueFromEnvSources(`DD_TRACE_AWS_SDK_${serviceId}_BATCH_PROPAGATION_ENABLED`) ?? - config.batchPropagationEnabled ?? - getValueFromEnvSources('DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED') + const serviceBatchKey = /** @type {import('../../dd-trace/src/config/config-types').ConfigPath} */( + `DD_TRACE_AWS_SDK_${serviceId}_BATCH_PROPAGATION_ENABLED` ) + const batchPropagationEnabled = tracerConfig.getOrigin(serviceBatchKey) === 'default' + ? tracerConfig.DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED + : tracerConfig[serviceBatchKey] // Merge the specific config back into the main config return { + batchPropagationEnabled, ...config, ...specificConfig, - batchPropagationEnabled, hooks, } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js b/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js index 5e19f15ede1..0e5151c5a4e 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js +++ b/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js @@ -6,7 +6,7 @@ class CloudwatchLogs extends BaseAwsSdkPlugin { static id = 'cloudwatchlogs' generateTags (params, operation) { - if (!params?.logGroupName) return {} + if (!params?.logGroupName) return return { 'resource.name': `${operation} ${params.logGroupName}`, diff --git a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js index 10f82bd6a7a..716fb3ef075 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +++ b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js @@ -7,7 +7,7 @@ class EventBridge extends BaseAwsSdkPlugin { static isPayloadReporter = true generateTags (params, operation, response) { - if (!params?.source) return {} + if (!params?.source) return const rulename = params.Name ?? '' return { 'resource.name': operation ? `${operation} ${params.source}` : params.source, diff --git a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js index daee18085a0..c8d102db054 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +++ b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js @@ -68,7 +68,7 @@ class Kinesis extends BaseAwsSdkPlugin { } generateTags (params, operation, response) { - if (!params || !params.StreamName) return {} + if (!params || !params.StreamName) return return { 'resource.name': `${operation} ${params.StreamName}`, diff --git a/packages/datadog-plugin-aws-sdk/src/services/lambda.js b/packages/datadog-plugin-aws-sdk/src/services/lambda.js index 99c7645b75d..2d3d81e5452 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/lambda.js +++ b/packages/datadog-plugin-aws-sdk/src/services/lambda.js @@ -7,7 +7,7 @@ class Lambda extends BaseAwsSdkPlugin { static id = 'lambda' generateTags (params, operation, response) { - if (!params?.FunctionName) return {} + if (!params?.FunctionName) return return { 'resource.name': `${operation} ${params.FunctionName}`, diff --git a/packages/datadog-plugin-aws-sdk/src/services/redshift.js b/packages/datadog-plugin-aws-sdk/src/services/redshift.js index 7bca0d23005..669c4e72bd4 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/redshift.js +++ b/packages/datadog-plugin-aws-sdk/src/services/redshift.js @@ -6,7 +6,7 @@ class Redshift extends BaseAwsSdkPlugin { static id = 'redshift' generateTags (params, operation, response) { - if (!params?.ClusterIdentifier) return {} + if (!params?.ClusterIdentifier) return return { 'resource.name': `${operation} ${params.ClusterIdentifier}`, diff --git a/packages/datadog-plugin-aws-sdk/src/services/s3.js b/packages/datadog-plugin-aws-sdk/src/services/s3.js index 5593c131f92..282eb6b10a7 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/s3.js +++ b/packages/datadog-plugin-aws-sdk/src/services/s3.js @@ -11,7 +11,7 @@ class S3 extends BaseAwsSdkPlugin { static isPayloadReporter = true generateTags (params, operation, response) { - if (!params?.Bucket) return {} + if (!params?.Bucket) return return { 'resource.name': `${operation} ${params.Bucket}`, diff --git a/packages/datadog-plugin-aws-sdk/src/services/sns.js b/packages/datadog-plugin-aws-sdk/src/services/sns.js index e3691595711..a98793f4407 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/sns.js +++ b/packages/datadog-plugin-aws-sdk/src/services/sns.js @@ -10,9 +10,9 @@ class Sns extends BaseAwsSdkPlugin { static isPayloadReporter = true generateTags (params, operation, response) { - if (!params) return {} + if (!params) return - if (!params.TopicArn && !(response.data && response.data.TopicArn)) return {} + if (!params.TopicArn && !(response.data && response.data.TopicArn)) return const TopicArn = params.TopicArn || response.data.TopicArn // Get the topic name from the last `:`-delimited segment of the ARN diff --git a/packages/datadog-plugin-aws-sdk/src/services/sqs.js b/packages/datadog-plugin-aws-sdk/src/services/sqs.js index 893431aacec..31b37b19752 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/sqs.js +++ b/packages/datadog-plugin-aws-sdk/src/services/sqs.js @@ -99,7 +99,7 @@ class Sqs extends BaseAwsSdkPlugin { } generateTags (params, operation, response) { - if (!params || (!params.QueueName && !params.QueueUrl)) return {} + if (!params || (!params.QueueName && !params.QueueUrl)) return const queueMetadata = extractQueueMetadata(params.QueueUrl) const queueName = queueMetadata?.queueName || params.QueueName diff --git a/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js b/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js index bba915e45a5..283ccd22be0 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +++ b/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js @@ -28,7 +28,7 @@ class Stepfunctions extends BaseAwsSdkPlugin { // } generateTags (params, operation, response) { - if (!params) return {} + if (!params) return const tags = { 'resource.name': params.name ? `${operation} ${params.name}` : `${operation}` } if (operation === 'startExecution' || operation === 'startSyncExecution') { tags.statemachinearn = `${params.stateMachineArn}` diff --git a/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js b/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js index 997a531a21e..2d575485b18 100644 --- a/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js @@ -7,17 +7,41 @@ const semver = require('semver') const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { setup, sort } = require('./spec_helpers') +const { setup, sort, withAwsSdkV2Versions, withAwsSdkVersions } = require('./spec_helpers') describe('Plugin', () => { + // The config singleton is built lazily on the first `agent.load(...)` and is + // not rebuilt across describes, so any env var the singleton needs to see + // must be set before then. The 'with env variable _BATCH_PROPAGATION_ENABLED' + // describe asserts on these specific values; they're harmless to the other + // describes (which assert on programmatic config that wins in the `??` chain). + const ORIGINAL_BATCH_PROPAGATION_ENV = { + GLOBAL: process.env.DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED, + KINESIS: process.env.DD_TRACE_AWS_SDK_KINESIS_BATCH_PROPAGATION_ENABLED, + SQS: process.env.DD_TRACE_AWS_SDK_SQS_BATCH_PROPAGATION_ENABLED, + } + before(() => { + process.env.DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED = 'true' + process.env.DD_TRACE_AWS_SDK_KINESIS_BATCH_PROPAGATION_ENABLED = 'false' + process.env.DD_TRACE_AWS_SDK_SQS_BATCH_PROPAGATION_ENABLED = 'true' + }) + after(() => { + function restore (name, original) { + if (original === undefined) delete process.env[name] + else process.env[name] = original + } + restore('DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED', ORIGINAL_BATCH_PROPAGATION_ENV.GLOBAL) + restore('DD_TRACE_AWS_SDK_KINESIS_BATCH_PROPAGATION_ENABLED', ORIGINAL_BATCH_PROPAGATION_ENV.KINESIS) + restore('DD_TRACE_AWS_SDK_SQS_BATCH_PROPAGATION_ENABLED', ORIGINAL_BATCH_PROPAGATION_ENV.SQS) + }) + // TODO: use the Request class directly for generic tests // TODO: add test files for every service describe('aws-sdk direct import', function () { setup() - withVersions('aws-sdk', ['aws-sdk'], (version) => { + withAwsSdkV2Versions((version) => { if (semver.intersects(version, '>2.3.0')) { const S3 = require(`../../../versions/aws-sdk@${version}`).get('aws-sdk/clients/s3') const s3 = new S3({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1', s3ForcePathStyle: true }) @@ -74,7 +98,7 @@ describe('Plugin', () => { describe('aws-sdk', function () { setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let s3 let sqs diff --git a/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js b/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js index 28bb31bd6f6..c2b4be60384 100644 --- a/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js @@ -4,8 +4,7 @@ const assert = require('node:assert') const { describe, it, before, after } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const { models } = require('./fixtures/bedrockruntime') const serviceName = 'bedrock-service-name-test' @@ -13,7 +12,7 @@ describe('Plugin', () => { describe('aws-sdk (bedrockruntime)', function () { setup() - withVersions('aws-sdk', ['@aws-sdk/smithy-client', 'aws-sdk'], '>=3', (version, moduleName) => { + withAwsSdkVersions('>=3', (version, moduleName) => { let AWS let bedrockRuntimeClient diff --git a/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js b/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js index b82999994f8..ed049dd8e0b 100644 --- a/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js @@ -9,10 +9,9 @@ const { after, before, beforeEach, describe, it } = require('mocha') const { DYNAMODB_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const DynamoDb = require('../src/services/dynamodb') const { generatePointerHash } = require('../src/util') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') /* eslint-disable no-console */ async function resetLocalStackDynamo () { try { @@ -28,7 +27,7 @@ describe('Plugin', () => { setup() this.timeout(10000) - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let tracer let AWS let dynamo diff --git a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js index 1177856e45f..ecbb4fe18c3 100644 --- a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js @@ -7,12 +7,12 @@ const { before, describe, it } = require('mocha') const sinon = require('sinon') const EventBridge = require('../src/services/eventbridge') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const tracer = require('../../dd-trace') +const { withAwsSdkVersions } = require('./spec_helpers') describe('EventBridge', () => { let span - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let traceId let parentId let spanId @@ -70,7 +70,7 @@ describe('EventBridge', () => { const params = { foo: 'bar', } - assert.deepStrictEqual(eventbridge.generateTags(params, 'putEvent', {}), {}) + assert.strictEqual(eventbridge.generateTags(params, 'putEvent', {}), undefined) }) it('injects trace context to Eventbridge putEvents', () => { @@ -126,17 +126,17 @@ describe('EventBridge', () => { assert.deepStrictEqual(request.params, request.params) }) - it('returns an empty object when params is null', () => { + it('returns undefined when params is null', () => { const eventbridge = new EventBridge(tracer) - assert.deepStrictEqual(eventbridge.generateTags(null, 'putEvent', {}), {}) + assert.strictEqual(eventbridge.generateTags(null, 'putEvent', {}), undefined) }) - it('returns an empty object when params.source is an empty string', () => { + it('returns undefined when params.source is an empty string', () => { const eventbridge = new EventBridge(tracer) const params = { source: '', } - assert.deepStrictEqual(eventbridge.generateTags(params, 'putEvent', {}), {}) + assert.strictEqual(eventbridge.generateTags(params, 'putEvent', {}), undefined) }) it('sets rulename as an empty string when params.Name is null', () => { diff --git a/packages/datadog-plugin-aws-sdk/test/integration-test/client.spec.js b/packages/datadog-plugin-aws-sdk/test/integration-test/client.spec.js index d2dcc9243d2..f0959ce7f0c 100644 --- a/packages/datadog-plugin-aws-sdk/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/integration-test/client.spec.js @@ -11,13 +11,13 @@ const { varySandbox, stopProc, } = require('../../../../integration-tests/helpers') -const { withVersions } = require('../../../dd-trace/test/setup/mocha') +const { withAwsSdkV2Versions } = require('../spec_helpers') describe('esm', () => { let agent let proc let variants - withVersions('aws-sdk', ['aws-sdk'], version => { + withAwsSdkV2Versions(version => { useSandbox([`'aws-sdk@${version}'`], false, [ './packages/datadog-plugin-aws-sdk/test/integration-test/*']) diff --git a/packages/datadog-plugin-aws-sdk/test/integration-test/sqs.spec.js b/packages/datadog-plugin-aws-sdk/test/integration-test/sqs.spec.js index f4398267b47..2d5b1029412 100644 --- a/packages/datadog-plugin-aws-sdk/test/integration-test/sqs.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/integration-test/sqs.spec.js @@ -9,13 +9,13 @@ const { spawnPluginIntegrationTestProcAndExpectExit, stopProc, } = require('../../../../integration-tests/helpers') -const { withVersions } = require('../../../dd-trace/test/setup/mocha') +const { withAwsSdkV3Versions } = require('../spec_helpers') describe('recursion regression test', () => { let agent let proc - withVersions('aws-sdk', ['@aws-sdk/smithy-client'], version => { + withAwsSdkV3Versions(version => { useSandbox([`'@aws-sdk/client-sqs'@${version}'`], false, [ './packages/datadog-plugin-aws-sdk/test/integration-test/*']) diff --git a/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js b/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js index 07f3bb281c9..c1a12fb1174 100644 --- a/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js @@ -6,20 +6,19 @@ const { afterEach, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const id = require('../../dd-trace/src/id') const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') const propagationHash = require('../../dd-trace/src/propagation-hash') const helpers = require('./kinesis_helpers') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') describe('Kinesis', function () { this.timeout(10000) setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let kinesis let tracer diff --git a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js index 70c8a99d2d0..acd0f0ef455 100644 --- a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js @@ -5,10 +5,10 @@ const assert = require('node:assert/strict') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') +const { withNamingSchema } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const id = require('../../dd-trace/src/id') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const helpers = require('./kinesis_helpers') const { rawExpectedSchema } = require('./kinesis-naming') @@ -16,7 +16,7 @@ describe('Kinesis', function () { this.timeout(10000) setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let kinesis diff --git a/packages/datadog-plugin-aws-sdk/test/lambda.spec.js b/packages/datadog-plugin-aws-sdk/test/lambda.spec.js index f2a1dade09c..f849bc5c5da 100644 --- a/packages/datadog-plugin-aws-sdk/test/lambda.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/lambda.spec.js @@ -6,10 +6,10 @@ const JSZip = require('jszip') const { after, before, describe, it } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') -const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') +const { withNamingSchema } = require('../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') const { rawExpectedSchema } = require('./lambda-naming') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const zip = new JSZip() @@ -20,7 +20,7 @@ describe('Plugin', () => { this.timeout(10000) setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let lambda let tracer diff --git a/packages/datadog-plugin-aws-sdk/test/s3.spec.js b/packages/datadog-plugin-aws-sdk/test/s3.spec.js index a5062234bb8..815c251efb9 100644 --- a/packages/datadog-plugin-aws-sdk/test/s3.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/s3.spec.js @@ -7,10 +7,10 @@ const { after, before, describe, it } = require('mocha') const { S3_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') -const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') +const { withNamingSchema, withPeerService } = require('../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') const { rawExpectedSchema } = require('./s3-naming') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const bucketName = 's3-bucket-name-test' @@ -28,7 +28,7 @@ describe('Plugin', () => { describe('aws-sdk (s3)', function () { setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let s3 let tracer diff --git a/packages/datadog-plugin-aws-sdk/test/serverless-peer-service.spec.js b/packages/datadog-plugin-aws-sdk/test/serverless-peer-service.spec.js index feeea073358..d5550c6ff6a 100644 --- a/packages/datadog-plugin-aws-sdk/test/serverless-peer-service.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/serverless-peer-service.spec.js @@ -6,9 +6,8 @@ const { promisify } = require('node:util') const { after, before, describe, it } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const helpers = require('./kinesis_helpers') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') describe('Plugin', () => { describe('Serverless', function () { @@ -17,7 +16,7 @@ describe('Plugin', () => { this.timeout(15000) setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS before(async () => { diff --git a/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js b/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js index a69144fcefa..d897168518f 100644 --- a/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js @@ -10,15 +10,14 @@ const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') const propagationHash = require('../../dd-trace/src/propagation-hash') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') describe('Sns', function () { setup() this.timeout(20000) - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let sns let sqs let subParams diff --git a/packages/datadog-plugin-aws-sdk/test/sns.spec.js b/packages/datadog-plugin-aws-sdk/test/sns.spec.js index f87f7970d56..9bdb25ca5c8 100644 --- a/packages/datadog-plugin-aws-sdk/test/sns.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sns.spec.js @@ -6,16 +6,16 @@ const { after, before, describe, it } = require('mocha') const semver = require('semver') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') +const { withNamingSchema, withPeerService } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const { rawExpectedSchema } = require('./sns-naming') describe('Sns', function () { setup() this.timeout(20000) - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let sns let sqs let subParams diff --git a/packages/datadog-plugin-aws-sdk/test/spec_helpers.js b/packages/datadog-plugin-aws-sdk/test/spec_helpers.js index 25309929e25..0273b7a9e1b 100644 --- a/packages/datadog-plugin-aws-sdk/test/spec_helpers.js +++ b/packages/datadog-plugin-aws-sdk/test/spec_helpers.js @@ -1,9 +1,76 @@ 'use strict' +const { withVersions } = require('../../dd-trace/test/setup/mocha') +const { NODE_MAJOR } = require('../../../version') + +const AWS_SDK_V3_RANGE = NODE_MAJOR === 18 ? '3.0.0' : '>3.0.0' + const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1) +/** + * @callback AwsSdkVersionCallback + * @param {string} version + * @param {string} moduleName + * @param {string} resolvedVersion + * @returns {void} + */ + +/** + * @param {string|AwsSdkVersionCallback} range + * @param {AwsSdkVersionCallback} [cb] + * @returns {void} + */ +function withAwsSdkV2Versions (range, cb) { + if (typeof range === 'function') { + cb = range + range = undefined + } + + withVersions('aws-sdk', ['aws-sdk'], range, cb) +} + +/** + * @param {string|AwsSdkVersionCallback} range + * @param {AwsSdkVersionCallback} [cb] + * @returns {void} + */ +function withAwsSdkV3Versions (range, cb) { + if (typeof range === 'function') { + cb = range + range = undefined + } + + withVersions('aws-sdk', ['@aws-sdk/smithy-client'], getAwsSdkV3Range(range), cb) +} + +/** + * @param {string|AwsSdkVersionCallback} range + * @param {AwsSdkVersionCallback} [cb] + * @returns {void} + */ +function withAwsSdkVersions (range, cb) { + if (typeof range === 'function') { + cb = range + range = undefined + } + + withAwsSdkV2Versions(range, cb) + withAwsSdkV3Versions(range, cb) +} + +/** + * @param {string|undefined} range + * @returns {string} + */ +function getAwsSdkV3Range (range) { + return range === undefined ? AWS_SDK_V3_RANGE : `${range} ${AWS_SDK_V3_RANGE}` +} + const helpers = { sort, + withAwsSdkV2Versions, + withAwsSdkV3Versions, + withAwsSdkVersions, setup () { before(() => { diff --git a/packages/datadog-plugin-aws-sdk/test/sqs-inject-to-message.spec.js b/packages/datadog-plugin-aws-sdk/test/sqs-inject-to-message.spec.js index 39a91d0d564..3bc43e27f35 100644 --- a/packages/datadog-plugin-aws-sdk/test/sqs-inject-to-message.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sqs-inject-to-message.spec.js @@ -14,11 +14,11 @@ const Sqs = require('../src/services/sqs') * a hand-rolled stub is enough to exercise the inject-time ordering. * * @param {object} options - * @param {boolean} [options.dsmEnabled=false] + * @param {boolean} [options.dsmEnabled] * @param {(span: unknown, format: string, info: object) => void} [options.inject] * Stand-in for `tracer.inject` that may populate the trace context * in `info`. - * @param {unknown} [options.dataStreamsContext=null] + * @param {unknown} [options.dataStreamsContext] * Value returned by the stubbed `setDSMCheckpoint`. * @returns {Sqs & { dsmCalls: Array<{ datadog: object | undefined }> }} */ diff --git a/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js b/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js index bab0deb28f4..a500dff9bb3 100644 --- a/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js @@ -11,9 +11,8 @@ const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') const propagationHash = require('../../dd-trace/src/propagation-hash') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const getQueueParams = (queueName) => { return { @@ -29,7 +28,7 @@ describe('Plugin', () => { this.timeout(10000) setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let sqs let queueNameDSM diff --git a/packages/datadog-plugin-aws-sdk/test/sqs.spec.js b/packages/datadog-plugin-aws-sdk/test/sqs.spec.js index 23adfc6dbc9..0d37d79c4e9 100644 --- a/packages/datadog-plugin-aws-sdk/test/sqs.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sqs.spec.js @@ -5,9 +5,9 @@ const { randomUUID } = require('node:crypto') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') -const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') +const { withNamingSchema, withPeerService } = require('../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const { rawExpectedSchema } = require('./sqs-naming') const getQueueParams = (queueName) => { @@ -24,7 +24,7 @@ describe('Plugin', () => { this.timeout(10000) setup() - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let AWS let sqs let queueName diff --git a/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js b/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js index 22a02aea4ce..d70d196c17c 100644 --- a/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js @@ -6,8 +6,7 @@ const { afterEach, before, beforeEach, describe, it } = require('mocha') const semver = require('semver') const agent = require('../../dd-trace/test/plugins/agent') -const { withVersions } = require('../../dd-trace/test/setup/mocha') -const { setup } = require('./spec_helpers') +const { setup, withAwsSdkVersions } = require('./spec_helpers') const helloWorldSMD = { Comment: 'A Hello World example of the Amazon States Language using a Pass state', StartAt: 'HelloWorld', @@ -23,7 +22,7 @@ const helloWorldSMD = { describe('Sfn', () => { let tracer - withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + withAwsSdkVersions((version, moduleName) => { let stateMachineArn let client diff --git a/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js b/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js index ae867be59e7..1484c0954c6 100644 --- a/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js +++ b/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js @@ -288,24 +288,65 @@ describe('Plugin', () => { }) it('should hit an error for the first send and not inject headers in later sends', async () => { - const testMessages = [{ key: 'key1', value: 'test1' }] - const testMessages2 = [{ key: 'key2', value: 'test2' }] - - try { - await producer.send({ topic: testTopic, messages: testMessages }) - assert.fail('First producer.send() should have thrown an error') - } catch (e) { - assert.strictEqual(e, error) + const dc = require('dc-polyfill') + const { deepFreeze } = require('../../../integration-tests/helpers') + + const startCh = dc.channel('apm:confluentinc-kafka-javascript:produce:start') + + // Snapshot headers at publish time. The underlying client + // converts each cloned message's `headers` to an array-of-pairs + // form before shipping, so the live reference would show that + // post-conversion shape; we want the bindStart-time injection + // result instead. + const headerSnapshots = [] + const headerPresence = [] + const captureStart = (ctx) => { + headerSnapshots.push(ctx.messages.map((m) => ( + m && typeof m === 'object' && m.headers ? { ...m.headers } : undefined + ))) + headerPresence.push(ctx.messages.map((m) => ( + m && typeof m === 'object' ? Object.hasOwn(m, 'headers') : null + ))) } - // Verify headers were injected in the first attempt - assert.ok(Object.hasOwn(testMessages[0].headers[0], 'x-datadog-trace-id')) + startCh.subscribe(captureStart) - // restore the stub to allow the next send to succeed - produceStub.restore() + // Deep-freeze the user's input on both sends; any boundary or + // library write to the array, its messages, or their headers + // throws synchronously. + const testMessages = deepFreeze([{ key: 'key1', value: 'test1' }]) + const testMessages2 = deepFreeze([{ key: 'key2', value: 'test2' }]) - const result = await producer.send({ topic: testTopic, messages: testMessages2 }) - assert.strictEqual(testMessages2[0].headers, null) - assert.notStrictEqual(result, undefined) + try { + await assert.rejects( + producer.send({ topic: testTopic, messages: testMessages }), + error + ) + + const firstHeaders = headerSnapshots.find( + (snapshot) => snapshot[0] && Object.hasOwn(snapshot[0], 'x-datadog-trace-id') + ) + assert.ok(firstHeaders, 'expected a captured batch with x-datadog-trace-id') + + const sendsBefore = headerSnapshots.length + produceStub.restore() + + const result = await producer.send({ topic: testTopic, messages: testMessages2 }) + + // After the broker reports ERR_UNKNOWN the producer skips + // injection. The boundary still clones, so the underlying + // client's `headers: null` post-publish mutation lands on the + // clone, not on the user's frozen array. The clone must not + // seed `headers: {}` either: brokers that reject any header + // field cannot recover otherwise. + const injectedAfterError = headerSnapshots + .slice(sendsBefore) + .filter((snap) => snap[0] && Object.hasOwn(snap[0], 'x-datadog-trace-id')) + assert.strictEqual(injectedAfterError.length, 0) + assert.deepStrictEqual(headerPresence[sendsBefore], [false]) + assert.notStrictEqual(result, undefined) + } finally { + startCh.unsubscribe(captureStart) + } }) }) }) diff --git a/packages/datadog-plugin-couchbase/src/index.js b/packages/datadog-plugin-couchbase/src/index.js index 9130cb900b6..b6b811f51a5 100644 --- a/packages/datadog-plugin-couchbase/src/index.js +++ b/packages/datadog-plugin-couchbase/src/index.js @@ -1,18 +1,49 @@ 'use strict' const StoragePlugin = require('../../dd-trace/src/plugins/storage') -const { storage } = require('../../datadog-core') class CouchBasePlugin extends StoragePlugin { static id = 'couchbase' static peerServicePrecursors = ['db.couchbase.seed.nodes'] - addBinds (func, start) { - this.addBind(`apm:couchbase:${func}:start`, start) - this.addSub(`apm:couchbase:${func}:error`, ({ error }) => this.addError(error)) - this.addSub(`apm:couchbase:${func}:finish`, ctx => this.finish(ctx)) - this.addBind(`apm:couchbase:${func}:callback:start`, callbackStart) - this.addBind(`apm:couchbase:${func}:callback:finish`, callbackFinish) + constructor (...args) { + super(...args) + + this.#addOpSubs('query', (ctx) => { + const { resource, bucket, seedNodes } = ctx + this.startSpan( + 'query', + { + 'span.type': 'sql', + 'resource.name': resource, + 'span.kind': this.constructor.kind, + }, + { bucket, seedNodes }, + ctx + ) + return ctx.currentStore + }) + + for (const op of ['upsert', 'insert', 'replace']) { + this.#addOpSubs(op, (ctx) => { + const { bucket, collection, seedNodes } = ctx + this.startSpan(op, {}, { bucket, collection, seedNodes }, ctx) + return ctx.currentStore + }) + } + } + + /** + * @param {string} op Operation name (`query`, `upsert`, ...). + * @param {(ctx: object) => object} bindStart Operation-specific span starter. + */ + #addOpSubs (op, bindStart) { + const prefix = `tracing:apm:couchbase:${op}` + this.addBind(`${prefix}:start`, bindStart) + this.addBind(`${prefix}:asyncStart`, bindAsyncStart) + this.addSub(`${prefix}:asyncEnd`, finishSpan) + this.addSub(`${prefix}:end`, finishSpanIfSync) + this.addSub(`${prefix}:error`, setSpanError) } startSpan (operation, customTags, { bucket, collection, seedNodes }, ctx) { @@ -27,8 +58,8 @@ class CouchBasePlugin extends StoragePlugin { if (bucket) tags['couchbase.bucket.name'] = bucket.name if (collection) tags['couchbase.collection.name'] = collection.name - for (const tag in customTags) { - tags[tag] = customTags[tag] + for (const key of Object.keys(customTags)) { + tags[key] = customTags[key] } return super.startSpan( @@ -40,55 +71,30 @@ class CouchBasePlugin extends StoragePlugin { ctx ) } +} - constructor (...args) { - super(...args) - - this.addBinds('query', (ctx) => { - const { resource, bucket, seedNodes } = ctx - - this.startSpan( - 'query', - { - 'span.type': 'sql', - 'resource.name': resource, - 'span.kind': this.constructor.kind, - }, - { bucket, seedNodes }, - ctx - ) - - return ctx.currentStore - }) - this.addBind('apm:couchbase:bucket:maybeInvoke:callback:start', callbackStart) - this.addBind('apm:couchbase:bucket:maybeInvoke:callback:finish', callbackFinish) - this.addBind('apm:couchbase:cluster:maybeInvoke:callback:start', callbackStart) - this.addBind('apm:couchbase:cluster:maybeInvoke:callback:finish', callbackFinish) - - this._addCommandSubs('upsert') - this._addCommandSubs('insert') - this._addCommandSubs('replace') - this._addCommandSubs('append') - this._addCommandSubs('prepend') - } - - _addCommandSubs (name) { - this.addBinds(name, (ctx) => { - const { bucket, collection, seedNodes } = ctx +function bindAsyncStart (ctx) { + return ctx.parentStore +} - this.startSpan(name, {}, { bucket, collection, seedNodes }, ctx) - return ctx.currentStore - }) - } +function finishSpan (ctx) { + ctx.currentStore?.span?.finish() } -function callbackStart (ctx) { - ctx.parentStore = storage('legacy').getStore() - return ctx.parentStore +// `end` fires synchronously after the wrapped function returns. For async +// resolutions ctx.result and ctx.error are still unset at that point and the +// span is closed later via asyncEnd. For a sync throw or sync-resolved +// callback, this is the only finalization signal. +function finishSpanIfSync (ctx) { + if ((ctx.error !== undefined || ctx.result !== undefined) && ctx.currentStore?.span) { + ctx.currentStore.span.finish() + } } -function callbackFinish (ctx) { - return ctx.parentStore +function setSpanError (ctx) { + if (ctx.error && ctx.currentStore?.span) { + ctx.currentStore.span.setTag('error', ctx.error) + } } module.exports = CouchBasePlugin diff --git a/packages/datadog-plugin-couchbase/test/index.spec.js b/packages/datadog-plugin-couchbase/test/index.spec.js index 935637f757d..0cb245a2121 100644 --- a/packages/datadog-plugin-couchbase/test/index.spec.js +++ b/packages/datadog-plugin-couchbase/test/index.spec.js @@ -24,151 +24,6 @@ describe('Plugin', () => { tracer = global.tracer = require('../../dd-trace') }) - withVersions('couchbase', 'couchbase', '<3.0.0', version => { - let N1qlQuery - // skipping tests due to bug with couchbase integration that is blocking CI. - // TODO: diagnose and fix failures. Link to bug issue: https://github.com/DataDog/dd-trace-js/issues/6400 - describe.skip('without configuration', () => { - beforeEach(done => { - agent.load('couchbase').then(() => { - couchbase = proxyquire(`../../../versions/couchbase@${version}`, {}).get() - N1qlQuery = couchbase.N1qlQuery - cluster = new couchbase.Cluster('localhost:8091') - cluster.authenticate('Administrator', 'password') - cluster.enableCbas('localhost:8095') - bucket = cluster.openBucket('datadog-test', (err) => done(err)) - }) - }) - - afterEach(() => { - bucket.disconnect() - }) - - after(() => { - return agent.close({ ritmReset: false }) - }) - - withNamingSchema( - done => cluster.query(N1qlQuery.fromString('SELECT 1+1'), err => err && done(err)), - rawExpectedSchema.query - ) - - it('should run the Query callback in the parent context', done => { - const query = 'SELECT 1+1' - const span = tracer.startSpan('test.query.cb') - - tracer.scope().activate(span, () => { - const n1qlQuery = N1qlQuery.fromString(query) - cluster.query(n1qlQuery, (err, rows) => { - assert.strictEqual(tracer.scope().active(), span) - done(err) - }) - }) - }) - - it('should run any Bucket operations in the parent context', done => { - const span = tracer.startSpan('test') - - tracer.scope().activate(span, () => { - bucket.get('1', () => { - assert.strictEqual(tracer.scope().active(), span) - done() - }) - }) - }) - - describe('queries on cluster', () => { - it('should handle N1QL queries', done => { - const query = 'SELECT 1+1' - - agent - .assertFirstTraceSpan({ - name: expectedSchema.query.opName, - service: expectedSchema.query.serviceName, - resource: query, - type: 'sql', - meta: { - 'span.kind': 'client', - 'couchbase.bucket.name': 'datadog-test', - component: 'couchbase', - '_dd.integration': 'couchbase', - }, - }) - .then(done) - .catch(done) - - const n1qlQuery = N1qlQuery.fromString(query) - cluster.query(n1qlQuery, (err) => { - if (err) done(err) - }) - - if (semver.intersects(version, '2.4.0 - 2.5.0')) { - // Due to bug JSCBC-491 in Couchbase, we have to reconnect to dispatch waiting queries - const triggerBucket = cluster.openBucket('datadog-test', (err) => { - if (err) done(err) - }) - triggerBucket.on('connect', () => triggerBucket.disconnect()) - } - }) - - it('should handle storage queries', done => { - agent - .assertFirstTraceSpan({ - name: expectedSchema.upsert.opName, - service: expectedSchema.upsert.serviceName, - resource: 'couchbase.upsert', - meta: { - 'span.kind': 'client', - 'couchbase.bucket.name': 'datadog-test', - component: 'couchbase', - }, - }) - .then(done) - .catch(done) - - bucket.upsert('testdoc', { name: 'Frank' }, (err, result) => { - if (err) done(err) - }) - }) - - it('should skip instrumentation for invalid arguments', (done) => { - try { - bucket.upsert('testdoc', { name: 'Frank' }) - } catch (e) { - assert.strictEqual(e.message, 'Third argument needs to be an object or callback.') - done() - } - }) - }) - - describe('queries on buckets', () => { - it('should handle N1QL queries', done => { - const query = 'SELECT 1+2' - - agent - .assertFirstTraceSpan({ - name: expectedSchema.query.opName, - service: expectedSchema.query.serviceName, - resource: query, - type: 'sql', - meta: { - 'span.kind': 'client', - 'couchbase.bucket.name': 'datadog-test', - component: 'couchbase', - }, - }) - .then(done) - .catch(done) - - const n1qlQuery = N1qlQuery.fromString(query) - bucket.query(n1qlQuery, (err) => { - if (err) done(err) - }) - }) - }) - }) - }) - withVersions('couchbase', 'couchbase', '>=3.0.0', version => { beforeEach(() => { tracer = global.tracer = require('../../dd-trace') diff --git a/packages/datadog-plugin-cucumber/src/index.js b/packages/datadog-plugin-cucumber/src/index.js index a108fdd3be8..c73d2fea2cc 100644 --- a/packages/datadog-plugin-cucumber/src/index.js +++ b/packages/datadog-plugin-cucumber/src/index.js @@ -141,6 +141,7 @@ class CucumberPlugin extends CiPlugin { 'cucumber' ), ...this.getSessionRequestErrorTags(), + ...this.getSessionItrSkippingEnabledTags(), } if (isUnskippable) { this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' }) diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index b3c12382f4c..cb7cd7b4206 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -31,11 +31,13 @@ const { TEST_SKIPPED_BY_ITR, TEST_ITR_UNSKIPPABLE, TEST_ITR_FORCED_RUN, + TEST_ITR_SKIPPING_ENABLED, ITR_CORRELATION_ID, TEST_SOURCE_FILE, TEST_IS_NEW, TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, + TEST_EARLY_FLAKE_ABORT_REASON, getTestSessionName, TEST_SESSION_NAME, TEST_LEVEL_EVENT_TYPES, @@ -52,19 +54,26 @@ const { getPullRequestDiff, getModifiedFilesFromDiff, getSessionRequestErrorTags, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, + getSessionItrSkippingEnabledTags, TEST_IS_MODIFIED, TEST_HAS_DYNAMIC_NAME, + getIsFaultyEarlyFlakeDetection, DYNAMIC_NAME_RE, recordAttemptToFixExecution, logAttemptToFixTestExecution, logTestOptimizationSummary, + getEfdRetryCount, + getMaxEfdRetryCount, getPullRequestBaseBranch, TEST_FINAL_STATUS, } = require('../../dd-trace/src/plugins/util/test') const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util') const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants') -const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper') +const getConfig = require('../../dd-trace/src/config') const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry') const log = require('../../dd-trace/src/log') @@ -263,6 +272,36 @@ function getSuiteStatus (suiteStats) { return 'pass' } +function getMatchingCypressTest (cypressTests, testName, attemptIndex, testStatus, preferIndexedMatch = false) { + let matchingTestByIndex + let matchingTestByStatus + let matchingTestIndex = 0 + + for (const cypressTest of cypressTests) { + if (cypressTest.title.join(' ') !== testName) { + continue + } + + if (matchingTestIndex === attemptIndex) { + matchingTestByIndex = cypressTest + } + matchingTestIndex++ + + if (!matchingTestByStatus && CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state] === testStatus) { + matchingTestByStatus = cypressTest + } + } + + return preferIndexedMatch + ? matchingTestByIndex || matchingTestByStatus + : matchingTestByStatus || matchingTestByIndex +} + +function isCypressHookFailure (cypressTest) { + return CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state] === 'fail' && + /\bhook\b/.test(String(cypressTest.displayError || '')) +} + const FINAL_STATUS_RETRY_KIND = { none: 'none', atr: 'atr', @@ -320,14 +359,20 @@ class CypressPlugin { finishedTestsByFile = {} testStatuses = {} + hasLibraryConfiguration = false isTestsSkipped = false isSuitesSkippingEnabled = false isCodeCoverageEnabled = false isFlakyTestRetriesEnabled = false flakyTestRetriesCount = 0 isEarlyFlakeDetectionEnabled = false + isEarlyFlakeDetectionFaulty = false isKnownTestsEnabled = false earlyFlakeDetectionNumRetries = 0 + earlyFlakeDetectionSlowTestRetries = {} + efdRetryCountByTest = {} + efdSlowAbortedTests = {} + earlyFlakeDetectionFaultyThreshold = 0 testsToSkip = [] skippedTests = [] hasForcedToRunSuites = false @@ -393,14 +438,20 @@ class CypressPlugin { this._isInit = false this.finishedTestsByFile = {} this.testStatuses = {} + this.hasLibraryConfiguration = false this.isTestsSkipped = false this.isSuitesSkippingEnabled = false this.isCodeCoverageEnabled = false this.isFlakyTestRetriesEnabled = false this.flakyTestRetriesCount = 0 this.isEarlyFlakeDetectionEnabled = false + this.isEarlyFlakeDetectionFaulty = false this.isKnownTestsEnabled = false this.earlyFlakeDetectionNumRetries = 0 + this.earlyFlakeDetectionSlowTestRetries = {} + this.efdRetryCountByTest = {} + this.efdSlowAbortedTests = {} + this.earlyFlakeDetectionFaultyThreshold = 0 this.testsToSkip = [] this.skippedTests = [] this.hasForcedToRunSuites = false @@ -454,8 +505,7 @@ class CypressPlugin { this.isTestIsolationEnabled = getIsTestIsolationEnabled(cypressConfig) - const envFlushWait = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) - this.rumFlushWaitMillis = Number.isFinite(envFlushWait) ? envFlushWait : undefined + this.rumFlushWaitMillis = getConfig().DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS if (!this.isTestIsolationEnabled) { log.warn('Test isolation is disabled, retries will not be enabled') @@ -471,16 +521,19 @@ class CypressPlugin { if (libraryConfigurationResponse.err) { log.error('Cypress plugin library config response error', libraryConfigurationResponse.err) this._pendingRequestErrorTags.push({ - tag: DD_CI_LIBRARY_CONFIGURATION_ERROR, + tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, value: 'true', }) } else { + this.hasLibraryConfiguration = true const { libraryConfig: { isSuitesSkippingEnabled, isCodeCoverageEnabled, isEarlyFlakeDetectionEnabled, earlyFlakeDetectionNumRetries, + earlyFlakeDetectionSlowTestRetries, + earlyFlakeDetectionFaultyThreshold, isFlakyTestRetriesEnabled, flakyTestRetriesCount, isKnownTestsEnabled, @@ -493,6 +546,8 @@ class CypressPlugin { this.isCodeCoverageEnabled = isCodeCoverageEnabled this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries + this.earlyFlakeDetectionSlowTestRetries = earlyFlakeDetectionSlowTestRetries ?? {} + this.earlyFlakeDetectionFaultyThreshold = earlyFlakeDetectionFaultyThreshold this.isKnownTestsEnabled = isKnownTestsEnabled if (isFlakyTestRetriesEnabled && this.isTestIsolationEnabled) { this.isFlakyTestRetriesEnabled = true @@ -533,9 +588,76 @@ class CypressPlugin { return { isAttemptToFix, isDisabled, isQuarantined } } + /** + * Returns how many EFD retries must be scheduled before the first duration is known. + * + * @returns {number} + */ + getConfiguredEfdRetryCount () { + const { earlyFlakeDetectionSlowTestRetries } = this + if (!earlyFlakeDetectionSlowTestRetries || !Object.keys(earlyFlakeDetectionSlowTestRetries).length) { + return this.earlyFlakeDetectionNumRetries + } + return getMaxEfdRetryCount(earlyFlakeDetectionSlowTestRetries) + } + + /** + * Returns the selected EFD retry count for a test, or the scheduling count if it has not run yet. + * + * @param {string} testSuite + * @param {string} testName + * @returns {number} + */ + getEfdRetryCountForTest (testSuite, testName) { + const testSuiteRetries = this.efdRetryCountByTest[testSuite] + if (!testSuiteRetries || testSuiteRetries[testName] === undefined) { + return this.getConfiguredEfdRetryCount() + } + return testSuiteRetries[testName] + } + + /** + * Stores the selected EFD retry count for a test after its first execution duration is known. + * + * @param {string} testSuite + * @param {string} testName + * @param {number | undefined} duration + * @returns {number} + */ + setEfdRetryCountForTest (testSuite, testName, duration) { + if (!this.efdRetryCountByTest[testSuite]) { + this.efdRetryCountByTest[testSuite] = {} + } + const retryCount = getEfdRetryCount(duration ?? 0, this.earlyFlakeDetectionSlowTestRetries) + this.efdRetryCountByTest[testSuite][testName] = retryCount + if (retryCount === 0) { + if (!this.efdSlowAbortedTests[testSuite]) { + this.efdSlowAbortedTests[testSuite] = {} + } + this.efdSlowAbortedTests[testSuite][testName] = true + } + return retryCount + } + + /** + * Returns whether an EFD retry clone is beyond the selected retry count and should be discarded. + * + * @param {string} testSuite + * @param {string} testName + * @param {number} efdRetryIndex + * @returns {boolean} + */ + shouldSkipEfdRetry (testSuite, testName, efdRetryIndex) { + const testSuiteRetries = this.efdRetryCountByTest[testSuite] + return testSuiteRetries?.[testName] !== undefined && efdRetryIndex > testSuiteRetries[testName] + } + getTestSuiteSpan ({ testSuite, testSuiteAbsolutePath }) { - const testSuiteSpanMetadata = - getTestSuiteCommonTags(this.command, this.frameworkVersion, testSuite, TEST_FRAMEWORK_NAME) + const testSuiteSpanMetadata = { + ...getTestSuiteCommonTags(this.command, this.frameworkVersion, testSuite, TEST_FRAMEWORK_NAME), + ...this.getSessionRequestErrorTags(), + ...this.getSessionItrSkippingEnabledTags(), + } this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite') @@ -588,6 +710,7 @@ class CypressPlugin { if (testSourceFile) { testSpanMetadata[TEST_SOURCE_FILE] = testSourceFile } + Object.assign(testSpanMetadata, this.getSessionItrSkippingEnabledTags()) const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile }) if (codeOwners) { @@ -637,6 +760,15 @@ class CypressPlugin { return getSessionRequestErrorTags(this.testSessionSpan) } + /** + * Returns ITR skipping-enabled tags from the test session span for propagation to child events. + * + * @returns {Record} + */ + getSessionItrSkippingEnabledTags () { + return getSessionItrSkippingEnabledTags(this.testSessionSpan) + } + ciVisEvent (name, testLevel, tags = {}) { incrementCountMetric(name, { testLevel, @@ -662,10 +794,11 @@ class CypressPlugin { ) if (knownTestsResponse.err) { log.error('Cypress known tests response error', knownTestsResponse.err) + this._pendingRequestErrorTags.push({ tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, value: 'true' }) this.isEarlyFlakeDetectionEnabled = false this.isKnownTestsEnabled = false } else { - if (knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]) { + if (knownTestsResponse.knownTests?.[TEST_FRAMEWORK_NAME]) { this.knownTestsByTestSuite = knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME] } else { this.isEarlyFlakeDetectionEnabled = false @@ -674,6 +807,21 @@ class CypressPlugin { } } + if (this.isKnownTestsEnabled && details.specs) { + const testSuites = details.specs.map(({ relative }) => relative) + const isFaulty = getIsFaultyEarlyFlakeDetection( + testSuites, + this.knownTestsByTestSuite, + this.earlyFlakeDetectionFaultyThreshold + ) + if (isFaulty) { + log.warn('New test detection is disabled because the number of new test files is too high.') + this.isEarlyFlakeDetectionEnabled = false + this.isEarlyFlakeDetectionFaulty = true + this.isKnownTestsEnabled = false + } + } + if (this.isSuitesSkippingEnabled) { const skippableTestsResponse = await getSkippableTests( this.tracer, @@ -681,6 +829,7 @@ class CypressPlugin { ) if (skippableTestsResponse.err) { log.error('Cypress skippable tests response error', skippableTestsResponse.err) + this._pendingRequestErrorTags.push({ tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, value: 'true' }) } else { const { skippableTests, correlationId } = skippableTestsResponse this.testsToSkip = skippableTests || [] @@ -696,6 +845,10 @@ class CypressPlugin { ) if (testManagementTestsResponse.err) { log.error('Cypress test management tests response error', testManagementTestsResponse.err) + this._pendingRequestErrorTags.push({ + tag: DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, + value: 'true', + }) this.isTestManagementTestsEnabled = false } else { this.testManagementTests = testManagementTestsResponse.testManagementTests @@ -731,6 +884,9 @@ class CypressPlugin { if (this.isEarlyFlakeDetectionEnabled) { testSessionSpanMetadata[TEST_EARLY_FLAKE_ENABLED] = 'true' } + if (this.isEarlyFlakeDetectionFaulty) { + testSessionSpanMetadata[TEST_EARLY_FLAKE_ABORT_REASON] = 'faulty' + } const trimmedCommand = DD_MAJOR < 6 ? this.command : 'cypress run' @@ -789,6 +945,11 @@ class CypressPlugin { }, integrationName: TEST_FRAMEWORK_NAME, }) + if (this.hasLibraryConfiguration) { + const skippingEnabled = this.isSuitesSkippingEnabled ? 'true' : 'false' + this.testSessionSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled) + this.testModuleSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled) + } this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module') return details @@ -833,7 +994,7 @@ class CypressPlugin { this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') incrementCountMetric(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName, - autoInjected: !!getValueFromEnvSources('DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER'), + autoInjected: !!getConfig().DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER, }) finishAllTraceSpans(this.testSessionSpan) @@ -934,17 +1095,25 @@ class CypressPlugin { for (const [testName, finishedTestAttempts] of Object.entries(finishedTestsByTestName)) { for (const [attemptIndex, finishedTest] of finishedTestAttempts.entries()) { - // TODO: there could be multiple if there have been retries! - // potentially we need to match the test status! - const cypressTest = cypressTests.find(test => test.title.join(' ') === testName) + // We can check if this is the last attempt regardless of the retry mechanism + const isLastAttempt = attemptIndex === finishedTestAttempts.length - 1 + const isDatadogManagedAttempt = finishedTest.isEfdManagedTest || finishedTest.isAttemptToFix + const cypressTest = isDatadogManagedAttempt + ? getMatchingCypressTest(cypressTests, testName, attemptIndex, finishedTest.testStatus, isLastAttempt) || + cypressTests.find(test => test.title.join(' ') === testName) + : cypressTests.find(test => test.title.join(' ') === testName) if (!cypressTest) { continue } // finishedTests can include multiple tests with the same name if they have been retried // by early flake detection. Cypress is unaware of this so .attempts does not necessarily have // the same length as `finishedTestAttempts` - let cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state] - if (cypressTest.attempts && cypressTest.attempts[attemptIndex]) { + const shouldUseCapturedStatus = isDatadogManagedAttempt && !(isLastAttempt && isCypressHookFailure(cypressTest)) + let cypressTestStatus = shouldUseCapturedStatus + ? finishedTest.testStatus + : CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state] + if (!finishedTest.isEfdManagedTest && !finishedTest.isAttemptToFix && + cypressTest.attempts && cypressTest.attempts[attemptIndex]) { cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state] const isAtrRetry = attemptIndex > 0 && this.isFlakyTestRetriesEnabled && @@ -961,6 +1130,9 @@ class CypressPlugin { } } } + if (finishedTest.isEfdManagedTest && finishedTest.testStatus !== 'skip' && cypressTestStatus === 'skip') { + cypressTestStatus = finishedTest.testStatus + } if (cypressTest.displayError) { latestError = new Error(cypressTest.displayError) } @@ -970,6 +1142,9 @@ class CypressPlugin { if (cypressTestStatus !== finishedTest.testStatus && (!isQuarantinedTest || finishedTest.isAttemptToFix)) { finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus) finishedTest.testSpan.setTag('error', latestError) + if (finishedTest.isAttemptToFix && cypressTestStatus === 'fail') { + finishedTest.testSpan.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'false') + } } if (this.itrCorrelationId) { finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId) @@ -989,8 +1164,6 @@ class CypressPlugin { finishedTest.testSpan.setTag(TEST_CODE_OWNERS, codeOwners) } - // We can check if this is the last attempt regardless of the retry mechanism - const isLastAttempt = attemptIndex === finishedTestAttempts.length - 1 if (isLastAttempt) { const testSpanTags = finishedTest.testSpan.context()._tags const retryKind = getFinalStatusRetryKind({ @@ -1042,7 +1215,8 @@ class CypressPlugin { const suitePayload = { isEarlyFlakeDetectionEnabled: this.isEarlyFlakeDetectionEnabled, knownTestsForSuite: this.knownTestsByTestSuite?.[testSuite] || [], - earlyFlakeDetectionNumRetries: this.earlyFlakeDetectionNumRetries, + earlyFlakeDetectionNumRetries: this.getConfiguredEfdRetryCount(), + earlyFlakeDetectionSlowTestRetries: this.earlyFlakeDetectionSlowTestRetries, isKnownTestsEnabled: this.isKnownTestsEnabled, isTestManagementEnabled: this.isTestManagementTestsEnabled, testManagementAttemptToFixRetries: this.testManagementAttemptToFixRetries, @@ -1058,7 +1232,10 @@ class CypressPlugin { return suitePayload }, 'dd:beforeEach': (test) => { - const { testName, testSuite } = test + const { testName, testSuite, isEfdRetry, efdRetryIndex } = test + if (isEfdRetry && this.shouldSkipEfdRetry(testSuite, testName, efdRetryIndex)) { + return { shouldSkip: true, shouldDiscard: true } + } const shouldSkip = this.testsToSkip.some(test => { return testName === test.name && testSuite === test.suite }) @@ -1115,6 +1292,7 @@ class CypressPlugin { isEfdRetry, isAttemptToFix, isModified, + duration, isQuarantined: isQuarantinedFromSupport, isDisabled: isDisabledFromSupport, } = test @@ -1136,7 +1314,19 @@ class CypressPlugin { } this.tracer._tracer._exporter.exportCoverage(formattedCoverage) } - const testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state] + const isEfdManagedTest = (isNew || isModified) && this.isEarlyFlakeDetectionEnabled && !isAttemptToFix + let testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state] + let didAbortSlowEfdRetries = false + if (isEfdManagedTest && !isEfdRetry && this.efdRetryCountByTest[testSuite]?.[testName] === undefined) { + const retryCount = this.setEfdRetryCountForTest(testSuite, testName, duration) + if (retryCount === 0) { + didAbortSlowEfdRetries = true + this.activeTestSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'slow') + } + } + if (didAbortSlowEfdRetries && testStatus === 'skip' && !error && duration > 0) { + testStatus = 'pass' + } this.activeTestSpan.setTag(TEST_STATUS, testStatus) // Save the test status to know if it has passed all retries @@ -1199,9 +1389,10 @@ class CypressPlugin { } } // Check if all EFD retries failed - if ((isNew || isModified) && this.earlyFlakeDetectionNumRetries > 0) { - const isLastEfdAttempt = testStatuses.length === this.earlyFlakeDetectionNumRetries + 1 - if (isLastEfdAttempt && testStatuses.every(status => status === 'fail')) { + if (isEfdManagedTest) { + const efdRetryCount = this.getEfdRetryCountForTest(testSuite, testName) + const isLastEfdAttempt = testStatuses.length === efdRetryCount + 1 + if (efdRetryCount > 0 && isLastEfdAttempt && testStatuses.every(status => status === 'fail')) { this.activeTestSpan.setTag(TEST_HAS_FAILED_ALL_RETRIES, 'true') } } @@ -1252,6 +1443,7 @@ class CypressPlugin { finishTime: this._now(), testSpan: this.activeTestSpan, isEfdRetry, + isEfdManagedTest, isAttemptToFix, } if (this.finishedTestsByFile[testSuite]) { diff --git a/packages/datadog-plugin-cypress/src/support.js b/packages/datadog-plugin-cypress/src/support.js index 01b97d279fd..589f080e2b5 100644 --- a/packages/datadog-plugin-cypress/src/support.js +++ b/packages/datadog-plugin-cypress/src/support.js @@ -79,6 +79,9 @@ function getRetriedTests (test, numRetries, tags) { // TODO: signal in framework logs that this is a retry. // TODO: Change it so these tests are allowed to fail. const clonedTest = test.clone() + if (tags.includes('_ddIsEfdRetry')) { + clonedTest._ddEfdRetryIndex = retryIndex + 1 + } for (const tag of tags) { if (tag) { clonedTest[tag] = true @@ -173,7 +176,12 @@ beforeEach(function () { cy.task('dd:beforeEach', { testName, testSuite: Cypress.mocha.getRootSuite().file, - }).then(({ traceId, shouldSkip }) => { + isEfdRetry: Cypress.mocha.getRunner().suite.ctx.currentTest._ddIsEfdRetry, + efdRetryIndex: Cypress.mocha.getRunner().suite.ctx.currentTest._ddEfdRetryIndex, + }).then(({ traceId, shouldSkip, shouldDiscard }) => { + if (shouldDiscard) { + this.currentTest._ddShouldDiscard = true + } if (traceId) { cy.setCookie(DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME, traceId).then(() => { // When testIsolation:false, the page is not reset between tests, so the RUM session @@ -242,6 +250,9 @@ after(() => { afterEach(function () { const currentTest = Cypress.mocha.getRunner().suite.ctx.currentTest + if (currentTest._ddShouldDiscard) { + return + } const testName = currentTest.fullTitle() // Check if this was a test management test that we suppressed the failure for. @@ -267,6 +278,7 @@ afterEach(function () { isEfdRetry: currentTest._ddIsEfdRetry, isAttemptToFix: currentTest._ddIsAttemptToFix, isModified: currentTest._ddIsModified, + duration: currentTest.duration, // Mark suppressed tests so the plugin can tag them with the correct test management reason. isQuarantined: isTestManagementTestThatFailed && suppressedTestFailure.isQuarantined, isDisabled: isTestManagementTestThatFailed && suppressedTestFailure.isDisabled, diff --git a/packages/datadog-plugin-electron/src/index.js b/packages/datadog-plugin-electron/src/index.js new file mode 100644 index 00000000000..3adc70a296f --- /dev/null +++ b/packages/datadog-plugin-electron/src/index.js @@ -0,0 +1,17 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const ElectronIpcPlugin = require('./ipc') +const ElectronNetPlugin = require('./net') + +class ElectronPlugin extends CompositePlugin { + static id = 'electron' + static get plugins () { + return { + net: ElectronNetPlugin, + ipc: ElectronIpcPlugin, + } + } +} + +module.exports = ElectronPlugin diff --git a/packages/datadog-plugin-electron/src/ipc.js b/packages/datadog-plugin-electron/src/ipc.js new file mode 100644 index 00000000000..aff28ace2c3 --- /dev/null +++ b/packages/datadog-plugin-electron/src/ipc.js @@ -0,0 +1,143 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') +const ProducerPlugin = require('../../dd-trace/src/plugins/producer') + +class ElectronIpcPlugin extends CompositePlugin { + static id = 'electron:ipc' + static get plugins () { + return { + main: ElectronMainPlugin, + renderer: ElectronRendererPlugin, + } + } +} + +class ElectronMainPlugin extends CompositePlugin { + static id = 'electron:ipc:main' + static get plugins () { + return { + receive: ElectronMainReceivePlugin, + handle: ElectronMainHandlePlugin, + send: ElectronMainSendPlugin, + } + } +} + +class ElectronRendererPlugin extends CompositePlugin { + static id = 'electron:ipc:renderer' + static get plugins () { + return { + receive: ElectronRendererReceivePlugin, + send: ElectronRendererSendPlugin, + } + } +} + +class ElectronRendererReceivePlugin extends ConsumerPlugin { + static id = 'electron:ipc:renderer:receive' + static component = 'electron' + static operation = 'receive' + static prefix = 'tracing:apm:electron:ipc:renderer:receive' + + bindStart (ctx) { + const { args, channel } = ctx + + if (channel?.startsWith('datadog:')) return + + const childOf = this._tracer.extract('text_map', args[args.length - 1]) + + if (childOf) { + args.pop() + } + + this.startSpan({ + childOf, + resource: channel, + type: 'worker', + meta: {}, + }, ctx) + + return ctx.currentStore + } + + asyncEnd (ctx) { + this.finish(ctx) + } +} + +class ElectronRendererSendPlugin extends ProducerPlugin { + static id = 'electron:ipc:renderer:send' + static component = 'electron' + static operation = 'send' + static prefix = 'tracing:apm:electron:ipc:renderer:send' + + bindStart (ctx) { + const { args, channel } = ctx + + if (channel?.startsWith('datadog:')) return + + const span = this.startSpan({ + resource: channel, + meta: {}, + }, ctx) + + if (this._shouldInject(ctx)) { + const carrier = {} + + this._tracer.inject(span, 'text_map', carrier) + + args.push(carrier) + } + + return ctx.currentStore + } + + end (ctx) { + if (ctx.hasOwnProperty('result')) { + this.finish(ctx) + } + } + + asyncEnd (ctx) { + this.finish(ctx) + } + + // Renderer can always inject since main is guaranteed to be patched. + _shouldInject () { + return true + } +} + +class ElectronMainReceivePlugin extends ElectronRendererReceivePlugin { + static id = 'electron:ipc:main:receive' + static prefix = 'tracing:apm:electron:ipc:main:receive' +} + +class ElectronMainHandlePlugin extends ElectronMainReceivePlugin { + static id = 'electron:ipc:main:handle' + static prefix = 'tracing:apm:electron:ipc:main:handle' +} + +class ElectronMainSendPlugin extends ElectronRendererSendPlugin { + static id = 'electron:ipc:main:send' + static prefix = 'tracing:apm:electron:ipc:main:send' + + constructor (...args) { + super(...args) + + this._renderers = new WeakSet() + + this.addSub('apm:electron:ipc:renderer:patched', event => { + this._renderers.add(event.sender) + }) + } + + // Only inject when the renderer was patched. + _shouldInject ({ self }) { + return this._renderers.has(self) + } +} + +module.exports = ElectronIpcPlugin diff --git a/packages/datadog-plugin-electron/src/net.js b/packages/datadog-plugin-electron/src/net.js new file mode 100644 index 00000000000..1f8675f1482 --- /dev/null +++ b/packages/datadog-plugin-electron/src/net.js @@ -0,0 +1,82 @@ +'use strict' + +const HttpClientPlugin = require('../../datadog-plugin-http/src/client') +const CompositePlugin = require('../../dd-trace/src/plugins/composite') + +class ElectronNetPlugin extends CompositePlugin { + static id = 'electron:net' + static get plugins () { + return { + request: ElectronRequestPlugin, + } + } +} + +class ElectronRequestPlugin extends HttpClientPlugin { + static id = 'electron:net:request' + static component = 'electron' + static operation = 'request' + static prefix = 'tracing:apm:electron:net:request' + + bindStart (ctx) { + const args = ctx.args + + let options = args[0] + + if (typeof options === 'string') { + options = args[0] = { url: options } + } else if (!options) { + options = args[0] = {} + } + + const headers = options.headers || {} + + try { + if (typeof options === 'string') { + options = new URL(options) + } else if (options.url) { + options = new URL(options.url) + } + } catch { + // leave options as-is + } + + options.headers = headers + ctx.args = { options } + + const store = super.bindStart(ctx) + + ctx.args = args + + for (const name in options.headers) { + if (!headers[name]) { + args[0].headers ??= {} + args[0].headers[name] = options.headers[name] + } + } + + return store + } + + asyncStart (ctx) { + const reqHeaders = {} + const resHeaders = {} + const responseHead = ctx.res?._responseHead + const { statusCode } = responseHead || {} + + for (const header in ctx.req._urlLoaderOptions?.headers || {}) { + reqHeaders[header.name] = header.value + } + + for (const header in responseHead?.rawHeaders || {}) { + resHeaders[header.name] = header.value + } + + ctx.req = { headers: reqHeaders } + ctx.res = { headers: resHeaders, statusCode } + + this.finish(ctx) + } +} + +module.exports = ElectronNetPlugin diff --git a/packages/datadog-plugin-electron/test/app/index.html b/packages/datadog-plugin-electron/test/app/index.html new file mode 100644 index 00000000000..1411fe6ba72 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/index.html @@ -0,0 +1,12 @@ + + + + + + + Hello World! + + + + + diff --git a/packages/datadog-plugin-electron/test/app/main.js b/packages/datadog-plugin-electron/test/app/main.js new file mode 100644 index 00000000000..bebc795b40e --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/main.js @@ -0,0 +1,93 @@ +'use strict' + +/* eslint-disable no-console */ + +const { join } = require('path') +const { BrowserWindow, app, ipcMain, net } = require('electron/main') + +const CONFIG_CHANNEL = 'datadog:bridge-config' + +ipcMain.on(CONFIG_CHANNEL, event => { + event.returnValue = null +}) + +app.on('ready', () => { + process.send('ready') + process.on('message', msg => { + try { + switch (msg.name) { + case 'quit': return app.quit() + case 'fetch': return onFetch(msg) + case 'request': return onRequest(msg) + case 'send': return onSend() + case 'receive': return onReceive() + case 'handle': return onHandle() + } + } catch (e) { + console.error(e) + } + }) + + ipcMain.on('datadog:log', (_event, level, ...args) => { + console.log('datadog:log') + console[level](...args) + }) +}) + +function onFetch ({ url }) { + net.fetch(url) +} + +function onRequest ({ options }) { + const req = net.request(options) + + req.on('error', e => console.error(e)) + req.on('response', res => { + res.on('data', () => {}) + }) + + req.end() +} + +function onSend () { + loadWindow(win => { + win.webContents.send('update-counter', 1) + }) +} + +function onReceive () { + const listener = () => { + ipcMain.off('set-title', listener) + } + + ipcMain.on('set-title', listener) + + loadWindow(win => { + win.webContents.send('datadog:test:send') + }) +} + +function onHandle () { + ipcMain.handleOnce('get-data', () => 'test') + + loadWindow(win => { + win.webContents.send('datadog:test:invoke') + }) +} + +function loadWindow (onShow) { + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + preload: join(__dirname, 'preload.js'), + }, + }) + + ipcMain.on('datadog:test:log', (_event, ...args) => { + console.log(...args) + }) + + mainWindow.loadFile('index.html') + mainWindow.once('ready-to-show', () => onShow?.(mainWindow)) +} diff --git a/packages/datadog-plugin-electron/test/app/preload.js b/packages/datadog-plugin-electron/test/app/preload.js new file mode 100644 index 00000000000..46335c42ad2 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/preload.js @@ -0,0 +1,24 @@ +'use strict' + +const { ipcRenderer } = require('electron/renderer') + +if (globalThis.logger) { + globalThis.logger.debug = (...args) => ipcRenderer.send('datadog:log', 'debug', ...args) + globalThis.logger.info = (...args) => ipcRenderer.send('datadog:log', 'info', ...args) + globalThis.logger.warn = (...args) => ipcRenderer.send('datadog:log', 'warn', ...args) + globalThis.logger.error = (...args) => ipcRenderer.send('datadog:log', 'error', ...args) +} + +function updateCounter () { + ipcRenderer.off('update-counter', updateCounter) +} + +ipcRenderer.on('update-counter', updateCounter) + +ipcRenderer.on('datadog:test:send', () => { + setImmediate(() => ipcRenderer.send('set-title', 'Test')) +}) + +ipcRenderer.on('datadog:test:invoke', () => { + setImmediate(() => ipcRenderer.invoke('get-data')) +}) diff --git a/packages/datadog-plugin-electron/test/app/renderer.js b/packages/datadog-plugin-electron/test/app/renderer.js new file mode 100644 index 00000000000..bdc38e3fa06 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/renderer.js @@ -0,0 +1,3 @@ +'use strict' + +// TODO: Renderer support for integrations in sandbox mode. diff --git a/packages/datadog-plugin-electron/test/index.spec.js b/packages/datadog-plugin-electron/test/index.spec.js new file mode 100644 index 00000000000..66d387a050e --- /dev/null +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -0,0 +1,216 @@ +'use strict' + +const assert = require('assert') +const proc = require('child_process') +const http = require('http') +const { join } = require('path') +const { afterEach, beforeEach, describe, it } = require('mocha') +const agent = require('../../dd-trace/test/plugins/agent') +const { withVersions } = require('../../dd-trace/test/setup/mocha') + +describe('Plugin', () => { + let child + let listener + let port + + before(done => { + const server = http.createServer((req, res) => { + res.writeHead(200) + res.end() + }) + + listener = server.listen(0, '127.0.0.1', () => { + port = listener.address().port + done() + }) + }) + + after(done => { + listener.close(done) + }) + + withVersions('electron', ['electron'], version => { + const startApp = done => { + const electron = require(`../../../versions/electron@${version}`).get() + + child = proc.spawn(electron, [join(__dirname, 'app', 'main')], { + env: { + ...process.env, + NODE_OPTIONS: `-r ${join(__dirname, 'tracer')}`, + DD_TRACE_AGENT_PORT: agent.port, + }, + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + windowsHide: true, + }) + + child.on('error', done) + child.on('message', msg => msg === 'ready' && done()) + } + + describe('electron', () => { + describe('without configuration', () => { + beforeEach(() => agent.load('electron')) + beforeEach(done => startApp(done)) + + afterEach(() => agent.close({ ritmReset: false })) + afterEach(done => { + child.send({ name: 'quit' }) + child.on('close', () => done()) + }) + + it('should do automatic instrumentation for fetch', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'http') + assert.strictEqual(span.name, 'http.request') + assert.strictEqual(span.resource, 'GET') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'client') + assert.strictEqual(meta['http.url'], `http://127.0.0.1:${port}/`) + assert.strictEqual(meta['http.method'], 'GET') + assert.strictEqual(meta['http.status_code'], '200') + }) + .then(done) + .catch(done) + + child.send({ name: 'fetch', url: `http://127.0.0.1:${port}` }) + }) + + it('should do automatic instrumentation for request', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'http') + assert.strictEqual(span.name, 'http.request') + assert.strictEqual(span.resource, 'GET') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'client') + assert.strictEqual(meta['http.url'], `http://127.0.0.1:${port}/`) + assert.strictEqual(meta['http.method'], 'GET') + assert.strictEqual(meta['http.status_code'], '200') + }) + .then(done) + .catch(done) + + child.send({ name: 'request', options: `http://127.0.0.1:${port}/` }) + }) + + it('should do automatic instrumentation for main IPC when receiving', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'worker') + assert.strictEqual(span.name, 'electron.main.receive') + assert.strictEqual(span.resource, 'set-title') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + assert.strictEqual(span.parent_id, span.trace_id) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'receive' }) + }) + + it('should do automatic instrumentation for main IPC when handling', done => { + agent + .assertSomeTraces(traces => { + const span = traces.flat().find(s => s.name === 'electron.main.handle') + const { meta } = span + + assert.strictEqual(span.type, 'worker') + assert.strictEqual(span.name, 'electron.main.handle') + assert.strictEqual(span.resource, 'get-data') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'handle' }) + }) + + it('should do automatic instrumentation for main IPC when sending', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.name, 'electron.main.send') + assert.strictEqual(span.resource, 'update-counter') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'producer') + }) + .then(done) + .catch(done) + + child.send({ name: 'send' }) + }) + + it('should do automatic instrumentation for renderer IPC when receiving', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'worker') + assert.strictEqual(span.name, 'electron.renderer.receive') + assert.strictEqual(span.resource, 'update-counter') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + assert.strictEqual(span.parent_id, span.trace_id) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'send' }) + }) + + it('should do automatic instrumentation for renderer IPC when sending', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.name, 'electron.renderer.send') + assert.strictEqual(span.resource, 'set-title') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'producer') + }) + .then(done) + .catch(done) + + child.send({ name: 'receive' }) + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-electron/test/tracer.js b/packages/datadog-plugin-electron/test/tracer.js new file mode 100644 index 00000000000..84acf352f50 --- /dev/null +++ b/packages/datadog-plugin-electron/test/tracer.js @@ -0,0 +1,20 @@ +'use strict' + +/* eslint-disable no-console */ + +const logger = globalThis.logger = { + debug: (...args) => console.debug(...args), + info: (...args) => console.info(...args), + warn: (...args) => console.warn(...args), + error: (...args) => console.error(...args), +} + +require('../../dd-trace') + .init({ + service: 'test', + env: 'tester', + logger, + flushInterval: 0, + plugins: false, + }) + .use('electron', true) diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/producer.js b/packages/datadog-plugin-google-cloud-pubsub/src/producer.js index e8e5138aa50..86e5cdf5343 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/producer.js @@ -54,9 +54,13 @@ class GoogleCloudPubsubProducerPlugin extends ProducerPlugin { * - Inject batch span context + metadata into all message attributes for downstream * consumers to reconstruct the trace and understand batch relationships */ - const spanLinkData = hasTraceContext - ? messages.slice(1).map(msg => this.#extractSpanLink(msg.attributes)).filter(Boolean) - : [] + const spanLinkData = [] + if (hasTraceContext) { + for (let i = 1; i < messageCount; i++) { + const link = this.#extractSpanLink(messages[i].attributes) + if (link) spanLinkData.push(link) + } + } const firstAttrs = messages[0]?.attributes const parentData = firstAttrs?.['x-datadog-trace-id'] && firstAttrs['x-datadog-parent-id'] @@ -107,35 +111,40 @@ class GoogleCloudPubsubProducerPlugin extends ProducerPlugin { )) } - for (let i = 0; i < messages.length; i++) { + const messageCountStr = String(messageCount) + const startTimeStr = String(Math.floor(batchSpan._startTime)) + const dsmEnabled = this.config.dsmEnabled + + for (let i = 0; i < messageCount; i++) { const msg = messages[i] - msg.attributes ??= {} + const attributes = msg.attributes ??= {} if (!hasTraceContext) { - this.tracer.inject(batchSpan, 'text_map', msg.attributes) + this.tracer.inject(batchSpan, 'text_map', attributes) } - Object.assign(msg.attributes, { - '_dd.pubsub_request.trace_id': batchTraceIdHex, - '_dd.pubsub_request.span_id': batchSpanIdHex, - '_dd.batch.size': String(messageCount), - '_dd.batch.index': String(i), - 'gcloud.project_id': projectId, - 'pubsub.topic': topic, - 'x-dd-publish-start-time': String(Math.floor(batchSpan._startTime)), - }) + // Assign keys one-by-one rather than via `Object.assign({...})` so V8 + // keeps the attributes object on its existing hidden class instead of + // allocating a fresh literal per message. + attributes['_dd.pubsub_request.trace_id'] = batchTraceIdHex + attributes['_dd.pubsub_request.span_id'] = batchSpanIdHex + attributes['_dd.batch.size'] = messageCountStr + attributes['_dd.batch.index'] = String(i) + attributes['gcloud.project_id'] = projectId + attributes['pubsub.topic'] = topic + attributes['x-dd-publish-start-time'] = startTimeStr if (batchTraceIdUpper) { - msg.attributes['_dd.pubsub_request.p.tid'] = batchTraceIdUpper + attributes['_dd.pubsub_request.p.tid'] = batchTraceIdUpper } - if (this.config.dsmEnabled) { + if (dsmEnabled) { const dataStreamsContext = this.tracer.setCheckpoint( ['direction:out', `topic:${topic}`, 'type:google-pubsub'], batchSpan, getHeadersSize(msg) ) - DsmPathwayCodec.encode(dataStreamsContext, msg.attributes) + DsmPathwayCodec.encode(dataStreamsContext, attributes) } } diff --git a/packages/datadog-plugin-graphql/src/execute.js b/packages/datadog-plugin-graphql/src/execute.js index 1dcd2e4d0a1..e7759aa995b 100644 --- a/packages/datadog-plugin-graphql/src/execute.js +++ b/packages/datadog-plugin-graphql/src/execute.js @@ -1,9 +1,7 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing') -const { extractErrorIntoSpanEvent } = require('./utils') - -let tools +const { extractErrorIntoSpanEvent, getSignature } = require('./utils') class GraphQLExecutePlugin extends TracingPlugin { static id = 'graphql' @@ -54,35 +52,15 @@ class GraphQLExecutePlugin extends TracingPlugin { // span-related function addVariableTags (config, span, variableValues) { - const tags = {} + if (!variableValues || !config.variables) return - if (variableValues && config.variables) { - const variables = config.variables(variableValues) - for (const [param, value] of Object.entries(variables)) { - tags[`graphql.variables.${param}`] = value - } + const tags = {} + const variables = config.variables(variableValues) + for (const [param, value] of Object.entries(variables)) { + tags[`graphql.variables.${param}`] = value } span.addTags(tags) } -function getSignature (document, operationName, operationType, calculate) { - if (calculate !== false && tools !== false) { - try { - try { - tools = tools || require('./tools') - } catch (e) { - tools = false - throw e - } - - return tools.defaultEngineReportingSignature(document, operationName) - } catch { - // safety net - } - } - - return [operationType, operationName].filter(Boolean).join(' ') -} - module.exports = GraphQLExecutePlugin diff --git a/packages/datadog-plugin-graphql/src/resolve.js b/packages/datadog-plugin-graphql/src/resolve.js index 3597f2002ae..6a1721bbc22 100644 --- a/packages/datadog-plugin-graphql/src/resolve.js +++ b/packages/datadog-plugin-graphql/src/resolve.js @@ -12,8 +12,6 @@ class GraphQLResolvePlugin extends TracingPlugin { start (fieldCtx) { const { info, rootCtx, args, path: pathAsArray, pathString } = fieldCtx - const path = getPath(this.config, pathAsArray) - // we need to get the parent span to the field if it exists for correct span parenting // of nested fields const parentField = getParentField(rootCtx, pathString) @@ -21,8 +19,10 @@ class GraphQLResolvePlugin extends TracingPlugin { fieldCtx.parent = parentField - if (!shouldInstrument(this.config, path)) return - const computedPathString = path.join('.') + if (!shouldInstrument(this.config, pathAsArray)) return + const computedPathString = this.config.collapse + ? buildCollapsedPathString(pathAsArray) + : pathString if (this.config.collapse) { if (rootCtx.fields[computedPathString]) return @@ -37,7 +37,7 @@ class GraphQLResolvePlugin extends TracingPlugin { } const document = rootCtx.source - const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field') + const fieldNode = info.fieldNodes[0] const loc = this.config.source && document && fieldNode && fieldNode.loc const source = loc && document.slice(loc.start, loc.end) @@ -78,9 +78,7 @@ class GraphQLResolvePlugin extends TracingPlugin { this.addTraceSub('updateField', (ctx) => { const { field, error, path: pathAsArray } = ctx - const path = getPath(this.config, pathAsArray) - - if (!shouldInstrument(this.config, path)) return + if (!shouldInstrument(this.config, pathAsArray)) return const span = ctx?.currentStore?.span || this.activeSpan field.finishTime = span._getTime ? span._getTime() : 0 @@ -107,56 +105,53 @@ class GraphQLResolvePlugin extends TracingPlugin { // helpers -function shouldInstrument (config, path) { +function shouldInstrument (config, pathAsArray) { + if (config.depth < 0) return true + let depth = 0 - for (const item of path) { - if (typeof item === 'string') { - depth += 1 + if (config.collapse) { + depth = pathAsArray.length + } else { + for (const segment of pathAsArray) { + if (typeof segment === 'string') depth += 1 } } - return config.depth < 0 || config.depth >= depth -} - -function getPath (config, pathAsArray) { - return config.collapse - ? withCollapse(pathAsArray) - : pathAsArray + return config.depth >= depth } -function withCollapse (pathAsArray) { - return pathAsArray.map(segment => typeof segment === 'number' ? '*' : segment) +function buildCollapsedPathString (pathAsArray) { + let result = '' + for (const segment of pathAsArray) { + if (result.length > 0) result += '.' + result += typeof segment === 'number' ? '*' : segment + } + return result } function getResolverInfo (info, args) { - let resolverInfo = null - const resolverVars = {} + let resolverVars - if (args) { - Object.assign(resolverVars, args) + if (args && Object.keys(args).length > 0) { + resolverVars = { ...args } } - let hasResolvers = false const directives = info.fieldNodes?.[0]?.directives if (Array.isArray(directives)) { for (const directive of directives) { + if (directive.arguments.length === 0) continue + const argList = {} for (const argument of directive.arguments) { argList[argument.name.value] = argument.value.value } - if (directive.arguments.length > 0) { - hasResolvers = true - resolverVars[directive.name.value] = argList - } + resolverVars ??= {} + resolverVars[directive.name.value] = argList } } - if (hasResolvers || args && Object.keys(resolverVars).length) { - resolverInfo = { [info.fieldName]: resolverVars } - } - - return resolverInfo + return resolverVars === undefined ? null : { [info.fieldName]: resolverVars } } function getParentField (parentCtx, pathToString) { diff --git a/packages/datadog-plugin-graphql/src/tools/signature.js b/packages/datadog-plugin-graphql/src/tools/signature.js index 0780c03be27..e36bda9d2f9 100644 --- a/packages/datadog-plugin-graphql/src/tools/signature.js +++ b/packages/datadog-plugin-graphql/src/tools/signature.js @@ -1,9 +1,34 @@ -// file mostly untouched from apollo-graphql +'use strict' -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const transforms_1 = require("./transforms"); -function defaultEngineReportingSignature(ast, operationName) { - return transforms_1.printWithReducedWhitespace(transforms_1.sortAST(transforms_1.removeAliases(transforms_1.hideLiterals(transforms_1.dropUnusedDefinitions(ast, operationName))))); +Object.defineProperty(exports, '__esModule', { value: true }) +const transforms = require('./transforms') + +// Apollo Server / Yoga / Mercurius hand back the same parsed `DocumentNode` +// from their own document caches per execute, so memoizing the signature on +// the document keeps the visit/print pipeline off the hot path. The inner +// Map keys on operationName since `separateOperations` picks a different +// sub-document for each operation. +const cache = new WeakMap() + +function defaultEngineReportingSignature (ast, operationName) { + const key = operationName == null ? '' : operationName + let inner = cache.get(ast) + if (inner !== undefined) { + const cached = inner.get(key) + if (cached !== undefined) { + return cached + } + } + const signature = transforms.printWithReducedWhitespace( + transforms.transformForSignature( + transforms.dropUnusedDefinitions(ast, operationName) + ) + ) + if (inner === undefined) { + inner = new Map() + cache.set(ast, inner) + } + inner.set(key, signature) + return signature } -exports.defaultEngineReportingSignature = defaultEngineReportingSignature; +exports.defaultEngineReportingSignature = defaultEngineReportingSignature diff --git a/packages/datadog-plugin-graphql/src/tools/transforms.js b/packages/datadog-plugin-graphql/src/tools/transforms.js index c96cb492008..ac53c089632 100644 --- a/packages/datadog-plugin-graphql/src/tools/transforms.js +++ b/packages/datadog-plugin-graphql/src/tools/transforms.js @@ -1,108 +1,126 @@ -// file mostly untouched from apollo-graphql +'use strict' -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const ddGlobal = globalThis[Symbol.for('dd-trace')]; -const visitor_1 = ddGlobal.graphql_visitor; -const printer_1 = ddGlobal.graphql_printer; -const utilities_1 = ddGlobal.graphql_utilities; -const lodash_sortby_1 = __importDefault(require("../../../../vendor/dist/lodash.sortby")); -function hideLiterals(ast) { - return visitor_1.visit(ast, { - IntValue(node) { - return Object.assign({}, node, { value: "0" }); - }, - FloatValue(node) { - return Object.assign({}, node, { value: "0" }); - }, - StringValue(node) { - return Object.assign({}, node, { value: "", block: false }); - }, - ListValue(node) { - return Object.assign({}, node, { values: [] }); - }, - ObjectValue(node) { - return Object.assign({}, node, { fields: [] }); - } - }); +Object.defineProperty(exports, '__esModule', { value: true }) + +const ddGlobal = globalThis[Symbol.for('dd-trace')] +const visitor = ddGlobal.graphql_visitor +const printer = ddGlobal.graphql_printer +const utilities = ddGlobal.graphql_utilities + +function dropUnusedDefinitions (ast, operationName) { + const separated = utilities.separateOperations(ast)[operationName] + if (!separated) { + return ast + } + return separated } -exports.hideLiterals = hideLiterals; -function hideStringAndNumericLiterals(ast) { - return visitor_1.visit(ast, { - IntValue(node) { - return Object.assign({}, node, { value: "0" }); - }, - FloatValue(node) { - return Object.assign({}, node, { value: "0" }); - }, - StringValue(node) { - return Object.assign({}, node, { value: "", block: false }); - } - }); + +// One walk replaces Apollo's `hideLiterals` + `removeAliases` + `sortAST` + +// the `StringValue` pre-pass that used to live in `printWithReducedWhitespace`. +// The byte output is unchanged: hideLiterals had already collapsed every +// `StringValue.value` to '', so the original hex round-trip degenerated to +// hex('') === '' anyway. +function transformForSignature (ast) { + return visitor.visit(ast, { + IntValue (node) { + return { ...node, value: '0' } + }, + FloatValue (node) { + return { ...node, value: '0' } + }, + StringValue (node) { + return { ...node, value: '', block: false } + }, + ListValue (node) { + return { ...node, values: [] } + }, + ObjectValue (node) { + return { ...node, fields: [] } + }, + Field (node) { + return { + ...node, + alias: undefined, + arguments: sortByName(node.arguments), + } + }, + OperationDefinition (node) { + return { + ...node, + variableDefinitions: sortByVariableName(node.variableDefinitions), + } + }, + SelectionSet (node) { + return { ...node, selections: sortByKindThenName(node.selections) } + }, + FragmentSpread (node) { + return { ...node, directives: sortByName(node.directives) } + }, + InlineFragment (node) { + return { ...node, directives: sortByName(node.directives) } + }, + FragmentDefinition (node) { + return { + ...node, + directives: sortByName(node.directives), + variableDefinitions: sortByVariableName(node.variableDefinitions), + } + }, + Directive (node) { + return { ...node, arguments: sortByName(node.arguments) } + }, + }) } -exports.hideStringAndNumericLiterals = hideStringAndNumericLiterals; -function dropUnusedDefinitions(ast, operationName) { - const separated = utilities_1.separateOperations(ast)[operationName]; - if (!separated) { - return ast; - } - return separated; + +function printWithReducedWhitespace (ast) { + return printer.print(ast) + .replaceAll(/\s+/g, ' ') + .replaceAll(/ (?=[^_a-zA-Z0-9])|(?<=[^_a-zA-Z0-9]) /g, '') } -exports.dropUnusedDefinitions = dropUnusedDefinitions; -function sorted(items) { - if (items) { - return lodash_sortby_1.default.apply(null, arguments); - } - return undefined; + +function sortByName (items) { + if (!items) return + return [...items].sort(byName) } -function sortAST(ast) { - return visitor_1.visit(ast, { - OperationDefinition(node) { - return Object.assign({}, node, { variableDefinitions: sorted(node.variableDefinitions, "variable.name.value") }); - }, - SelectionSet(node) { - return Object.assign({}, node, { selections: lodash_sortby_1.default(node.selections, "kind", "name.value") }); - }, - Field(node) { - return Object.assign({}, node, { arguments: sorted(node.arguments, "name.value") }); - }, - FragmentSpread(node) { - return Object.assign({}, node, { directives: sorted(node.directives, "name.value") }); - }, - InlineFragment(node) { - return Object.assign({}, node, { directives: sorted(node.directives, "name.value") }); - }, - FragmentDefinition(node) { - return Object.assign({}, node, { directives: sorted(node.directives, "name.value"), variableDefinitions: sorted(node.variableDefinitions, "variable.name.value") }); - }, - Directive(node) { - return Object.assign({}, node, { arguments: sorted(node.arguments, "name.value") }); - } - }); + +function byName (a, b) { + const left = a.name.value + const right = b.name.value + if (left < right) return -1 + if (left > right) return 1 + return 0 } -exports.sortAST = sortAST; -function removeAliases(ast) { - return visitor_1.visit(ast, { - Field(node) { - return Object.assign({}, node, { alias: undefined }); - } - }); + +function sortByVariableName (items) { + if (!items) return + return [...items].sort(byVariableName) } -exports.removeAliases = removeAliases; -function printWithReducedWhitespace(ast) { - const sanitizedAST = visitor_1.visit(ast, { - StringValue(node) { - return Object.assign({}, node, { value: Buffer.from(node.value, "utf8").toString("hex"), block: false }); - } - }); - const withWhitespace = printer_1.print(sanitizedAST); - const minimizedButStillHex = withWhitespace - .replace(/\s+/g, " ") - .replace(/([^_a-zA-Z0-9]) /g, (_, c) => c) - .replace(/ ([^_a-zA-Z0-9])/g, (_, c) => c); - return minimizedButStillHex.replace(/"([a-f0-9]+)"/g, (_, hex) => JSON.stringify(Buffer.from(hex, "hex").toString("utf8"))); + +function byVariableName (a, b) { + const left = a.variable.name.value + const right = b.variable.name.value + if (left < right) return -1 + if (left > right) return 1 + return 0 +} + +// SelectionSet children include InlineFragment, which has no `name`, so the +// secondary key falls back to undefined and stable sort keeps sibling order. +function sortByKindThenName (items) { + return [...items].sort(byKindThenName) } -exports.printWithReducedWhitespace = printWithReducedWhitespace; + +function byKindThenName (a, b) { + if (a.kind < b.kind) return -1 + if (a.kind > b.kind) return 1 + const left = a.name?.value + const right = b.name?.value + if (left === right) return 0 + if (left === undefined) return 1 + if (right === undefined) return -1 + return left < right ? -1 : 1 +} + +exports.dropUnusedDefinitions = dropUnusedDefinitions +exports.transformForSignature = transformForSignature +exports.printWithReducedWhitespace = printWithReducedWhitespace diff --git a/packages/datadog-plugin-graphql/src/utils.js b/packages/datadog-plugin-graphql/src/utils.js index a50cebbc6fb..33824519e4c 100644 --- a/packages/datadog-plugin-graphql/src/utils.js +++ b/packages/datadog-plugin-graphql/src/utils.js @@ -44,6 +44,35 @@ function extractErrorIntoSpanEvent (config, span, exc) { span.addEvent('dd.graphql.query.error', attributes, Date.now()) } +let tools + +function getSignature (document, operationName, operationType, calculate) { + if (calculate !== false && tools !== false) { + try { + try { + tools ||= require('./tools') + } catch (e) { + tools = false + throw e + } + + return tools.defaultEngineReportingSignature(document, operationName) + } catch { + // safety net + } + } + + if (operationType) { + if (operationName) { + return `${operationType} ${operationName}` + } + return operationType + } + + return operationName ?? '' +} + module.exports = { extractErrorIntoSpanEvent, + getSignature, } diff --git a/packages/datadog-plugin-graphql/test/index.spec.js b/packages/datadog-plugin-graphql/test/index.spec.js index ff328048cf0..cb7f7056798 100644 --- a/packages/datadog-plugin-graphql/test/index.spec.js +++ b/packages/datadog-plugin-graphql/test/index.spec.js @@ -1755,6 +1755,20 @@ describe('Plugin', () => { graphql.graphql({ schema, source }).catch(done) }) + + it('should fallback to the operation type', async () => { + const source = '{ friends { name } }' + + await Promise.all([ + agent.assertSomeTraces(traces => { + const spans = sort(traces[0]) + + assert.strictEqual(spans[0].name, expectedSchema.server.opName) + assert.strictEqual(spans[0].resource, 'query') + }), + graphql.graphql({ schema, source }), + ]) + }) }) describe('with hooks configuration', () => { diff --git a/packages/datadog-plugin-graphql/test/tools/signature.spec.js b/packages/datadog-plugin-graphql/test/tools/signature.spec.js new file mode 100644 index 00000000000..061510d77b3 --- /dev/null +++ b/packages/datadog-plugin-graphql/test/tools/signature.spec.js @@ -0,0 +1,243 @@ +'use strict' + +const assert = require('node:assert/strict') +const Module = require('node:module') + +const { describe, it, before, after } = require('mocha') +const sinon = require('sinon') + +/** + * @typedef {{ filename?: string }} ModuleParent + * @typedef {(request: string, parent: ModuleParent | undefined, isMain: boolean) => unknown} ModuleLoad + * @typedef {typeof import('node:module') & { _load: ModuleLoad }} LoadableModule + */ + +const loadableModule = /** @type {LoadableModule} */ (Module) + +// The transforms module reads `globalThis[Symbol.for('dd-trace')].graphql_*` +// at require time, so populate them before requiring the tools. +const visitor = require('graphql/language/visitor') +const printer = require('graphql/language/printer') +const ddGlobal = globalThis[Symbol.for('dd-trace')] +ddGlobal.graphql_visitor = visitor +ddGlobal.graphql_printer = printer +ddGlobal.graphql_utilities = require('graphql/utilities') + +const { parse } = require('graphql') + +const { assertObjectContains } = require('../../../../integration-tests/helpers') +const { defaultEngineReportingSignature } = require('../../src/tools/signature') + +describe('graphql signature memoization', () => { + let visitSpy + let printSpy + + before(() => { + visitSpy = sinon.spy(visitor, 'visit') + printSpy = sinon.spy(printer, 'print') + }) + + after(() => { + visitSpy.restore() + printSpy.restore() + }) + + it('returns the same signature without re-running visit/print for the same document and operation', () => { + const ast = parse('query Foo($id: ID!) { user(id: $id, name: "Alice") { id name } }') + + const visitsBefore = visitSpy.callCount + const printsBefore = printSpy.callCount + const first = defaultEngineReportingSignature(ast, 'Foo') + const visitsAfterFirst = visitSpy.callCount + const printsAfterFirst = printSpy.callCount + const second = defaultEngineReportingSignature(ast, 'Foo') + const third = defaultEngineReportingSignature(ast, 'Foo') + + assert.notEqual(visitsAfterFirst, visitsBefore) + assert.notEqual(printsAfterFirst, printsBefore) + assert.equal(visitSpy.callCount, visitsAfterFirst) + assert.equal(printSpy.callCount, printsAfterFirst) + assert.equal(first, second) + assert.equal(second, third) + }) + + it('treats undefined and null operationName as the same cache key', () => { + const ast = parse('{ a b c }') + + const printsBefore = printSpy.callCount + const first = defaultEngineReportingSignature(ast, undefined) + const second = defaultEngineReportingSignature(ast, null) + + assert.equal(printSpy.callCount - printsBefore, 1) + assert.equal(first, second) + }) + + it('keys the cache by operationName so different operations on the same document compute independently', () => { + const ast = parse('query A { a } query B { b }') + + const printsBefore = printSpy.callCount + const sigA = defaultEngineReportingSignature(ast, 'A') + const sigB = defaultEngineReportingSignature(ast, 'B') + defaultEngineReportingSignature(ast, 'A') + defaultEngineReportingSignature(ast, 'B') + + assert.equal(printSpy.callCount - printsBefore, 2) + assert.notEqual(sigA, sigB) + }) + + it('recomputes for a different document instance even if the source is identical', () => { + const source = '{ x y }' + + const printsBefore = printSpy.callCount + const sigA = defaultEngineReportingSignature(parse(source), undefined) + const sigB = defaultEngineReportingSignature(parse(source), undefined) + + assert.equal(printSpy.callCount - printsBefore, 2) + assert.equal(sigA, sigB) + }) +}) + +describe('graphql signature byte output', () => { + // Pinned against the multi-pass output of `hideLiterals` + `removeAliases` + + // `sortAST` + `printWithReducedWhitespace` from before the visitor walks + // were collapsed; any future drift trips the assertion. + const representative = ` + query GetUser($id: ID!, $verbose: Boolean = false) { + aliasUser: user(id: $id, name: "Alice", limit: 100, scale: 1.5) { + name + id + profile @include(if: $verbose) @customDirective(meta: "x") { + bio + avatar + } + ...UserFragment + ... on AdminUser { + permissions @include(if: $verbose) + } + friends(first: 10, filter: { active: true, names: ["a", "b"] }) { + edges { + node { + name + id + } + } + } + } + } + + fragment UserFragment on User @include(if: $verbose) @priority { + metadata + tags + createdAt + } + ` + + const expected = + 'query GetUser($id:ID!,$verbose:Boolean=false){user(id:$id,limit:0,name:"",scale:0)' + + '{friends(filter:{},first:0){edges{node{id name}}}id name profile' + + '@include(if:$verbose)@customDirective(meta:""){avatar bio}...UserFragment' + + '...on AdminUser{permissions@include(if:$verbose)}}}fragment UserFragment ' + + 'on User@include(if:$verbose)@priority{createdAt metadata tags}' + + it('matches the pre-consolidation pipeline byte-for-byte', () => { + const ast = parse(representative) + assert.equal(defaultEngineReportingSignature(ast, 'GetUser'), expected) + }) + + it('hides literals, drops aliases, and sorts arguments / selections / variableDefinitions', () => { + const ast = parse('query Q($z: Int, $a: ID!) { aliased: f(b: 2, a: 1, ids: [1, 2]) { y x } }') + assert.equal( + defaultEngineReportingSignature(ast, 'Q'), + 'query Q($a:ID!,$z:Int){f(a:0,b:0,ids:[]){x y}}' + ) + }) + + it('keeps duplicate variable and directive sort keys stable', () => { + const ast = parse(` + query Q($a: Int, $a: String) { + ...F + } + fragment F on Query @same(b: 2) @same(a: 1) { + f + } + `) + + assert.equal( + defaultEngineReportingSignature(ast, 'Q'), + 'query Q($a:Int,$a:String){...F}fragment F on Query@same(b:0)@same(a:0){f}' + ) + }) +}) + +describe('graphql signature fallback', () => { + it('uses the tools signature when they can load', () => { + delete require.cache[require.resolve('../../src/utils')] + const { getSignature } = require('../../src/utils') + const ast = parse('query Q { f(a: "value") }') + + assert.equal(getSignature(ast, 'Q', 'query'), 'query Q{f(a:"")}') + }) + + it('uses operation type and name when the tools cannot load', () => { + const utilsPath = require.resolve('../../src/utils') + const toolsPath = require.resolve('../../src/tools') + const originalLoad = loadableModule._load + + delete require.cache[utilsPath] + delete require.cache[toolsPath] + loadableModule._load = function (request, parent, isMain) { + if (parent?.filename === utilsPath && request === './tools') { + throw new Error('load failed') + } + return originalLoad(request, parent, isMain) + } + + try { + const { getSignature } = require('../../src/utils') + + assert.equal(getSignature({}, 'Q', 'query'), 'query Q') + assert.equal(getSignature({}, 'Q'), 'Q') + } finally { + loadableModule._load = originalLoad + delete require.cache[utilsPath] + delete require.cache[toolsPath] + } + }) + + it('adds configured error extensions to span events', () => { + const { extractErrorIntoSpanEvent } = require('../../src/utils') + const span = { + addEvent: sinon.spy(), + } + const error = { + name: 'GraphQLError', + message: 'test', + stack: 'stack', + extensions: { + code: 'E_TEST', + retryable: true, + detail: 42, + }, + locations: [{ line: 1, column: 2 }], + path: ['hello', 0], + } + + extractErrorIntoSpanEvent({ + DD_TRACE_GRAPHQL_ERROR_EXTENSIONS: ['code', 'retryable', 'detail', 'missing'], + }, span, error) + + const [name, attributes] = span.addEvent.firstCall.args + assert.equal(name, 'dd.graphql.query.error') + assertObjectContains(attributes, { + type: 'GraphQLError', + message: 'test', + stacktrace: 'stack', + 'extensions.code': 'E_TEST', + 'extensions.retryable': true, + 'extensions.detail': 42, + locations: ['1:2'], + path: ['hello', '0'], + }) + assert.ok(!Object.hasOwn(attributes, 'extensions.missing')) + }) +}) diff --git a/packages/datadog-plugin-grpc/src/client.js b/packages/datadog-plugin-grpc/src/client.js index e1f96a37a9f..23bce206cd4 100644 --- a/packages/datadog-plugin-grpc/src/client.js +++ b/packages/datadog-plugin-grpc/src/client.js @@ -86,12 +86,11 @@ class GrpcClientPlugin extends ClientPlugin { // The only scheme we want to support here is ipv[46]:port, although // more are supported by the library // https://github.com/grpc/grpc/blob/v1.60.0/doc/naming.md - const parts = peer.split(':') - if (/^\d+/.test(parts.at(-1))) { - const port = parts.at(-1) - const ip = parts.slice(0, -1).join(':') - span.setTag('network.destination.ip', ip) - span.setTag('network.destination.port', port) + const colonIndex = peer.lastIndexOf(':') + const tail = colonIndex === -1 ? '' : peer.slice(colonIndex + 1) + if (tail && /^\d+$/.test(tail)) { + span.setTag('network.destination.ip', peer.slice(0, colonIndex)) + span.setTag('network.destination.port', tail) } else { span.setTag('network.destination.ip', peer) } @@ -121,7 +120,7 @@ function inject (tracer, span, metadata) { tracer.inject(span, TEXT_MAP, carrier) - for (const key in carrier) { + for (const key of Object.keys(carrier)) { metadata.set(key, carrier[key]) } } diff --git a/packages/datadog-plugin-grpc/src/util.js b/packages/datadog-plugin-grpc/src/util.js index 67afa3502de..b083ebfee72 100644 --- a/packages/datadog-plugin-grpc/src/util.js +++ b/packages/datadog-plugin-grpc/src/util.js @@ -3,46 +3,81 @@ const pick = require('../../datadog-core/src/utils/src/pick') const log = require('../../dd-trace/src/log') +/** + * @typedef {object} ParsedMethodPath + * @property {string} name + * @property {string} service + * @property {string} package + */ + +// Sentinel returned by `getFilter` when the user has not configured a metadata +// filter. `addMetadataTags` short-circuits on this identity to skip the +// `metadata.getMap()` clone in the default no-filter case. function getEmptyObject () { return {} } -module.exports = { - getMethodMetadata (path, kind) { - const tags = { - path, - kind, - name: '', - service: '', - package: '', +/** + * gRPC method paths are stable per service definition (e.g. + * `/pkg.Service/Method`); a service typically only has a small finite set. + * Cache the parsed `{name, service, package}` triple by path so we skip the + * `path.split('/')` + `serviceParts.split('.')` + `serviceParts.pop()` work + * on every call. + * + * @type {Map} + */ +const methodPathCache = new Map() + +/** + * @param {string} path + * @returns {ParsedMethodPath} + */ +function parseMethodPath (path) { + const methodParts = path.split('/') + + if (methodParts.length > 2) { + const serviceParts = methodParts[1].split('.') + return { + name: methodParts[2], + service: serviceParts.pop(), + package: serviceParts.join('.'), } + } - if (typeof path !== 'string') return tags + return { name: methodParts.at(-1), service: '', package: '' } +} - const methodParts = path.split('/') +module.exports = { + getEmptyObject, - if (methodParts.length > 2) { - const serviceParts = methodParts[1].split('.') - const name = methodParts[2] - const service = serviceParts.pop() - const pkg = serviceParts.join('.') + getMethodMetadata (path, kind) { + if (typeof path !== 'string') { + return { path, kind, name: '', service: '', package: '' } + } - tags.name = name - tags.service = service - tags.package = pkg - } else { - tags.name = methodParts.at(-1) + let parsed = methodPathCache.get(path) + if (parsed === undefined) { + parsed = parseMethodPath(path) + methodPathCache.set(path, parsed) } - return tags + return { + path, + kind, + name: parsed.name, + service: parsed.service, + package: parsed.package, + } }, addMetadataTags (span, metadata, filter, type) { if (!metadata || typeof metadata.getMap !== 'function') return + // Default no-op filter: skip the full metadata clone via `getMap()`. + if (filter === getEmptyObject) return const values = filter(metadata.getMap()) - for (const key in values) { + for (const key of Object.keys(values)) { span.setTag(`grpc.${type}.metadata.${key}`, values[key]) } }, diff --git a/packages/datadog-plugin-grpc/test/peer-tags.spec.js b/packages/datadog-plugin-grpc/test/peer-tags.spec.js new file mode 100644 index 00000000000..24fc37f4e35 --- /dev/null +++ b/packages/datadog-plugin-grpc/test/peer-tags.spec.js @@ -0,0 +1,86 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { describe, it } = require('mocha') + +require('../../dd-trace/test/setup/core') + +const GrpcClientPlugin = require('../src/client') + +// Exercise the peer-string parser inside `GrpcClientPlugin.prototype.finish` +// directly via `.call(fakeThis, ...)`. The tightened parser only emits +// `network.destination.port` when the last segment is strictly numeric +// *and* a colon is present; the previous `/^\d+/` and `split(':')` shape +// let two malformed-peer cases through. +function tagsFor (peer) { + const tags = {} + const fakeSpan = { + setTag (key, value) { tags[key] = value }, + finish () {}, + } + const fakePlugin = { + config: {}, + addCode () {}, + tagPeerService () {}, + } + GrpcClientPlugin.prototype.finish.call(fakePlugin, { span: fakeSpan, result: {}, peer }) + return tags +} + +describe('grpc client finish peer-string tags', () => { + it('splits a standard `ipv4:port` peer', () => { + assert.deepStrictEqual(tagsFor('127.0.0.1:50051'), { + 'network.destination.ip': '127.0.0.1', + 'network.destination.port': '50051', + }) + }) + + it('splits an IPv6-style peer on the last colon only', () => { + assert.deepStrictEqual(tagsFor('[::1]:50051'), { + 'network.destination.ip': '[::1]', + 'network.destination.port': '50051', + }) + assert.deepStrictEqual(tagsFor('::1:50051'), { + 'network.destination.ip': '::1', + 'network.destination.port': '50051', + }) + }) + + it('drops the port tag when the trailing segment is only partially numeric', () => { + // Regression: `/^\d+/` was unanchored, so the previous parser tagged + // `port='80abc'` and `ip='1.2.3.4'`. The anchored `/^\d+$/` rejects + // the entire tail and falls back to tagging the raw peer. + assert.deepStrictEqual(tagsFor('1.2.3.4:80abc'), { + 'network.destination.ip': '1.2.3.4:80abc', + }) + }) + + it('drops the port tag for pure-digit peers without a colon', () => { + // Regression: `'8080'.split(':') === ['8080']`, the unanchored regex + // matched, and `parts.slice(0, -1).join(':')` produced an empty `ip`, + // so a peer with no host info leaked `ip=''` plus a numeric `port`. + assert.deepStrictEqual(tagsFor('8080'), { 'network.destination.ip': '8080' }) + assert.deepStrictEqual(tagsFor('12abc'), { 'network.destination.ip': '12abc' }) + }) + + it('tags the raw peer for unix-socket peers', () => { + assert.deepStrictEqual(tagsFor('unix:'), { 'network.destination.ip': 'unix:' }) + assert.deepStrictEqual(tagsFor('unix:/tmp/socket'), { 'network.destination.ip': 'unix:/tmp/socket' }) + }) + + it('tags the raw peer when there is no colon and no digits', () => { + assert.deepStrictEqual(tagsFor('localhost'), { 'network.destination.ip': 'localhost' }) + }) + + it('still tags an empty ip when the host half is empty (existing behaviour)', () => { + // Boundary: the parser does not require a non-empty *host* — a malformed + // peer with an empty host but a strictly numeric port still yields the + // (empty, numeric) split. This matches both the previous and the new + // parser; pinning it so a future tightening does not drop it silently. + assert.deepStrictEqual(tagsFor(':50051'), { + 'network.destination.ip': '', + 'network.destination.port': '50051', + }) + }) +}) diff --git a/packages/datadog-plugin-grpc/test/util.spec.js b/packages/datadog-plugin-grpc/test/util.spec.js new file mode 100644 index 00000000000..323b59abb7c --- /dev/null +++ b/packages/datadog-plugin-grpc/test/util.spec.js @@ -0,0 +1,130 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { afterEach, describe, it } = require('mocha') + +const { addMetadataTags, getEmptyObject, getFilter, getMethodMetadata } = require('../src/util') + +describe('grpc util', () => { + describe('getMethodMetadata', () => { + it('parses a fully-qualified method path', () => { + const result = getMethodMetadata('/pkg.sub.Service/Method', 'unary') + assert.deepStrictEqual(result, { + path: '/pkg.sub.Service/Method', + kind: 'unary', + name: 'Method', + service: 'Service', + package: 'pkg.sub', + }) + }) + + it('parses a path without a package', () => { + const result = getMethodMetadata('/Service/Method', 'serverStream') + assert.deepStrictEqual(result, { + path: '/Service/Method', + kind: 'serverStream', + name: 'Method', + service: 'Service', + package: '', + }) + }) + + it('falls back when the path is not fully qualified', () => { + const result = getMethodMetadata('LegacyMethod', 'unary') + assert.deepStrictEqual(result, { + path: 'LegacyMethod', + kind: 'unary', + name: 'LegacyMethod', + service: '', + package: '', + }) + }) + + it('returns empty fields when the path is not a string', () => { + const result = getMethodMetadata(undefined, 'unary') + assert.deepStrictEqual(result, { + path: undefined, + kind: 'unary', + name: '', + service: '', + package: '', + }) + }) + + it('threads the kind through on a cache hit', () => { + const path = '/cache.Hit.Service/Method' + const first = getMethodMetadata(path, 'unary') + const second = getMethodMetadata(path, 'serverStream') + assert.strictEqual(first.kind, 'unary') + assert.strictEqual(second.kind, 'serverStream') + assert.strictEqual(first.name, second.name) + assert.strictEqual(first.service, second.service) + assert.strictEqual(first.package, second.package) + }) + }) + + describe('addMetadataTags', () => { + function fakeMetadata (map) { + return { getMap: () => map } + } + + function fakeSpan () { + const tags = {} + return { + tags, + setTag (key, value) { tags[key] = value }, + } + } + + it('skips the metadata clone when the default empty filter is in use', () => { + const span = fakeSpan() + let called = false + const metadata = { + getMap () { + called = true + return { traceparent: 'should-not-leak' } + }, + } + addMetadataTags(span, metadata, getEmptyObject, 'request') + assert.strictEqual(called, false) + assert.deepStrictEqual(span.tags, {}) + }) + + it('forwards filtered values to setTag on the span', () => { + const span = fakeSpan() + const filter = (map) => ({ 'x-trace-id': map['x-trace-id'] }) + addMetadataTags(span, fakeMetadata({ 'x-trace-id': 'abc', secret: 'shh' }), filter, 'request') + assert.deepStrictEqual(span.tags, { 'grpc.request.metadata.x-trace-id': 'abc' }) + }) + + const objectProto = Object.prototype + + afterEach(() => { + // Defence-in-depth: tests shouldn't leak prototype pollution. + delete objectProto.injected + }) + + it('does not pick up inherited keys when iterating filter output', () => { + const span = fakeSpan() + objectProto.injected = 'leak' + const filter = () => ({ direct: 'kept' }) + addMetadataTags(span, fakeMetadata({}), filter, 'response') + assert.deepStrictEqual(span.tags, { 'grpc.response.metadata.direct': 'kept' }) + }) + }) + + describe('getFilter', () => { + it('returns the same empty-object sentinel when no filter is configured', () => { + const filterA = getFilter({}, 'metadata') + const filterB = getFilter({}, 'metadata') + assert.strictEqual(filterA, getEmptyObject) + assert.strictEqual(filterB, getEmptyObject) + }) + + it('returns the user-provided function as-is', () => { + const userFilter = (input) => input + assert.strictEqual(getFilter({ metadata: userFilter }, 'metadata'), userFilter) + }) + }) +}) diff --git a/packages/datadog-plugin-http/src/client.js b/packages/datadog-plugin-http/src/client.js index c1284f83f76..36b2c17be31 100644 --- a/packages/datadog-plugin-http/src/client.js +++ b/packages/datadog-plugin-http/src/client.js @@ -39,10 +39,10 @@ class HttpClientPlugin extends ClientPlugin { // TODO delegate to super.startspan const span = this.startSpan(this.operationName(), { childOf, - integrationName: this.constructor.id, + integrationName: this.component, service: this.serviceName({ pluginConfig: this.config, sessionDetails: extractSessionDetails(options) }), meta: { - [COMPONENT]: this.constructor.id, + [COMPONENT]: this.component, 'span.kind': 'client', 'resource.name': method, 'span.type': 'http', diff --git a/packages/datadog-plugin-http/test/server.spec.js b/packages/datadog-plugin-http/test/server.spec.js index bf986e4ac10..d8a3b2747a5 100644 --- a/packages/datadog-plugin-http/test/server.spec.js +++ b/packages/datadog-plugin-http/test/server.spec.js @@ -258,6 +258,61 @@ describe('Plugin', () => { }) }) + // see https://github.com/DataDog/dd-trace-js/issues/2334 + describe('with hooks configuration', () => { + beforeEach(() => { + return agent.load('http', { + client: false, + server: { + hooks: { + request: (span, req) => { + span.setTag('test.hook', 'ran') + if (req.url?.startsWith('/products')) { + span.setTag('resource.name', 'GET /products') + } + }, + }, + }, + }) + .then(() => { + http = require(pluginToBeLoaded) + }) + }) + + beforeEach(done => { + const server = new http.Server(listener) + appListener = server + .listen(0, 'localhost', () => { + port = appListener.address().port + done() + }) + }) + + it('should let the request hook override the default resource name', done => { + agent + .assertSomeTraces(traces => { + assert.strictEqual(traces[0][0].resource, 'GET /products') + assert.strictEqual(traces[0][0].meta['test.hook'], 'ran') + }) + .then(done) + .catch(done) + + axios.get(`http://localhost:${port}/products`).catch(done) + }) + + it('should keep the default resource name when the hook does not touch it', done => { + agent + .assertSomeTraces(traces => { + assert.strictEqual(traces[0][0].resource, 'GET') + assert.strictEqual(traces[0][0].meta['test.hook'], 'ran') + }) + .then(done) + .catch(done) + + axios.get(`http://localhost:${port}/health`).catch(done) + }) + }) + describe('with a blocklist configuration', () => { beforeEach(() => { return agent.load('http', { client: false, blocklist: '/health' }) diff --git a/packages/datadog-plugin-ioredis/test/index.spec.js b/packages/datadog-plugin-ioredis/test/index.spec.js index 4359ad2dbae..37b49ec4bab 100644 --- a/packages/datadog-plugin-ioredis/test/index.spec.js +++ b/packages/datadog-plugin-ioredis/test/index.spec.js @@ -55,6 +55,22 @@ describe('Plugin', () => { }, { spanResourceMatch: /^get$/ }) }) + it('formats numeric args without coercing to ?', async () => { + await redis.expire('foo', 60) + + await agent.assertFirstTraceSpan({ + meta: { 'redis.raw_command': 'EXPIRE foo 60' }, + }, { spanResourceMatch: /^expire$/ }) + }) + + it('redacts non-string non-number args as ?', async () => { + await redis.set('foo', Buffer.from('binary-value')) + + await agent.assertFirstTraceSpan({ + meta: { 'redis.raw_command': 'SET foo ?' }, + }, { spanResourceMatch: /^set$/ }) + }) + it('should run the callback in the parent context', () => { const span = tracer.startSpan('test') diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index b56046be72a..3d123ac571f 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -21,6 +21,7 @@ const { TEST_SOURCE_START, TEST_ITR_UNSKIPPABLE, TEST_ITR_FORCED_RUN, + TEST_ITR_SKIPPING_ENABLED, TEST_CODE_OWNERS, ITR_CORRELATION_ID, TEST_SOURCE_FILE, @@ -107,6 +108,7 @@ class JestPlugin extends CiPlugin { process.on('message', handler) } this.testSuiteSpanPerTestSuiteAbsolutePath = new Map() + this.pendingTestSuiteFinishes = new Set() this.addSub('ci:jest:session:finish', ({ status, @@ -123,58 +125,66 @@ class JestPlugin extends CiPlugin { isTestManagementTestsEnabled, onDone, }) => { - this.testSessionSpan.setTag(TEST_STATUS, status) - this.testModuleSpan.setTag(TEST_STATUS, status) + const finishSession = () => { + this.testSessionSpan.setTag(TEST_STATUS, status) + this.testModuleSpan.setTag(TEST_STATUS, status) - if (error) { - this.testSessionSpan.setTag('error', error) - this.testModuleSpan.setTag('error', error) - } - - addIntelligentTestRunnerSpanTags( - this.testSessionSpan, - this.testModuleSpan, - { - isSuitesSkipped, - isSuitesSkippingEnabled, - isCodeCoverageEnabled, - testCodeCoverageLinesTotal, - skippingType: 'suite', - skippingCount: numSkippedSuites, - hasUnskippableSuites, - hasForcedToRunSuites, + if (error) { + this.testSessionSpan.setTag('error', error) + this.testModuleSpan.setTag('error', error) } - ) - if (isEarlyFlakeDetectionEnabled) { - this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true') - } - if (isEarlyFlakeDetectionFaulty) { - this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty') - } - if (isTestManagementTestsEnabled) { - this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') - } + addIntelligentTestRunnerSpanTags( + this.testSessionSpan, + this.testModuleSpan, + { + isSuitesSkipped, + isSuitesSkippingEnabled, + isCodeCoverageEnabled, + testCodeCoverageLinesTotal, + skippingType: 'suite', + skippingCount: numSkippedSuites, + hasUnskippableSuites, + hasForcedToRunSuites, + } + ) - this.testModuleSpan.finish() - this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') - this.testSessionSpan.finish() - this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session', { - hasFailedTestReplay: this.libraryConfig?.isDiEnabled || undefined, - }) - finishAllTraceSpans(this.testSessionSpan) + if (isEarlyFlakeDetectionEnabled) { + this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true') + } + if (isEarlyFlakeDetectionFaulty) { + this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty') + } + if (isTestManagementTestsEnabled) { + this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true') + } - this.telemetry.count(TELEMETRY_TEST_SESSION, { - provider: this.ciProviderName, - autoInjected: !!this._tracerConfig.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER, - }) + this.testModuleSpan.finish() + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') + this.testSessionSpan.finish() + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session', { + hasFailedTestReplay: this.libraryConfig?.isDiEnabled || undefined, + }) + finishAllTraceSpans(this.testSessionSpan) + + this.telemetry.count(TELEMETRY_TEST_SESSION, { + provider: this.ciProviderName, + autoInjected: !!this._tracerConfig.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER, + }) + + appClosingTelemetry() + this.tracer._exporter.flush(() => { + if (onDone) { + onDone() + } + }) + } - appClosingTelemetry() - this.tracer._exporter.flush(() => { - if (onDone) { - onDone() - } - }) + if (this.pendingTestSuiteFinishes.size > 0) { + Promise.all(this.pendingTestSuiteFinishes).then(finishSession) + } else { + finishSession() + } }) // Test suites can be run in a different process from jest's main one. @@ -197,6 +207,7 @@ class JestPlugin extends CiPlugin { config._ddIsDiEnabled = this.libraryConfig?.isDiEnabled ?? false config._ddIsKnownTestsEnabled = this.libraryConfig?.isKnownTestsEnabled ?? false config._ddIsImpactedTestsEnabled = this.libraryConfig?.isImpactedTestsEnabled ?? false + config._ddItrSkippingEnabledTags = this.getSessionItrSkippingEnabledTags() } }) @@ -217,6 +228,7 @@ class JestPlugin extends CiPlugin { _ddForcedToRun, _ddUnskippable, _ddTestCodeCoverageEnabled, + _ddItrSkippingEnabledTags: itrSkippingEnabledTags, } = testEnvironmentOptions const testSessionSpanContext = this.tracer.extract('text_map', { @@ -228,6 +240,7 @@ class JestPlugin extends CiPlugin { ...getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, 'jest'), // requestErrorTags from test env options may be undefined ...(requestErrorTags !== undefined && requestErrorTags !== null ? requestErrorTags : {}), + ...(itrSkippingEnabledTags !== undefined && itrSkippingEnabledTags !== null ? itrSkippingEnabledTags : {}), } if (_ddUnskippable) { @@ -305,10 +318,18 @@ class JestPlugin extends CiPlugin { } }) - this.addSub('ci:jest:test-suite:finish', ({ status, errorMessage, error, testSuiteAbsolutePath }) => { + this.addSub('ci:jest:test-suite:finish', ({ + status, + errorMessage, + error, + testSuiteAbsolutePath, + waitForFinish, + onDone, + }) => { const testSuiteSpan = this.testSuiteSpanPerTestSuiteAbsolutePath.get(testSuiteAbsolutePath) if (!testSuiteSpan) { log.warn('"ci:jest:test-suite:finish": no span found for test suite absolute path %s', testSuiteAbsolutePath) + onDone?.() return } testSuiteSpan.setTag(TEST_STATUS, status) @@ -319,8 +340,13 @@ class JestPlugin extends CiPlugin { testSuiteSpan.setTag('error', new Error(errorMessage)) testSuiteSpan.setTag(TEST_STATUS, 'fail') } - // We need to give the potential error in 'ci:jest:test-suite:error' time to be published - process.nextTick(() => { + let resolvePendingFinish + const pendingFinish = new Promise(resolve => { + resolvePendingFinish = resolve + }) + this.pendingTestSuiteFinishes.add(pendingFinish) + + const finish = () => { testSuiteSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite') // Suites potentially run in a different process than the session, @@ -334,7 +360,19 @@ class JestPlugin extends CiPlugin { } this.removeAllDiProbes() this.testSuiteSpanPerTestSuiteAbsolutePath.delete(testSuiteAbsolutePath) - }) + this.pendingTestSuiteFinishes.delete(pendingFinish) + resolvePendingFinish() + if (onDone) { + onDone() + } + } + + if (waitForFinish) { + // Give late async work time to run before Jest restarts a worker because of workerIdleMemoryLimit. + realSetTimeout(finish) + } else { + process.nextTick(finish) + } }) this.addSub('ci:jest:test-suite:error', ({ error, errorMessage, testSuiteAbsolutePath }) => { @@ -558,6 +596,10 @@ class JestPlugin extends CiPlugin { extraTags[TEST_HAS_DYNAMIC_NAME] = 'true' } const testSuiteSpan = this.testSuiteSpanPerTestSuiteAbsolutePath.get(testSuiteAbsolutePath) || this.testSuiteSpan + const skippingEnabled = testSuiteSpan?.context()._tags?.[TEST_ITR_SKIPPING_ENABLED] + if (skippingEnabled !== undefined) { + extraTags[TEST_ITR_SKIPPING_ENABLED] = skippingEnabled + } return super.startTestSpan(name, suite, testSuiteSpan, extraTags) } diff --git a/packages/datadog-plugin-kafkajs/test/dsm.spec.js b/packages/datadog-plugin-kafkajs/test/dsm.spec.js index 3d06146c2fc..5cf97ccc295 100644 --- a/packages/datadog-plugin-kafkajs/test/dsm.spec.js +++ b/packages/datadog-plugin-kafkajs/test/dsm.spec.js @@ -17,17 +17,11 @@ const { assertObjectContains } = require('../../../integration-tests/helpers') const testKafkaClusterId = '5L6g3nShT-eMCtK--X86sw' -const getDsmPathwayHash = (testTopic, clusterIdAvailable, isProducer, parentHash) => { - let edgeTags - if (isProducer) { - edgeTags = ['direction:out', 'topic:' + testTopic, 'type:kafka'] - } else { - edgeTags = ['direction:in', 'group:test-group', 'topic:' + testTopic, 'type:kafka'] - } - - if (clusterIdAvailable) { - edgeTags.push(`kafka_cluster_id:${testKafkaClusterId}`) - } +const getDsmPathwayHash = (testTopic, isProducer, parentHash) => { + const edgeTags = isProducer + ? ['direction:out', 'topic:' + testTopic, 'type:kafka'] + : ['direction:in', 'group:test-group', 'topic:' + testTopic, 'type:kafka'] + edgeTags.push(`kafka_cluster_id:${testKafkaClusterId}`) edgeTags.sort() return computePathwayHash('test', 'tester', edgeTags, parentHash, propagationHash.getHash()) } @@ -45,7 +39,6 @@ describe('Plugin', () => { let admin let tracer let Kafka - let clusterIdAvailable let expectedProducerHash let expectedConsumerHash let testTopic @@ -86,9 +79,8 @@ describe('Plugin', () => { replicationFactor: 1, })), }) - clusterIdAvailable = semver.intersects(version, '>=1.13') - expectedProducerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, true, ENTRY_PARENT_HASH) - expectedConsumerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, false, expectedProducerHash) + expectedProducerHash = getDsmPathwayHash(testTopic, true, ENTRY_PARENT_HASH) + expectedConsumerHash = getDsmPathwayHash(testTopic, false, expectedProducerHash) }) describe('checkpoints', () => { @@ -310,15 +302,11 @@ describe('Plugin', () => { assert.strictEqual(runArg?.offset, commitMeta.offset) assert.strictEqual(runArg?.partition, commitMeta.partition) assert.strictEqual(runArg?.topic, commitMeta.topic) - const expectedBacklog = { + assertObjectContains(runArg, { type: 'kafka_commit', consumer_group: 'test-group', - } - // kafka_cluster_id is only available in kafkajs >=1.13 - if (clusterIdAvailable) { - expectedBacklog.kafka_cluster_id = testKafkaClusterId - } - assertObjectContains(runArg, expectedBacklog) + kafka_cluster_id: testKafkaClusterId, + }) }) } @@ -327,10 +315,7 @@ describe('Plugin', () => { sinon.assert.calledOnce(setOffsetSpy) const runArg = setOffsetSpy.lastCall.args[0] assert.strictEqual(runArg.topic, testTopic) - // kafka_cluster_id is only available in kafkajs >=1.13 - if (clusterIdAvailable) { - assert.strictEqual(runArg.kafka_cluster_id, testKafkaClusterId) - } + assert.strictEqual(runArg.kafka_cluster_id, testKafkaClusterId) }) }) }) diff --git a/packages/datadog-plugin-kafkajs/test/index.spec.js b/packages/datadog-plugin-kafkajs/test/index.spec.js index 8be7fa3f4c9..09ed36fb273 100644 --- a/packages/datadog-plugin-kafkajs/test/index.spec.js +++ b/packages/datadog-plugin-kafkajs/test/index.spec.js @@ -12,7 +12,7 @@ const { withNamingSchema, withPeerService, withVersions } = require('../../dd-tr const agent = require('../../dd-trace/test/plugins/agent') const { expectSomeSpan, withDefaults } = require('../../dd-trace/test/plugins/helpers') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') -const { assertObjectContains } = require('../../../integration-tests/helpers') +const { assertObjectContains, deepFreeze } = require('../../../integration-tests/helpers') const { expectedSchema, rawExpectedSchema } = require('./naming') @@ -32,12 +32,10 @@ describe('Plugin', () => { let tracer let Kafka let Broker - let clusterIdAvailable let testTopic describe('without configuration', () => { const messages = [{ key: 'key1', value: 'test2' }] - const messages2 = [{ key: 'key2', value: 'test3' }] beforeEach(async () => { tracer = require('../../dd-trace') @@ -60,23 +58,20 @@ describe('Plugin', () => { replicationFactor: 1, }], }) - clusterIdAvailable = semver.intersects(version, '>=1.13') }) describe('producer', () => { it('should be instrumented', async () => { - const meta = { - 'span.kind': 'producer', - component: 'kafkajs', - 'messaging.destination.name': testTopic, - 'messaging.kafka.bootstrap.servers': '127.0.0.1:9092', - } - if (clusterIdAvailable) meta['kafka.cluster_id'] = testKafkaClusterId - const expectedSpanPromise = expectSpanWithDefaults({ name: expectedSchema.send.opName, service: expectedSchema.send.serviceName, - meta, + meta: { + 'span.kind': 'producer', + component: 'kafkajs', + 'messaging.destination.name': testTopic, + 'messaging.kafka.bootstrap.servers': '127.0.0.1:9092', + 'kafka.cluster_id': testKafkaClusterId, + }, metrics: { 'kafka.batch_size': messages.length, }, @@ -90,6 +85,18 @@ describe('Plugin', () => { return expectedSpanPromise }) + it('should not call Kafka.admin from instrumentation during normal send', async () => { + // Spy after the test setup has already created topics; from here + // on no internal admin connection should be opened. + const adminSpy = sinon.spy(kafka, 'admin') + try { + await sendMessages(kafka, testTopic, messages) + assert.strictEqual(adminSpy.callCount, 0) + } finally { + adminSpy.restore() + } + }) + withPeerService( () => tracer, 'kafkajs', @@ -98,6 +105,17 @@ describe('Plugin', () => { 'messaging.kafka.bootstrap.servers' ) + it('should not mutate user-supplied message objects', async () => { + // Deep-freezing the input means any accidental write to a + // message, its headers, or the array itself throws synchronously. + const userMessages = deepFreeze([ + { key: 'key', value: 'value', headers: { foo: 'bar' } }, + { key: 'key2', value: 'value2' }, + ]) + + await sendMessages(kafka, testTopic, userMessages) + }) + it('should be instrumented w/ error', async () => { const producer = kafka.producer() const resourceName = expectedSchema.send.opName @@ -190,16 +208,38 @@ describe('Plugin', () => { }) it('should hit an error for the first send and not inject headers in later sends', async () => { - await assert.rejects(producer.send({ topic: testTopic, messages }), error) + const startCh = dc.channel('apm:kafkajs:produce:start') + const sentMessageBatches = [] + const captureStart = (ctx) => sentMessageBatches.push(ctx.messages) + startCh.subscribe(captureStart) - assert.ok(Object.hasOwn(messages[0].headers, 'x-datadog-trace-id')) - - // restore the stub to allow the next send to succeed - sendRequestStub.restore() + // Freeze both batches: any boundary or plugin write to the + // user's array, its messages, or their headers throws here. + const firstBatch = deepFreeze([{ key: 'key1', value: 'test2' }]) + const secondBatch = deepFreeze([{ key: 'key2', value: 'test3' }]) - const result2 = await producer.send({ topic: testTopic, messages: messages2 }) - assert.strictEqual(messages2[0].headers, undefined) - assert.strictEqual(result2[0].errorCode, 0) + try { + await assert.rejects(producer.send({ topic: testTopic, messages: firstBatch }), error) + + // The first send injects trace headers into the cloned + // batch that kafkajs serializes. + assert.ok(Object.hasOwn(sentMessageBatches[0][0].headers, 'x-datadog-trace-id')) + + sendRequestStub.restore() + + const result2 = await producer.send({ topic: testTopic, messages: secondBatch }) + + // After UNKNOWN the boundary clones with `cloneMessages` + // (frozen input stays untouched) and the clone has no + // `headers` field at all — brokers that reject any header + // field can recover. + const [clonedAfterDisable] = sentMessageBatches[1] + assert.notStrictEqual(clonedAfterDisable, secondBatch[0]) + assert.strictEqual(Object.hasOwn(clonedAfterDisable, 'headers'), false) + assert.strictEqual(result2[0].errorCode, 0) + } finally { + startCh.unsubscribe(captureStart) + } }) }) @@ -421,19 +461,17 @@ describe('Plugin', () => { }) it('should be instrumented', async () => { - const meta = { - 'span.kind': 'consumer', - component: 'kafkajs', - 'kafka.topic': testTopic, - 'messaging.destination.name': testTopic, - 'messaging.system': 'kafka', - } - if (clusterIdAvailable) meta['kafka.cluster_id'] = testKafkaClusterId - const expectedSpanPromise = expectSpanWithDefaults({ name: expectedSchema.receive.opName, service: expectedSchema.receive.serviceName, - meta, + meta: { + 'span.kind': 'consumer', + component: 'kafkajs', + 'kafka.topic': testTopic, + 'messaging.destination.name': testTopic, + 'messaging.system': 'kafka', + 'kafka.cluster_id': testKafkaClusterId, + }, metrics: { 'messaging.batch.message_count': batchMessages.length, }, @@ -492,19 +530,17 @@ describe('Plugin', () => { const messagesWithHeaders = [ { key: 'key1', value: 'test1', headers: { 'x-custom-header': 'value' } }, ] - const meta = { - 'span.kind': 'consumer', - component: 'kafkajs', - 'kafka.topic': testTopic, - 'messaging.destination.name': testTopic, - 'messaging.system': 'kafka', - } - if (clusterIdAvailable) meta['kafka.cluster_id'] = testKafkaClusterId - const expectedSpanPromise = expectSpanWithDefaults({ name: expectedSchema.receive.opName, service: expectedSchema.receive.serviceName, - meta, + meta: { + 'span.kind': 'consumer', + component: 'kafkajs', + 'kafka.topic': testTopic, + 'messaging.destination.name': testTopic, + 'messaging.system': 'kafka', + 'kafka.cluster_id': testKafkaClusterId, + }, resource: testTopic, error: 0, type: 'worker', diff --git a/packages/datadog-plugin-mocha/src/index.js b/packages/datadog-plugin-mocha/src/index.js index c7a78a2ca8b..6c1e5ae9034 100644 --- a/packages/datadog-plugin-mocha/src/index.js +++ b/packages/datadog-plugin-mocha/src/index.js @@ -105,6 +105,7 @@ class MochaPlugin extends CiPlugin { 'mocha' ), ...this.getSessionRequestErrorTags(), + ...this.getSessionItrSkippingEnabledTags(), } if (isUnskippable) { testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true' diff --git a/packages/datadog-plugin-mongodb-core/src/index.js b/packages/datadog-plugin-mongodb-core/src/index.js index 1bcbe0eda7e..a6975a55edd 100644 --- a/packages/datadog-plugin-mongodb-core/src/index.js +++ b/packages/datadog-plugin-mongodb-core/src/index.js @@ -90,9 +90,8 @@ class MongodbCorePlugin extends DatabasePlugin { } } -function sanitizeBigInt (data) { - return JSON.stringify(data, (_key, value) => typeof value === 'bigint' ? value.toString() : value) -} +const MAX_DEPTH = 10 +const MAX_QUERY_LENGTH = 10_000 function extractQuery (statements) { if (statements.length === 1 && statements[0].q) return statements[0].q @@ -100,7 +99,7 @@ function extractQuery (statements) { const extractedQueries = [] for (let i = 0; i < statements.length; i++) { if (statements[i].q) { - extractedQueries.push(limitDepth(statements[i].q)) + extractedQueries.push(statements[i].q) } } @@ -110,12 +109,12 @@ function extractQuery (statements) { function getQuery (cmd) { if (!cmd || (typeof cmd !== 'object' && !Array.isArray(cmd))) return - if (Array.isArray(cmd)) return sanitizeBigInt(extractQuery(cmd)) - if (cmd.query) return sanitizeBigInt(limitDepth(cmd.query)) - if (cmd.filter) return sanitizeBigInt(limitDepth(cmd.filter)) - if (cmd.pipeline) return sanitizeBigInt(limitDepth(cmd.pipeline)) - if (cmd.deletes) return sanitizeBigInt(extractQuery(cmd.deletes)) - if (cmd.updates) return sanitizeBigInt(extractQuery(cmd.updates)) + if (Array.isArray(cmd)) return sanitiseAndStringify(extractQuery(cmd)) + if (cmd.query) return sanitiseAndStringify(cmd.query) + if (cmd.filter) return sanitiseAndStringify(cmd.filter) + if (cmd.pipeline) return sanitiseAndStringify(cmd.pipeline) + if (cmd.deletes) return sanitiseAndStringify(extractQuery(cmd.deletes)) + if (cmd.updates) return sanitiseAndStringify(extractQuery(cmd.updates)) } function getResource (plugin, ns, query, operationName) { @@ -129,73 +128,40 @@ function getResource (plugin, ns, query, operationName) { } function truncate (input) { - return input.slice(0, Math.min(input.length, 10_000)) -} - -function shouldSimplify (input) { - return !isObject(input) || typeof input.toJSON === 'function' + return input.length > MAX_QUERY_LENGTH ? input.slice(0, MAX_QUERY_LENGTH) : input } -function shouldHide (input) { - return Buffer.isBuffer(input) || typeof input === 'function' || isBinary(input) -} - -function limitDepth (input) { - if (isBSON(input)) { - input = input.toJSON() - } - - if (shouldHide(input)) return '?' - if (shouldSimplify(input)) return input - - const output = {} - const queue = [{ - input, - output, - depth: 0, - }] - - while (queue.length) { - const { - input, output, depth, - } = queue.pop() - const nextDepth = depth + 1 - for (const key of Object.keys(input)) { - let child = input[key] - if (typeof child === 'function') continue - - if (isBSON(child)) { - child = typeof child.toJSON === 'function' ? child.toJSON() : '?' - } - - if (depth >= 10 || shouldHide(child)) { - output[key] = '?' - } else if (shouldSimplify(child)) { - output[key] = child - } else { - output[key] = {} - queue.push({ - input: child, - output: output[key], - depth: nextDepth, - }) +// Single-pass sanitisation. The replacer: +// - skips functions and coerces bigint to its decimal string, +// - returns '?' for Buffer / BSON Binary on the *original* value (JSON.stringify already invoked +// toJSON before calling us; Buffer / Binary do have toJSON outputs we want to suppress), +// - lets JSON.stringify call toJSON on other BSON types (ObjectId, Long, Decimal128, Date, Timestamp, ...) +// so the result lands here as a primitive or plain object, +// - returns '?' for BSON types without toJSON (MinKey, MaxKey) where `value === original`, +// - tracks depth via an ancestor stack so cycles and depth >= MAX_DEPTH collapse to '?'. +function sanitiseAndStringify (input) { + const ancestors = [] + return JSON.stringify(input, function (key, value) { + if (typeof value === 'function') return + if (typeof value === 'bigint') return value.toString() + + const original = key === '' ? value : this[key] + if (typeof original === 'object' && original !== null) { + if (Buffer.isBuffer(original)) return '?' + const bsontype = original._bsontype + if (bsontype !== undefined && (bsontype === 'Binary' || value === original)) { + return '?' } } - } - return output -} + if (value === null || typeof value !== 'object') return value -function isObject (val) { - return val !== null && typeof val === 'object' && !Array.isArray(val) -} - -function isBSON (val) { - return val && val._bsontype && !isBinary(val) -} + while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop() + if (ancestors.length >= MAX_DEPTH || ancestors.includes(value)) return '?' + ancestors.push(value) -function isBinary (val) { - return val && val._bsontype === 'Binary' + return value + }) } function isHeartbeat (ops, config) { diff --git a/packages/datadog-plugin-mongodb-core/test/core.spec.js b/packages/datadog-plugin-mongodb-core/test/core.spec.js index dcfecaaeb9c..5400cae8c52 100644 --- a/packages/datadog-plugin-mongodb-core/test/core.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/core.spec.js @@ -521,6 +521,61 @@ describe('Plugin', () => { }) }) + describe('with dbmPropagationMode full from tracer configuration', () => { + before(() => { + // Tracer-level config (third arg) only takes effect if the global + // tracer is wiped first; tracer.init() short-circuits once the + // process-wide singleton has been initialized by an earlier load. + agent.wipe() + return agent.load('mongodb-core', {}, { + dbmPropagationMode: 'full', + sampler: { sampleRate: 1 }, + }) + }) + + after(() => { + return agent.close({ ritmReset: false, wipe: true }) + }) + + beforeEach(done => { + const Server = getServer() + + server = new Server({ + host: '127.0.0.1', + port: 27017, + reconnect: false, + }) + + server.on('connect', () => done()) + server.on('error', done) + + server.connect() + + startSpy = sinon.spy(MongodbCorePlugin.prototype, 'start') + }) + + afterEach(() => { + startSpy?.restore() + }) + + it('DBM propagation should inject full mode comment with traceparent', done => { + agent + .assertFirstTraceSpan(span => { + const traceId = span.meta['_dd.p.tid'] + span.trace_id.toString(16).padStart(16, '0') + const spanId = span.span_id.toString(16).padStart(16, '0') + + assert.strictEqual(startSpy.called, true) + const { comment } = startSpy.getCall(0).args[0].ops + assert.ok(comment.includes(`traceparent='00-${traceId}-${spanId}-01'`)) + assert.strictEqual(span.meta['_dd.dbm_trace_injected'], 'true') + }) + .then(done) + .catch(done) + + server.insert(`test.${collection}`, [{ a: 1 }], () => {}) + }) + }) + describe('with dbmPropagationMode service', () => { before(() => { return agent.load('mongodb-core', { dbmPropagationMode: 'service' }) diff --git a/packages/datadog-plugin-mongodb-core/test/limit-depth.spec.js b/packages/datadog-plugin-mongodb-core/test/limit-depth.spec.js index 98715509519..f890f776ec1 100644 --- a/packages/datadog-plugin-mongodb-core/test/limit-depth.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/limit-depth.spec.js @@ -48,4 +48,66 @@ describe('mongodb-core query depth limiter', () => { assert.deepStrictEqual(JSON.parse(query), { outer: { ownNested: 'kept' } }) }) + + it('extracts cmd.filter when no .query is present', () => { + const query = callBindStart({ + ns: 'db.coll', + ops: { filter: { user: 'alice' } }, + name: 'find', + }) + + assert.deepStrictEqual(JSON.parse(query), { user: 'alice' }) + }) + + it('extracts cmd.pipeline when no .query / .filter is present', () => { + const query = callBindStart({ + ns: 'db.coll', + ops: { pipeline: [{ $match: { user: 'alice' } }, { $count: 'total' }] }, + name: 'aggregate', + }) + + assert.deepStrictEqual(JSON.parse(query), [ + { $match: { user: 'alice' } }, + { $count: 'total' }, + ]) + }) + + it('extracts the inner q from a single cmd.deletes statement', () => { + const query = callBindStart({ + ns: 'db.coll', + ops: { deletes: [{ q: { user: 'alice' }, limit: 1 }] }, + name: 'delete', + }) + + assert.deepStrictEqual(JSON.parse(query), { user: 'alice' }) + }) + + it('collects every q from multi-statement cmd.updates', () => { + const query = callBindStart({ + ns: 'db.coll', + ops: { + updates: [ + { q: { user: 'alice' }, u: { $set: { a: 1 } } }, + { q: { user: 'bob' }, u: { $set: { b: 2 } } }, + ], + }, + name: 'update', + }) + + assert.deepStrictEqual(JSON.parse(query), [ + { user: 'alice' }, + { user: 'bob' }, + ]) + }) + + it('renders Binary BSON values as "?"', () => { + const binary = { _bsontype: 'Binary', buffer: Buffer.from('payload') } + const query = callBindStart({ + ns: 'db.coll', + ops: { query: { blob: binary } }, + name: 'find', + }) + + assert.deepStrictEqual(JSON.parse(query), { blob: '?' }) + }) }) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index 5a7adbb2c73..77d1beec628 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -378,6 +378,42 @@ describe('Plugin', () => { }).toArray() }) + it('should collapse beyond max depth', done => { + let nested = { a: 1 } + for (let i = 0; i < 12; i++) { + nested = { a: nested } + } + + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + assert.strictEqual(span.resource, `find test.${collectionName}`) + // 10 levels of `{"a":` then `"?"`, then 10 closing braces. + assert.strictEqual(span.meta['mongodb.query'], `${'{"a":'.repeat(10)}"?"${'}'.repeat(10)}`) + }) + .then(done) + .catch(done) + + collection.find(nested).toArray().catch(() => {}) + }) + + it('should collapse cyclic queries to ?', done => { + const cyclic = { name: 'foo' } + cyclic.self = cyclic + + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + assert.strictEqual(span.resource, `find test.${collectionName}`) + assert.strictEqual(span.meta['mongodb.query'], '{"name":"foo","self":"?"}') + }) + .then(done) + .catch(done) + + // Driver rejects cyclic structures before the wire write; sanitisation runs before that. + collection.find(cyclic).toArray().catch(() => {}) + }) + it('should skip functions when sanitizing', done => { agent .assertSomeTraces(traces => { diff --git a/packages/datadog-plugin-mongodb-core/test/synthesize-topology.spec.js b/packages/datadog-plugin-mongodb-core/test/synthesize-topology.spec.js new file mode 100644 index 00000000000..24b6bc8814e --- /dev/null +++ b/packages/datadog-plugin-mongodb-core/test/synthesize-topology.spec.js @@ -0,0 +1,44 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { describe, it } = require('mocha') + +require('../../dd-trace/test/setup/core') + +const { synthesizeTopology } = require('../../datadog-instrumentations/src/mongodb-core') + +const EMPTY_TOPOLOGY = { s: { options: {} } } + +describe('mongodb-core synthesizeTopology', () => { + it('parses a standard `host:port` address', () => { + assert.deepStrictEqual( + synthesizeTopology('127.0.0.1:27017'), + { s: { options: { host: '127.0.0.1', port: '27017' } } } + ) + }) + + it('returns the empty-options envelope for an IPv6 form (multiple colons)', () => { + assert.deepStrictEqual(synthesizeTopology('[::1]:27017'), EMPTY_TOPOLOGY) + assert.deepStrictEqual(synthesizeTopology('::1:27017'), EMPTY_TOPOLOGY) + }) + + it('returns the empty-options envelope for a random-UUID address (no colon)', () => { + assert.deepStrictEqual(synthesizeTopology('e8a93f01-1234-5678-9abc-def012345678'), EMPTY_TOPOLOGY) + }) + + it('returns the empty-options envelope when the port half is empty', () => { + // Boundary: previous `address.split(':').length === 2` accepted these and + // tagged `host=host, port=''` / `host='', port='27017'`. The tightened + // gate rejects either side being empty. + assert.deepStrictEqual(synthesizeTopology('host:'), EMPTY_TOPOLOGY) + assert.deepStrictEqual(synthesizeTopology(':27017'), EMPTY_TOPOLOGY) + assert.deepStrictEqual(synthesizeTopology(':'), EMPTY_TOPOLOGY) + }) + + it('returns the empty-options envelope for non-string addresses', () => { + assert.deepStrictEqual(synthesizeTopology(undefined), EMPTY_TOPOLOGY) + assert.deepStrictEqual(synthesizeTopology(null), EMPTY_TOPOLOGY) + assert.deepStrictEqual(synthesizeTopology(12_345), EMPTY_TOPOLOGY) + }) +}) diff --git a/packages/datadog-plugin-mysql/src/index.js b/packages/datadog-plugin-mysql/src/index.js index 0911863c655..02c88e42214 100644 --- a/packages/datadog-plugin-mysql/src/index.js +++ b/packages/datadog-plugin-mysql/src/index.js @@ -1,7 +1,7 @@ 'use strict' const { storage } = require('../../datadog-core') -const CLIENT_PORT_KEY = require('../../dd-trace/src/constants') +const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants') const DatabasePlugin = require('../../dd-trace/src/plugins/database') class MySQLPlugin extends DatabasePlugin { diff --git a/packages/datadog-plugin-mysql/test/index.spec.js b/packages/datadog-plugin-mysql/test/index.spec.js index fd35f75d757..903d74956b5 100644 --- a/packages/datadog-plugin-mysql/test/index.spec.js +++ b/packages/datadog-plugin-mysql/test/index.spec.js @@ -93,6 +93,9 @@ describe('Plugin', () => { component: 'mysql', '_dd.integration': 'mysql', }, + metrics: { + 'network.destination.port': 3306, + }, }, { spanResourceMatch: /SELECT 1 \+ 1 AS solution/ }) .then(done) .catch(done) @@ -427,6 +430,46 @@ describe('Plugin', () => { }) }) }) + + describe('with DBM propagation enabled with service using tracer configurations', () => { + let connection + + before(async () => { + // Tracer-level config (third arg) only takes effect if the global + // tracer is wiped first; tracer.init() short-circuits once the + // process-wide singleton has been initialized by an earlier load. + agent.wipe() + await agent.load('mysql', { service: 'serviced' }, { dbmPropagationMode: 'service' }) + mysql = proxyquire(`../../../versions/mysql@${version}`, {}).get() + + connection = mysql.createConnection({ + host: '127.0.0.1', + user: 'root', + database: 'db', + }) + connection.connect() + }) + + after((done) => { + connection.end(() => { + agent.close({ ritmReset: false, wipe: true }).then(done) + }) + }) + + it('should contain service mode comment in query text', done => { + connection.query('SELECT 1 + 1 AS solution', () => { + try { + assert.strictEqual(connection._protocol._queue[0].sql, + '/*dddb=\'db\',dddbs=\'serviced\',dde=\'tester\',ddh=\'127.0.0.1\',ddps=\'test\',' + + `ddpv='${ddpv}'*/ SELECT 1 + 1 AS solution`) + } catch (e) { + done(e) + } + done() + }) + }) + }) + describe('DBM propagation should handle special characters', () => { let connection diff --git a/packages/datadog-plugin-openai/src/services.js b/packages/datadog-plugin-openai/src/services.js index 917aa260544..93c4363ed45 100644 --- a/packages/datadog-plugin-openai/src/services.js +++ b/packages/datadog-plugin-openai/src/services.js @@ -36,7 +36,8 @@ module.exports.init = function (tracerConfig) { interval = setInterval(() => { metrics.flush() - }, FLUSH_INTERVAL).unref() + }, FLUSH_INTERVAL) + interval.unref?.() return { metrics, logger } } diff --git a/packages/datadog-plugin-pg/src/index.js b/packages/datadog-plugin-pg/src/index.js index 9e7e1f5b501..4bf283d1cfc 100644 --- a/packages/datadog-plugin-pg/src/index.js +++ b/packages/datadog-plugin-pg/src/index.js @@ -9,9 +9,9 @@ class PGPlugin extends DatabasePlugin { static system = 'postgres' bindStart (ctx) { - const { params = {}, query, processId, stream } = ctx + const { params = {}, query, originalText, processId, stream } = ctx const service = this.serviceName({ pluginConfig: this.config, params }) - const originalStatement = this.maybeTruncate(query.text) + const originalStatement = this.maybeTruncate(originalText) const span = this.startSpan(this.operationName(), { service, @@ -32,7 +32,7 @@ class PGPlugin extends DatabasePlugin { span.setTag('db.stream', 1) } - query.__ddInjectableQuery = this.injectDbmQuery(span, query.text, service.name, !!query.name) + ctx.injected = this.injectDbmQuery(span, originalText, service.name, !!query.name) return ctx.currentStore } diff --git a/packages/datadog-plugin-pg/test/index.spec.js b/packages/datadog-plugin-pg/test/index.spec.js index 948bb93d663..93f8b90b3b3 100644 --- a/packages/datadog-plugin-pg/test/index.spec.js +++ b/packages/datadog-plugin-pg/test/index.spec.js @@ -784,6 +784,17 @@ describe('Plugin', () => { ) }) + it('reuses prepared statements across calls without "must be unique" error', async () => { + const buildQuery = () => ({ + name: 'pgRepeatedSelect', + text: 'SELECT $1::text as message', + }) + + await client.query(buildQuery(), ['first']) + await client.query(buildQuery(), ['second']) + await client.query(buildQuery(), ['third']) + }) + it('should not fail when using query object with getters', done => { const query = { name: 'pgSelectQuery', @@ -798,6 +809,65 @@ describe('Plugin', () => { '*/ SELECT $1::text as message') }) + it('does not accumulate the DBM comment when reusing a prepared-statement query object', done => { + const expected = + `/*dddb='postgres',dddbs='post',dde='tester',ddh='127.0.0.1',ddps='test',ddpv='${ddpv}'` + + '*/ SELECT $1::text as message' + const query = { + name: 'pgSelectQuery', + text: 'SELECT $1::text as message', + } + + client.query(query, ['Hello world!'], (err) => { + if (err) return done(err) + + client.query(query, ['Hello world!'], (err2) => { + if (err2) return done(err2) + + assert.strictEqual(query.text, expected) + done() + }) + }) + }) + + it('does not accumulate the DBM comment when reusing a getter-shaped query object', done => { + const expected = + `/*dddb='postgres',dddbs='post',dde='tester',ddh='127.0.0.1',ddps='test',ddpv='${ddpv}'` + + '*/ SELECT $1::text as message' + const query = { + name: 'pgSelectQuery', + get text () { return 'SELECT $1::text as message' }, + } + + client.query(query, ['Hello world!'], (err) => { + if (err) return done(err) + + client.query(query, ['Hello world!'], (err2) => { + if (err2) return done(err2) + + assert.strictEqual(query.text, expected) + done() + }) + }) + }) + + it('handles a non-configurable text property without crashing', done => { + const query = { name: 'pgSelectQuery' } + Object.defineProperty(query, 'text', { + value: 'SELECT $1::text as message', + writable: true, + enumerable: true, + configurable: false, + }) + + client.query(query, ['Hello world!'], (err) => { + if (err) return done(err) + + assert.strictEqual(query.text, 'SELECT $1::text as message') + done() + }) + }) + it('should not fail when using query object that is an EventEmitter', done => { class Query extends EventEmitter { constructor (name, text) { @@ -865,5 +935,55 @@ describe('Plugin', () => { }) }) }) + + // Lives outside `withVersions` so the global-tracer wipe needed to test + // tracer-level config (third `agent.load` arg) does not strand sibling + // describe blocks in the next pg-version iteration. + describe('with DBM propagation enabled with append comment using tracer configuration', () => { + before(async () => { + // Tracer-level config (third arg) only takes effect if the global + // tracer is wiped first; tracer.init() short-circuits once the + // process-wide singleton has been initialized by an earlier load. + agent.wipe() + await agent.load('pg', { + appendComment: true, + service: () => 'serviced', + }, { + dbmPropagationMode: 'service', + }) + pg = require('../../../versions/pg').get() + }) + + after(() => { + return agent.close({ ritmReset: false, wipe: true }) + }) + + beforeEach((done) => { + client = new pg.Client({ + host: '127.0.0.1', + user: 'postgres', + password: 'postgres', + database: 'postgres', + }) + client.connect(err => done(err)) + }) + + afterEach((done) => { + client.end(done) + }) + + it('should append service mode comment in query text', async () => { + const queryQueueName = Object.hasOwn(client, '_queryQueue') ? '_queryQueue' : 'queryQueue' + + const queryPromise = client.query('SELECT $1::text as message', ['Hello world!']) + + assert.strictEqual(client[queryQueueName][0].text, + 'SELECT $1::text as message /*dddb=\'postgres\',dddbs=\'serviced\',dde=\'tester\',' + + `ddh='127.0.0.1',ddps='test',ddpv='${ddpv}'*/` + ) + + await queryPromise + }) + }) }) }) diff --git a/packages/datadog-plugin-playwright/src/index.js b/packages/datadog-plugin-playwright/src/index.js index b684fd0070b..281e1b8283a 100644 --- a/packages/datadog-plugin-playwright/src/index.js +++ b/packages/datadog-plugin-playwright/src/index.js @@ -321,6 +321,7 @@ class PlaywrightPlugin extends CiPlugin { isAtrRetry, isModified, finalStatus, + earlyFlakeAbortReason, onDone, }) => { if (!span) return @@ -384,6 +385,9 @@ class PlaywrightPlugin extends CiPlugin { if (finalStatus) { span.setTag(TEST_FINAL_STATUS, finalStatus) } + if (earlyFlakeAbortReason) { + span.setTag(TEST_EARLY_FLAKE_ABORT_REASON, earlyFlakeAbortReason) + } for (const step of steps) { const stepStartTime = step.startTime.getTime() const stepSpan = this.tracer.startSpan('playwright.step', { diff --git a/packages/datadog-plugin-prisma/test/index.spec.js b/packages/datadog-plugin-prisma/test/index.spec.js index 1f3bd2d7c93..92121b20533 100644 --- a/packages/datadog-plugin-prisma/test/index.spec.js +++ b/packages/datadog-plugin-prisma/test/index.spec.js @@ -4,9 +4,11 @@ const assert = require('node:assert/strict') const { execSync } = require('node:child_process') const fs = require('node:fs/promises') const path = require('node:path') + const { after, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const semifies = require('semifies') + const { assertObjectContains } = require('../../../integration-tests/helpers') const { storage } = require('../../datadog-core') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') @@ -82,7 +84,7 @@ async function copySchemaToVersionDir (schemaPath, range) { } function createPrismaClient (prisma, config) { - // With the introduction of v7 prisma now enforces the use of adpaters + // With the introduction of v7 prisma now enforces the use of adapters if (config.v7) { const { PrismaPg } = require('@prisma/adapter-pg') const adapter = new PrismaPg({ connectionString: process.env[TEST_DATABASE_ENV_NAME] }) @@ -94,6 +96,21 @@ function createPrismaClient (prisma, config) { return new prisma.PrismaClient() } +function createEngineDbQuerySpan (queryText) { + return [{ + id: '1', + parentId: null, + name: 'prisma:engine:db_query', + startTime: [1745340876, 436861000], + endTime: [1745340876, 438601541], + kind: 'client', + attributes: { + 'db.system': 'postgresql', + 'db.query.text': queryText, + }, + }] +} + describe('Plugin', () => { let prisma let prismaClient @@ -495,24 +512,63 @@ describe('Plugin', () => { }) const engineSpans = [ - { - id: '1', - parentId: null, - name: 'prisma:engine:db_query', - startTime: [1745340876, 436861000], - endTime: [1745340876, 438601541], - kind: 'client', - attributes: { - 'db.system': 'postgresql', - 'db.query.text': 'SELECT 1', - }, - }, + ...createEngineDbQuerySpan('SELECT 1'), ] tracingHelper.dispatchEngineSpans(engineSpans) await Promise.all([ tracingPromise, ]) }) + + if (config.v7) { + it('should tag db_query spans with the active client adapter metadata in read-replica setups', async () => { + const initialDbUrl = process.env[TEST_DATABASE_ENV_NAME] + process.env[TEST_DATABASE_ENV_NAME] = + 'postgres://postgres:postgres@primary.db.internal:5432/postgres' + const primaryClient = createPrismaClient(prisma, config) + + process.env[TEST_DATABASE_ENV_NAME] = + 'postgres://postgres:postgres@replica.db.internal:5433/postgres' + const replicaClient = createPrismaClient(prisma, config) + + if (initialDbUrl === undefined) { + delete process.env[TEST_DATABASE_ENV_NAME] + } else { + process.env[TEST_DATABASE_ENV_NAME] = initialDbUrl + } + + assert.ok(primaryClient._tracingHelper) + assert.ok(replicaClient._tracingHelper) + + const replicaReadTrace = agent.assertSomeTraces(traces => { + const dbQuerySpan = traces[0].find(span => span.meta['prisma.name'] === 'db_query') + assertObjectContains(dbQuerySpan, { + resource: 'SELECT 1', + meta: { + 'out.host': 'replica.db.internal', + 'network.destination.port': '5433', + }, + }) + }) + replicaClient._tracingHelper.dispatchEngineSpans(createEngineDbQuerySpan('SELECT 1')) + await replicaReadTrace + + const primaryWriteTrace = agent.assertSomeTraces(traces => { + const dbQuerySpan = traces[0].find(span => span.meta['prisma.name'] === 'db_query') + assertObjectContains(dbQuerySpan, { + resource: 'INSERT INTO "User" ("name") VALUES ($1)', + meta: { + 'out.host': 'primary.db.internal', + 'network.destination.port': '5432', + }, + }) + }) + primaryClient._tracingHelper.dispatchEngineSpans(createEngineDbQuerySpan( + 'INSERT INTO "User" ("name") VALUES ($1)' + )) + await primaryWriteTrace + }) + } }) describe('without tracer initialization', () => { diff --git a/packages/datadog-plugin-prisma/test/integration-test/client.spec.js b/packages/datadog-plugin-prisma/test/integration-test/client.spec.js index 0ba0625656c..2e15d955f86 100644 --- a/packages/datadog-plugin-prisma/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-prisma/test/integration-test/client.spec.js @@ -201,6 +201,41 @@ const prismaClientConfigs = [{ 'db.type': 'mssql', }, }, +}, +{ + name: 'prisma-generator v7 pg adapter with OTel TracerProvider registration', + serverFile: 'server-ts-v7-otel.mjs', + schema: `./packages/datadog-plugin-prisma/test/${SCHEMA_FIXTURES.tsEsmV7}`, + configFile: `./packages/datadog-plugin-prisma/test/${SCHEMA_FIXTURES.tsEsmV7Config}`, + env: { + PRISMA_CLIENT_OUTPUT: './generated/prisma', + DATABASE_URL: TEST_DATABASE_URL, + }, + ts: true, + skipMigrateReset: true, + customTest: { + title: 'uses active OTel span IDs in Prisma DBM traceparent comments', + async run ({ agent, config }) { + let stdout = '' + const proc = await spawnPluginIntegrationTestProcAndExpectExit( + sandboxCwd(), + config.serverFile, + agent.port, + { DD_TRACE_FLUSH_INTERVAL: '2000', ...config.env }, + undefined, + data => { + stdout += data.toString() + } + ) + + const marker = stdout.match(/TRACEPARENT_OK:(00-[a-f0-9]{32}-[a-f0-9]{16}-01)/) + assert.ok(marker, `Expected TRACEPARENT_OK output marker, got: ${stdout}`) + assert.notStrictEqual(marker[1], '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01') + assert.notStrictEqual(marker[1], '00-00000000000000000000000000000000-0000000000000000-01') + + return proc + }, + }, }] describe('esm', () => { @@ -238,13 +273,15 @@ describe('esm', () => { useSandbox(deps, false, paths) - before(function () { - variants = varySandbox(config.serverFile, config.ts ? 'PrismaClient' : 'prismaLib', - config.ts ? 'PrismaClient' : undefined, config.importPath, config.ts) - if (!variants[config.variant]) { - throw new Error(`Unknown variant ${config.variant} for ${config.name}`) - } - }) + if (!config.customTest) { + before(function () { + variants = varySandbox(config.serverFile, config.ts ? 'PrismaClient' : 'prismaLib', + config.ts ? 'PrismaClient' : undefined, config.importPath, config.ts) + if (!variants[config.variant]) { + throw new Error(`Unknown variant ${config.variant} for ${config.name}`) + } + }) + } beforeEach(async function () { this.timeout(60000) @@ -303,41 +340,48 @@ describe('esm', () => { await agent?.stop() }) - const variant = config.variant - it(`is instrumented with ${variant} import`, async function () { - this.timeout(60000) - const dbSpanExpectation = config.dbSpan || { - name: config.configFile ? 'pg.query' : 'prisma.engine', - service: config.configFile ? 'node-postgres' : 'node-prisma', - meta: { - 'db.user': 'postgres', - 'db.name': 'postgres', - 'db.type': 'postgres', - }, - } - const res = agent.assertMessageReceived(({ headers, payload }) => { - assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) - assertObjectContains(payload, [[{ - name: 'prisma.client', - resource: 'User.create', - service: 'node-prisma', - }], [dbSpanExpectation]]) + if (config.customTest) { + it(config.customTest.title, async function () { + this.timeout(60000) + proc = await config.customTest.run({ agent, config }) }) + } else { + const variant = config.variant + it(`is instrumented with ${variant} import`, async function () { + this.timeout(60000) + const dbSpanExpectation = config.dbSpan || { + name: config.configFile ? 'pg.query' : 'prisma.engine', + service: config.configFile ? 'node-postgres' : 'node-prisma', + meta: { + 'db.user': 'postgres', + 'db.name': 'postgres', + 'db.type': 'postgres', + }, + } + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assertObjectContains(payload, [[{ + name: 'prisma.client', + resource: 'User.create', + service: 'node-prisma', + }], [dbSpanExpectation]]) + }) - const procPromise = spawnPluginIntegrationTestProcAndExpectExit( - sandboxCwd(), - variants[variant], - agent.port, - { DD_TRACE_FLUSH_INTERVAL: '2000', ...config.env } - ) + const procPromise = spawnPluginIntegrationTestProcAndExpectExit( + sandboxCwd(), + variants[variant], + agent.port, + { DD_TRACE_FLUSH_INTERVAL: '2000', ...config.env } + ) - await Promise.all([ - procPromise.then((res) => { - proc = res - }), - res, - ]) - }) + await Promise.all([ + procPromise.then((res) => { + proc = res + }), + res, + ]) + }) + } }) }) }) diff --git a/packages/datadog-plugin-prisma/test/integration-test/server-ts-v7-otel.mjs b/packages/datadog-plugin-prisma/test/integration-test/server-ts-v7-otel.mjs new file mode 100644 index 00000000000..138697741a2 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/integration-test/server-ts-v7-otel.mjs @@ -0,0 +1,76 @@ +import ddTrace from 'dd-trace' +import assert from 'node:assert/strict' + +import { trace } from '@opentelemetry/api' +// @ts-expect-error +import { PrismaPg } from '@prisma/adapter-pg' +// @ts-expect-error +import { PrismaClient } from './dist/client.js' + +const placeholderTraceparent = '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01' +const zeroTraceparent = '00-00000000000000000000000000000000-0000000000000000-01' + +ddTrace.init({ + dbmPropagationMode: 'full', + plugins: false, +}) +ddTrace.use('prisma', true) + +const provider = new ddTrace.TracerProvider() +provider.register() + +let observedTraceparent + +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }) +const prismaClient = new PrismaClient({ adapter }) +const otelTracer = trace.getTracer('prisma-otel-dbm-integration') +const tracingHelper = prismaClient._tracingHelper || prismaClient._engine?.tracingHelper + +assert.ok(tracingHelper && typeof tracingHelper.getTraceParent === 'function', 'Expected Prisma tracing helper') + +const originalGetTraceParent = tracingHelper.getTraceParent.bind(tracingHelper) +tracingHelper.getTraceParent = function wrappedGetTraceParent (...args) { + observedTraceparent = originalGetTraceParent(...args) + const activeSpan = ddTrace.scope().active() + if (activeSpan) { + const activeContext = activeSpan.context() + const expectedFromActiveSpan = `00-${activeContext.toTraceId(true)}-${activeContext.toSpanId(true)}-01` + assert.strictEqual(observedTraceparent, expectedFromActiveSpan) + } + return observedTraceparent +} + +await new Promise((resolve, reject) => { + otelTracer.startActiveSpan('otel-parent', async (span) => { + try { + const spanContext = span.spanContext() + const unique = `${Date.now()}-${process.pid}` + + await prismaClient.user.create({ + data: { + name: 'John Doe', + email: `john.doe+${unique}@datadoghq.com`, + }, + }) + + await prismaClient.user.findUnique({ + where: { + email: `john.doe+${unique}@datadoghq.com`, + }, + }) + + assert.ok(observedTraceparent, 'Expected Prisma query to include traceparent comment') + assert.ok(observedTraceparent.startsWith(`00-${spanContext.traceId}-`)) + assert.notStrictEqual(observedTraceparent, placeholderTraceparent) + assert.notStrictEqual(observedTraceparent, zeroTraceparent) + process.stdout.write(`TRACEPARENT_OK:${observedTraceparent}\n`) + resolve(undefined) + } catch (error) { + reject(error) + } finally { + span.end() + } + }) +}) + +await prismaClient.$disconnect() diff --git a/packages/datadog-plugin-redis/src/index.js b/packages/datadog-plugin-redis/src/index.js index e1738c55ab6..d15166785b1 100644 --- a/packages/datadog-plugin-redis/src/index.js +++ b/packages/datadog-plugin-redis/src/index.js @@ -4,6 +4,9 @@ const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants') const CachePlugin = require('../../dd-trace/src/plugins/cache') const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') +const MAX_ARG_LENGTH = 100 +const MAX_COMMAND_LENGTH = 1000 + class RedisPlugin extends CachePlugin { static id = 'redis' static system = 'redis' @@ -14,7 +17,7 @@ class RedisPlugin extends CachePlugin { } bindStart (ctx) { - const { db, command, args, connectionOptions, connectionName } = ctx + const { db, command, args, argsStartIndex, connectionOptions, connectionName } = ctx const resource = command const normalizedCommand = command.toUpperCase() @@ -29,7 +32,7 @@ class RedisPlugin extends CachePlugin { meta: { 'db.type': this._spanType, 'db.name': db || '0', - [`${this._spanType}.raw_command`]: formatCommand(normalizedCommand, args), + [`${this._spanType}.raw_command`]: formatCommand(normalizedCommand, args, argsStartIndex), 'out.host': connectionOptions.host, [CLIENT_PORT_KEY]: connectionOptions.port, }, @@ -43,36 +46,28 @@ class RedisPlugin extends CachePlugin { } } -function formatCommand (command, args) { +function formatCommand (command, args, argsStartIndex = 0) { if (!args || command === 'AUTH') return command - for (let i = 0, l = args.length; i < l; i++) { - if (typeof args[i] === 'function') continue - - command = `${command} ${formatArg(args[i])}` + let result = command + for (let i = argsStartIndex, l = args.length; i < l; i++) { + const arg = args[i] + if (typeof arg === 'function') continue - if (command.length > 1000) return trim(command, 1000) + result = `${result} ${formatArg(arg)}` + if (result.length > MAX_COMMAND_LENGTH) return result.slice(0, MAX_COMMAND_LENGTH - 3) + '...' } - return command + return result } function formatArg (arg) { - switch (typeof arg) { - case 'string': - case 'number': - return trim(String(arg), 100) - default: - return '?' - } -} - -function trim (str, maxlen) { - if (str.length > maxlen) { - str = str.slice(0, maxlen - 3) + '...' + if (typeof arg === 'string') { + return arg.length > MAX_ARG_LENGTH ? arg.slice(0, MAX_ARG_LENGTH - 3) + '...' : arg } - - return str + // Number stringification is bounded (~23 chars max), so it never hits MAX_ARG_LENGTH. + if (typeof arg === 'number') return String(arg) + return '?' } function normalizeConfig (config) { diff --git a/packages/datadog-plugin-redis/test/client.spec.js b/packages/datadog-plugin-redis/test/client.spec.js index f3ce6592074..472dac3d159 100644 --- a/packages/datadog-plugin-redis/test/client.spec.js +++ b/packages/datadog-plugin-redis/test/client.spec.js @@ -90,6 +90,51 @@ describe('Plugin', () => { await Promise.all([client.get('foo'), promise]) }) + it('keeps every arg when formatting a multi-arg command', async () => { + const promise = agent.assertSomeTraces(traces => { + assert.strictEqual(traces[0][0].meta['redis.raw_command'], 'SET multi-arg-key multi-arg-value') + }, { spanResourceMatch: /^SET$/ }) + + await Promise.all([client.set('multi-arg-key', 'multi-arg-value'), promise]) + }) + + it('trims a string arg longer than 100 chars', async () => { + const longValue = 'x'.repeat(150) + const promise = agent.assertSomeTraces(traces => { + const rawCommand = traces[0][0].meta['redis.raw_command'] + assert.strictEqual(rawCommand, `SET long-key ${'x'.repeat(97)}...`) + assert.strictEqual(rawCommand.length, 'SET long-key '.length + 100) + }, { spanResourceMatch: /^SET$/ }) + + await Promise.all([client.set('long-key', longValue), promise]) + }) + + it('redacts the AUTH password from the raw command', async () => { + const promise = agent.assertSomeTraces(traces => { + assert.strictEqual(traces[0][0].meta['redis.raw_command'], 'AUTH') + }, { spanResourceMatch: /^AUTH$/ }) + + await Promise.all([ + client.sendCommand(['AUTH', 'super-secret-password']).catch(() => {}), + promise, + ]) + }) + + it('caps the joined raw command at 1000 chars across many args', async () => { + const args = [] + for (let index = 0; index < 200; index++) { + args.push(`key${index}`, `value${index}`) + } + const promise = agent.assertSomeTraces(traces => { + const rawCommand = traces[0][0].meta['redis.raw_command'] + assert.strictEqual(rawCommand.length, 1000) + assert.ok(rawCommand.startsWith('MSET ')) + assert.ok(rawCommand.endsWith('...')) + }, { spanResourceMatch: /^MSET$/ }) + + await Promise.all([client.sendCommand(['MSET', ...args]), promise]) + }) + withPeerService( () => tracer, 'redis', diff --git a/packages/datadog-plugin-ws/test/index.spec.js b/packages/datadog-plugin-ws/test/index.spec.js index 5612c69005b..34b0f745f66 100644 --- a/packages/datadog-plugin-ws/test/index.spec.js +++ b/packages/datadog-plugin-ws/test/index.spec.js @@ -4,7 +4,8 @@ const assert = require('node:assert') const { once } = require('node:events') const dc = require('dc-polyfill') -const { after, afterEach, before, beforeEach, describe, it } = require('mocha') +const setSocketCh = dc.channel('tracing:ws:server:connect:setSocket') +const { afterEach, beforeEach, describe, it } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') const { storage } = require('../../datadog-core') @@ -26,6 +27,7 @@ function findSpan (traces, predicate) { } function closeWsServer (server) { + if (!server) return for (const ws of server.clients) { ws.terminate() } @@ -36,7 +38,7 @@ describe('Plugin', () => { let WebSocket let wsServer let connectionReceived - let clientPort = 6015 + let clientPort let client let messageReceived let route @@ -44,7 +46,10 @@ describe('Plugin', () => { describe('ws', () => { withVersions('ws', 'ws', '>=8.0.0', version => { describe('regression tests', () => { - before(async () => { + let regressionServer + let regressionSocket + + beforeEach(async () => { await agent.load(['ws'], [{ service: 'some', traceWebsocketMessagesEnabled: true, @@ -52,42 +57,53 @@ describe('Plugin', () => { WebSocket = require(`../../../versions/ws@${version}`).get() }) + afterEach(() => { + regressionSocket?.terminate() + regressionSocket = undefined + }) + + afterEach(async () => { + await closeWsServer(regressionServer) + }) + + afterEach(() => { + regressionServer = undefined + }) + + afterEach(async () => { + await agent.close({ ritmReset: false, wipe: true }) + }) + it('should not crash when sending on a socket without spanContext', async () => { - const server = new WebSocket.Server({ port: 16015 }) - const connectionPromise = once(server, 'connection') + regressionServer = new WebSocket.Server({ port: 0 }) + await once(regressionServer, 'listening') + const { port } = regressionServer.address() + const connectionPromise = once(regressionServer, 'connection') - const socket = new WebSocket('ws://localhost:16015') + regressionSocket = new WebSocket(`ws://localhost:${port}`) const [serverSocket] = await connectionPromise - await once(socket, 'open') + await once(regressionSocket, 'open') - assert.strictEqual(socket.spanContext, undefined) + assert.strictEqual(regressionSocket.spanContext, undefined) const messagePromise = once(serverSocket, 'message') await new Promise((resolve, reject) => { - socket.send('test message', {}, (err) => err ? reject(err) : resolve()) + regressionSocket.send('test message', {}, (err) => err ? reject(err) : resolve()) }) await messagePromise - - socket.close() - await once(socket, 'close') - server.close() }) it('should emit original error in case close is called before connection is established', async () => { - const socket = new WebSocket('wss://localhost:12345') + regressionSocket = new WebSocket('wss://localhost:12345') - const errorPromise = once(socket, 'error') - socket.close() + const errorPromise = once(regressionSocket, 'error') + regressionSocket.close() const error = await errorPromise // Some versions emit an array with an error, some directly emit the error assert.strictEqual(error?.[0]?.message, 'WebSocket was closed before the connection was established') }) - - after(async () => { - await agent.close({ ritmReset: false, wipe: true }) - }) }) describe('when using WebSocket', () => { @@ -106,22 +122,27 @@ describe('Plugin', () => { }]) WebSocket = require(`../../../versions/ws@${version}`).get() - wsServer = new WebSocket.Server({ port: clientPort }) + wsServer = new WebSocket.Server({ port: 0 }) await once(wsServer, 'listening') + clientPort = wsServer.address().port }) - afterEach(async () => { - clientPort++ + afterEach(() => { if (client) { client.removeAllListeners('error') client.on('error', () => {}) } + }) + + afterEach(async () => { await closeWsServer(wsServer) + }) + + afterEach(async () => { await agent.close({ ritmReset: false, wipe: true }) }) it('should not retain the connection span during socket setup', async () => { - const setSocketCh = dc.channel('tracing:ws:server:connect:setSocket') let resolve const promise = new Promise((_resolve) => { resolve = _resolve @@ -464,17 +485,23 @@ describe('Plugin', () => { }]) WebSocket = require(`../../../versions/ws@${version}`).get() - wsServer = new WebSocket.Server({ port: clientPort }) + wsServer = new WebSocket.Server({ port: 0 }) await once(wsServer, 'listening') + clientPort = wsServer.address().port }) - afterEach(async () => { - clientPort++ + afterEach(() => { if (client) { client.removeAllListeners('error') client.on('error', () => {}) } + }) + + afterEach(async () => { await closeWsServer(wsServer) + }) + + afterEach(async () => { await agent.close({ ritmReset: false, wipe: true }) }) @@ -576,17 +603,22 @@ describe('Plugin', () => { }]) WebSocket = require(`../../../versions/ws@${version}`).get() - wsServer = new WebSocket.Server({ port: clientPort }) + wsServer = new WebSocket.Server({ port: 0 }) await once(wsServer, 'listening') }) - afterEach(async () => { - clientPort++ + afterEach(() => { if (client) { client.removeAllListeners('error') client.on('error', () => {}) } + }) + + afterEach(async () => { await closeWsServer(wsServer) + }) + + afterEach(async () => { await agent.close({ ritmReset: false, wipe: true }) }) @@ -625,17 +657,23 @@ describe('Plugin', () => { }]) WebSocket = require(`../../../versions/ws@${version}`).get() - wsServer = new WebSocket.Server({ port: clientPort }) + wsServer = new WebSocket.Server({ port: 0 }) await once(wsServer, 'listening') + clientPort = wsServer.address().port }) - afterEach(async () => { - clientPort++ + afterEach(() => { if (client) { client.removeAllListeners('error') client.on('error', () => {}) } + }) + + afterEach(async () => { await closeWsServer(wsServer) + }) + + afterEach(async () => { await agent.close({ ritmReset: false, wipe: true }) }) @@ -721,8 +759,9 @@ describe('Plugin', () => { }]) WebSocket = require(`../../../versions/ws@${version}`).get() - wsServer = new WebSocket.Server({ port: clientPort }) + wsServer = new WebSocket.Server({ port: 0 }) await once(wsServer, 'listening') + clientPort = wsServer.address().port parentHeaders = {} tracer.trace('test.parent', parentSpan => { @@ -730,13 +769,18 @@ describe('Plugin', () => { }) }) - afterEach(async () => { - clientPort++ + afterEach(() => { if (client) { client.removeAllListeners('error') client.on('error', () => {}) } + }) + + afterEach(async () => { await closeWsServer(wsServer) + }) + + afterEach(async () => { await agent.close({ ritmReset: false, wipe: true }) }) diff --git a/packages/dd-trace/src/aiguard/index.js b/packages/dd-trace/src/aiguard/index.js index dd26baa6bd1..3d92dc9c200 100644 --- a/packages/dd-trace/src/aiguard/index.js +++ b/packages/dd-trace/src/aiguard/index.js @@ -3,6 +3,7 @@ const log = require('../log') const { incomingHttpRequestStart, aiguardChannel } = require('./channels') const AIGuard = require('./sdk') +const { SOURCE_AUTO, INTEGRATION_NONE } = require('./tags') let isEnabled = false let aiguard @@ -51,7 +52,8 @@ function onEvaluate (ctx) { return } - aiguard.evaluate(ctx.messages, { block }) + const opts = { block, source: SOURCE_AUTO, integration: ctx.integration || INTEGRATION_NONE } + aiguard.evaluate(ctx.messages, opts) .then(() => { ctx.resolve() }) diff --git a/packages/dd-trace/src/aiguard/sdk.js b/packages/dd-trace/src/aiguard/sdk.js index 74a3c9b04e4..f187944a870 100644 --- a/packages/dd-trace/src/aiguard/sdk.js +++ b/packages/dd-trace/src/aiguard/sdk.js @@ -11,23 +11,23 @@ const { keepTrace } = require('../priority_sampler') const { AI_GUARD } = require('../standalone/product') const NoopAIGuard = require('./noop') const executeRequest = require('./client') -const { - AI_GUARD_RESOURCE, - AI_GUARD_TARGET_TAG_KEY, - AI_GUARD_REASON_TAG_KEY, - AI_GUARD_ACTION_TAG_KEY, - AI_GUARD_EVENT_TAG_KEY, - AI_GUARD_BLOCKED_TAG_KEY, - AI_GUARD_META_STRUCT_KEY, - AI_GUARD_TOOL_NAME_TAG_KEY, - AI_GUARD_TELEMETRY_REQUESTS, - AI_GUARD_TELEMETRY_TRUNCATED, -} = require('./tags') +const TAGS = require('./tags') -const appsecMetrics = telemetryMetrics.manager.namespace('appsec') +const aiguardMetrics = telemetryMetrics.manager.namespace('ai_guard') const ALLOW = 'ALLOW' +/** + * Reports a telemetry error + * + * @param {string} errorType - The error type constant (client_error, bad_status, bad_response) + * @param {{ source: string, integration: string }} telemetryTags - Source and integration tags + */ +function reportTelemetryError (errorType, telemetryTags) { + aiguardMetrics.count(TAGS.TELEMETRY_REQUESTS, { error: true, ...telemetryTags }).inc(1) + aiguardMetrics.count(TAGS.TELEMETRY_ERROR, { type: errorType, ...telemetryTags }).inc(1) +} + class AIGuardAbortError extends Error { constructor (reason, tags, tagProbs, sds) { super(reason) @@ -99,10 +99,10 @@ class AIGuard extends NoopAIGuard { * - Clones each message so callers cannot mutate the data set in the meta struct. * - Truncates the list of messages and `content` fields emitting metrics accordingly. */ - #buildMessagesForMetaStruct (messages) { + #buildMessagesForMetaStruct (messages, telemetryTags) { const size = Math.min(messages.length, this.#maxMessagesLength) if (messages.length > size) { - appsecMetrics.count(AI_GUARD_TELEMETRY_TRUNCATED, { type: 'messages' }).inc(1) + aiguardMetrics.count(TAGS.TELEMETRY_TRUNCATED, { type: 'messages', ...telemetryTags }).inc(1) } const result = [] let contentTruncated = false @@ -115,7 +115,7 @@ class AIGuard extends NoopAIGuard { result.push(message) } if (contentTruncated) { - appsecMetrics.count(AI_GUARD_TELEMETRY_TRUNCATED, { type: 'content' }).inc(1) + aiguardMetrics.count(TAGS.TELEMETRY_TRUNCATED, { type: 'content', ...telemetryTags }).inc(1) } return result } @@ -185,22 +185,23 @@ class AIGuard extends NoopAIGuard { if (!this.#initialized) { return super.evaluate(messages, opts) } - const { block = true } = opts ?? {} - return this.#tracer.trace(AI_GUARD_RESOURCE, {}, async (span) => { + const { block = true, source = TAGS.SOURCE_SDK, integration = TAGS.INTEGRATION_NONE } = opts ?? {} + const telemetryTags = { source, integration } + return this.#tracer.trace(TAGS.RESOURCE, {}, async (span) => { const last = messages[messages.length - 1] const target = this.#isToolCall(last) ? 'tool' : 'prompt' - span.setTag(AI_GUARD_TARGET_TAG_KEY, target) + span.setTag(TAGS.TARGET_TAG_KEY, target) if (target === 'tool') { const name = this.#getToolName(last, messages) if (name) { - span.setTag(AI_GUARD_TOOL_NAME_TAG_KEY, name) + span.setTag(TAGS.TOOL_NAME_TAG_KEY, name) } } const metaStruct = { - messages: this.#buildMessagesForMetaStruct(messages), + messages: this.#buildMessagesForMetaStruct(messages, telemetryTags), } span.meta_struct = { - [AI_GUARD_META_STRUCT_KEY]: metaStruct, + [TAGS.META_STRUCT_KEY]: metaStruct, } const rootSpan = span.context()?._trace?.started?.[0] if (rootSpan) { @@ -208,7 +209,7 @@ class AIGuard extends NoopAIGuard { // keepTrace must be called before executeRequest so the sampling decision // is propagated correctly to outgoing HTTP client calls. keepTrace(rootSpan, AI_GUARD) - rootSpan.setTag(AI_GUARD_EVENT_TAG_KEY, 'true') + rootSpan.setTag(TAGS.EVENT_TAG_KEY, 'true') } let response try { @@ -224,18 +225,18 @@ class AIGuard extends NoopAIGuard { payload, { url: this.#evaluateUrl, headers: this.#headers, timeout: this.#timeout }) } catch (e) { - appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1) + reportTelemetryError(TAGS.ERROR_TYPE_CLIENT, telemetryTags) throw new AIGuardClientError(`Unexpected error calling AI Guard service: ${e.message}`, { cause: e }) } if (response.status !== 200) { - appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1) + reportTelemetryError(TAGS.ERROR_TYPE_STATUS, telemetryTags) throw new AIGuardClientError( `AI Guard service call failed, status ${response.status}`, { errors: response.body?.errors }) } const attr = response.body?.data?.attributes if (!attr?.action) { - appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1) + reportTelemetryError(TAGS.ERROR_TYPE_RESPONSE, telemetryTags) throw new AIGuardClientError(`AI Guard service returned unexpected response : ${response.body}`) } const action = attr.action @@ -254,13 +255,18 @@ class AIGuard extends NoopAIGuard { } const blockingEnabled = attr.is_blocking_enabled ?? false const shouldBlock = block && blockingEnabled && action !== ALLOW - appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { action, error: false, block: shouldBlock }).inc(1) - span.setTag(AI_GUARD_ACTION_TAG_KEY, action) + aiguardMetrics.count(TAGS.TELEMETRY_REQUESTS, { + action, + error: false, + block: shouldBlock, + ...telemetryTags, + }).inc(1) + span.setTag(TAGS.ACTION_TAG_KEY, action) if (reason) { - span.setTag(AI_GUARD_REASON_TAG_KEY, reason) + span.setTag(TAGS.REASON_TAG_KEY, reason) } if (shouldBlock) { - span.setTag(AI_GUARD_BLOCKED_TAG_KEY, 'true') + span.setTag(TAGS.BLOCKED_TAG_KEY, 'true') throw new AIGuardAbortError(reason, tags, tagProbabilities, sdsFindings) } return { action, reason, tags, tagProbabilities, sds: sdsFindings } diff --git a/packages/dd-trace/src/aiguard/tags.js b/packages/dd-trace/src/aiguard/tags.js index 9192b0b2a8e..00f50411cf2 100644 --- a/packages/dd-trace/src/aiguard/tags.js +++ b/packages/dd-trace/src/aiguard/tags.js @@ -1,15 +1,24 @@ 'use strict' module.exports = { - AI_GUARD_RESOURCE: 'ai_guard', - AI_GUARD_TARGET_TAG_KEY: 'ai_guard.target', - AI_GUARD_TOOL_NAME_TAG_KEY: 'ai_guard.tool_name', - AI_GUARD_ACTION_TAG_KEY: 'ai_guard.action', - AI_GUARD_REASON_TAG_KEY: 'ai_guard.reason', - AI_GUARD_BLOCKED_TAG_KEY: 'ai_guard.blocked', - AI_GUARD_EVENT_TAG_KEY: 'ai_guard.event', - AI_GUARD_META_STRUCT_KEY: 'ai_guard', - - AI_GUARD_TELEMETRY_REQUESTS: 'ai_guard.requests', - AI_GUARD_TELEMETRY_TRUNCATED: 'ai_guard.truncated', + RESOURCE: 'ai_guard', + TARGET_TAG_KEY: 'ai_guard.target', + TOOL_NAME_TAG_KEY: 'ai_guard.tool_name', + ACTION_TAG_KEY: 'ai_guard.action', + REASON_TAG_KEY: 'ai_guard.reason', + BLOCKED_TAG_KEY: 'ai_guard.blocked', + EVENT_TAG_KEY: 'ai_guard.event', + META_STRUCT_KEY: 'ai_guard', + + TELEMETRY_REQUESTS: 'requests', + TELEMETRY_TRUNCATED: 'truncated', + TELEMETRY_ERROR: 'error', + + SOURCE_SDK: 'sdk', + SOURCE_AUTO: 'auto', + INTEGRATION_NONE: 'none', + + ERROR_TYPE_CLIENT: 'client_error', + ERROR_TYPE_STATUS: 'bad_status', + ERROR_TYPE_RESPONSE: 'bad_response', } diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js index 5f47af08550..978423fb1ba 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js @@ -208,8 +208,8 @@ let enableEsmRewriter = function (telemetryVerbosity) { } }) - port1.unref() - port2.unref() + port1.unref?.() + port2.unref?.() try { Module.register('./rewriter-esm.mjs', { diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index 9edf2d62016..34de46a5a81 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -100,7 +100,7 @@ function clearCache () { // only for test purposes function startClearCacheTimer () { resetVulnerabilityCacheTimer = setInterval(clearCache, RESET_VULNERABILITY_CACHE_INTERVAL) - resetVulnerabilityCacheTimer.unref() + resetVulnerabilityCacheTimer.unref?.() } function stopClearCacheTimer () { diff --git a/packages/dd-trace/src/azure_metadata.js b/packages/dd-trace/src/azure_metadata.js index 1bf59eb03cd..d0d5d53904d 100644 --- a/packages/dd-trace/src/azure_metadata.js +++ b/packages/dd-trace/src/azure_metadata.js @@ -3,10 +3,8 @@ // Modeled after https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/ddcommon/src/azure_app_services.rs const os = require('os') -const { - getEnvironmentVariable, - getValueFromEnvSources, -} = require('./config/helper') +const getConfig = require('./config') +const { getEnvironmentVariable } = require('./config/helper') const { getIsAzureFunction } = require('./serverless') function extractSubscriptionID (ownerName) { @@ -19,7 +17,20 @@ function extractSubscriptionID (ownerName) { } function extractResourceGroup (ownerName) { - return /.+\+(.+)-.+webspace(-Linux)?/.exec(ownerName)?.[1] + // WEBSITE_OWNER_NAME format: `+-webspace[-Linux]`. Region + // names have no `-`; resource groups can. Plain string ops read more directly + // than `/.+\+(.+)-.+webspace(-Linux)?/` and avoid the engine backtracking + // through three `.+` quantifiers to land on the right `-`. + if (typeof ownerName !== 'string') return + const plusIdx = ownerName.indexOf('+') + if (plusIdx === -1) return + let rest = ownerName.slice(plusIdx + 1) + if (rest.endsWith('-Linux')) rest = rest.slice(0, -'-Linux'.length) + if (!rest.endsWith('webspace')) return + rest = rest.slice(0, -'webspace'.length) + const lastDash = rest.lastIndexOf('-') + if (lastDash === -1) return + return rest.slice(0, lastDash) } function buildResourceID (subscriptionID, siteName, resourceGroup) { @@ -56,7 +67,7 @@ function buildMetadata () { const WEBSITE_SITE_NAME = getEnvironmentVariable('WEBSITE_SITE_NAME') const WEBSITE_SKU = getEnvironmentVariable('WEBSITE_SKU') - const DD_AZURE_RESOURCE_GROUP = getValueFromEnvSources('DD_AZURE_RESOURCE_GROUP') + const { DD_AZURE_RESOURCE_GROUP } = getConfig() const isAzureFunction = FUNCTIONS_EXTENSION_VERSION !== undefined && FUNCTIONS_WORKER_RUNTIME !== undefined const isFlexConsumptionAzureFunction = isAzureFunction && WEBSITE_SKU === 'FlexConsumption' diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index 5df5b9c8b59..581f1bfe382 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -120,7 +120,7 @@ class TestVisDynamicInstrumentation { }) // Allow the parent to exit even if the worker is still running - this.worker.unref() + this.worker.unref?.() this.breakpointSetChannel.port2.on('message', (probeId) => { const resolve = probeIdToResolveBreakpointSet.get(probeId) @@ -128,7 +128,7 @@ class TestVisDynamicInstrumentation { resolve() probeIdToResolveBreakpointSet.delete(probeId) } - }).unref() + }).unref?.() this.breakpointHitChannel.port2.on('message', ({ snapshot }) => { const { probe: { id: probeId } } = snapshot @@ -138,7 +138,7 @@ class TestVisDynamicInstrumentation { } else { log.warn('Received a breakpoint hit for an unknown probe') } - }).unref() + }).unref?.() this.breakpointRemoveChannel.port2.on('message', (probeId) => { const resolve = probeIdToResolveBreakpointRemove.get(probeId) @@ -146,7 +146,7 @@ class TestVisDynamicInstrumentation { resolve() probeIdToResolveBreakpointRemove.delete(probeId) } - }).unref() + }).unref?.() } } diff --git a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js index e3f0c69d612..e16273734e8 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +++ b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js @@ -48,11 +48,13 @@ class CiVisibilityExporter extends BufferingExporter { const gitUploadTimeoutId = setTimeout(() => { this._resolveGit(new Error('Timeout while uploading git metadata')) - }, GIT_UPLOAD_TIMEOUT).unref() + }, GIT_UPLOAD_TIMEOUT) + gitUploadTimeoutId.unref?.() const canUseCiVisProtocolTimeoutId = setTimeout(() => { this._resolveCanUseCiVisProtocol(false) - }, CAN_USE_CI_VIS_PROTOCOL_TIMEOUT).unref() + }, CAN_USE_CI_VIS_PROTOCOL_TIMEOUT) + canUseCiVisProtocolTimeoutId.unref?.() this._gitUploadPromise = new Promise(resolve => { this._resolveGit = (err) => { diff --git a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js index a58b07bb1d4..85be6fb8a93 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js @@ -11,10 +11,12 @@ const { VITEST_WORKER_TRACE_PAYLOAD_CODE, VITEST_WORKER_LOGS_PAYLOAD_CODE, } = require('../../../plugins/util/test') -const { getEnvironmentVariable, getValueFromEnvSources } = require('../../../config/helper') +const getConfig = require('../../../config') +const { getEnvironmentVariable } = require('../../../config/helper') const Writer = require('./writer') function getInterprocessTraceCode () { + const { DD_PLAYWRIGHT_WORKER, DD_VITEST_WORKER } = getConfig() if (getEnvironmentVariable('JEST_WORKER_ID')) { return JEST_WORKER_TRACE_PAYLOAD_CODE } @@ -24,13 +26,13 @@ function getInterprocessTraceCode () { if (getEnvironmentVariable('MOCHA_WORKER_ID')) { return MOCHA_WORKER_TRACE_PAYLOAD_CODE } - if (getValueFromEnvSources('DD_PLAYWRIGHT_WORKER')) { + if (DD_PLAYWRIGHT_WORKER) { return PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE } if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) { return VITEST_WORKER_TRACE_PAYLOAD_CODE } - if (getValueFromEnvSources('DD_VITEST_WORKER')) { + if (DD_VITEST_WORKER) { return VITEST_WORKER_TRACE_PAYLOAD_CODE } return null @@ -51,7 +53,7 @@ function getInterprocessLogsCode () { if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) { return VITEST_WORKER_LOGS_PAYLOAD_CODE } - if (getValueFromEnvSources('DD_VITEST_WORKER')) { + if (getConfig().DD_VITEST_WORKER) { return VITEST_WORKER_LOGS_PAYLOAD_CODE } return null diff --git a/packages/dd-trace/src/ci-visibility/requests/fs-cache.js b/packages/dd-trace/src/ci-visibility/requests/fs-cache.js index fc1e029dad2..d7b2c9d46ee 100644 --- a/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +++ b/packages/dd-trace/src/ci-visibility/requests/fs-cache.js @@ -149,7 +149,7 @@ function touchLock (cacheKey) { */ function startLockHeartbeat (cacheKey) { const interval = setInterval(() => touchLock(cacheKey), CACHE_LOCK_HEARTBEAT_MS) - interval.unref() + interval.unref?.() return () => { clearInterval(interval) try { fs.unlinkSync(getLockPath(cacheKey)) } catch { /* ignore */ } diff --git a/packages/dd-trace/src/config/defaults.js b/packages/dd-trace/src/config/defaults.js index 247fdbdfa2c..cfc117262e1 100644 --- a/packages/dd-trace/src/config/defaults.js +++ b/packages/dd-trace/src/config/defaults.js @@ -5,29 +5,18 @@ const util = require('util') const { DD_MAJOR } = require('../../../../version') const { parsers, transformers, telemetryTransformers, setWarnInvalidValue } = require('./parsers') +const applyMajorOverrides = require('./major-overrides') const { supportedConfigurations, } = /** @type {import('./helper').SupportedConfigurationsJson} */ (require('./supported-configurations.json')) +applyMajorOverrides(supportedConfigurations, DD_MAJOR) + let log let seqId = 0 const configWithOrigin = new Map() const parseErrors = new Map() -if (DD_MAJOR >= 6) { - // Programmatic configuration of DD_IAST_SECURITY_CONTROLS_CONFIGURATION is not supported - // in newer major versions. This is special handled here until a better solution is found. - // TODO: Remove the programmatic configuration from supported-configurations.json once v5 is not supported anymore. - supportedConfigurations.DD_IAST_SECURITY_CONTROLS_CONFIGURATION[0].internalPropertyName = - supportedConfigurations.DD_IAST_SECURITY_CONTROLS_CONFIGURATION[0].configurationNames?.[0] - delete supportedConfigurations.DD_IAST_SECURITY_CONTROLS_CONFIGURATION[0].configurationNames -} else { - // Default value for DD_TRACE_STARTUP_LOGS is 'false' in older major versions. - // This is special handled here until a better solution is found. - // TODO: Remove this here once v5 is not supported anymore. - supportedConfigurations.DD_TRACE_STARTUP_LOGS[0].default = 'false' -} - /** * Warns about an invalid value for an option and adds the error to the last telemetry entry if it is not already set. * Logging happens only if the error is not already set or the option name is different from the last telemetry entry. diff --git a/packages/dd-trace/src/config/generated-config-types.d.ts b/packages/dd-trace/src/config/generated-config-types.d.ts index 5baea844c6c..8e66d7d894c 100644 --- a/packages/dd-trace/src/config/generated-config-types.d.ts +++ b/packages/dd-trace/src/config/generated-config-types.d.ts @@ -85,11 +85,12 @@ export interface GeneratedConfig { DD_CIVISIBILITY_TEST_MODULE_ID: string | undefined; DD_CIVISIBILITY_TEST_SESSION_ID: string | undefined; DD_CRASHTRACKING_ENABLED: boolean; + DD_CUSTOM_PARENT_ID: string | undefined; DD_CUSTOM_TRACE_ID: string | undefined; DD_ENABLE_LAGE_PACKAGE_NAME: boolean; DD_ENABLE_NX_SERVICE_NAME: boolean; DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED: boolean; - DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR: string; + DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR: string | undefined; DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED: boolean; DD_EXPERIMENTAL_TEST_OPT_SETTINGS_CACHE: string; DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE: boolean; @@ -243,6 +244,7 @@ export interface GeneratedConfig { DD_TRACE_ELASTIC_ELASTICSEARCH_ENABLED: boolean; DD_TRACE_ELASTIC_TRANSPORT_ENABLED: boolean; DD_TRACE_ELASTICSEARCH_ENABLED: boolean; + DD_TRACE_ELECTRON_ENABLED: boolean; DD_TRACE_ENCODING_DEBUG: boolean; DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED: boolean; DD_TRACE_EXPERIMENTAL_SPAN_COUNTS: boolean; diff --git a/packages/dd-trace/src/config/helper.js b/packages/dd-trace/src/config/helper.js index 011fa0caa1a..f0646738491 100644 --- a/packages/dd-trace/src/config/helper.js +++ b/packages/dd-trace/src/config/helper.js @@ -21,10 +21,14 @@ */ const { deprecate } = require('util') +const { DD_MAJOR } = require('../../../../version') +const applyMajorOverrides = require('./major-overrides') const { supportedConfigurations, } = /** @type {SupportedConfigurationsJson} */ (require('./supported-configurations.json')) +applyMajorOverrides(supportedConfigurations, DD_MAJOR) + /** * Types for environment variable handling. * diff --git a/packages/dd-trace/src/config/index.js b/packages/dd-trace/src/config/index.js index 4c7f521145b..20226397e0b 100644 --- a/packages/dd-trace/src/config/index.js +++ b/packages/dd-trace/src/config/index.js @@ -165,7 +165,7 @@ class Config extends ConfigBase { } /** - * @param {TracerOptions} [options={}] + * @param {TracerOptions} [options] */ constructor (options = {}) { super() @@ -357,7 +357,7 @@ class Config extends ConfigBase { setAndTrack(this, 'DD_METRICS_OTEL_ENABLED', false) } - if (this.OTEL_TRACES_EXPORTER === 'otlp' && this.protocolVersion && this.protocolVersion !== '0.4') { + if (this.OTEL_TRACES_EXPORTER === 'otlp' && trackedConfigOrigins.has('protocolVersion')) { log.warn('DD_TRACE_AGENT_PROTOCOL_VERSION is set, disabling OTLP traces export') setAndTrack(this, 'OTEL_TRACES_EXPORTER', 'none') } diff --git a/packages/dd-trace/src/config/major-overrides.js b/packages/dd-trace/src/config/major-overrides.js new file mode 100644 index 00000000000..2d8c355c428 --- /dev/null +++ b/packages/dd-trace/src/config/major-overrides.js @@ -0,0 +1,98 @@ +'use strict' + +/** + * @typedef {import('./helper').SupportedConfigurationsJson['supportedConfigurations']} SupportedConfigurations + */ + +const EXPERIMENTAL_APPSEC_PREFIX = 'experimental.appsec' +const EXPERIMENTAL_IAST_PREFIX = 'experimental.iast' +const INGESTION_PREFIX = 'ingestion.' + +/** + * @param {SupportedConfigurations} supportedConfigurations Mutated in place. + * @param {number} majorVersion + */ +function applyMajorOverrides (supportedConfigurations, majorVersion) { + if (majorVersion < 6) { + applyV5Overrides(supportedConfigurations) + return + } + + for (const entries of Object.values(supportedConfigurations)) { + for (const entry of entries) { + if (Array.isArray(entry.configurationNames)) { + entry.configurationNames = entry.configurationNames.filter( + (name) => + name !== EXPERIMENTAL_APPSEC_PREFIX && + name !== EXPERIMENTAL_IAST_PREFIX && + !name.startsWith(`${EXPERIMENTAL_APPSEC_PREFIX}.`) && + !name.startsWith(`${EXPERIMENTAL_IAST_PREFIX}.`) && + !name.startsWith(INGESTION_PREFIX) + ) + if (entry.configurationNames.length === 0) delete entry.configurationNames + } + } + } + delete supportedConfigurations.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED + delete supportedConfigurations.DD_TRACE_EXPERIMENTAL_B3_ENABLED + + /* eslint-disable eslint-rules/eslint-env-aliases */ + for (const name of [ + 'DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED', + 'DD_PROFILING_EXPERIMENTAL_CPU_ENABLED', + 'DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED', + 'DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED', + ]) { + delete supportedConfigurations[name] + } + /* eslint-enable eslint-rules/eslint-env-aliases */ + for (const canonical of [ + 'DD_PROFILING_CODEHOTSPOTS_ENABLED', + 'DD_PROFILING_CPU_ENABLED', + 'DD_PROFILING_ENDPOINT_COLLECTION_ENABLED', + 'DD_PROFILING_TIMELINE_ENABLED', + ]) { + dropAlias(supportedConfigurations[canonical], (alias) => alias.startsWith('DD_PROFILING_EXPERIMENTAL_')) + } + + delete supportedConfigurations.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED + // eslint-disable-next-line eslint-rules/eslint-env-aliases + dropAlias(supportedConfigurations.DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED, 'DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED') +} + +/** + * @param {SupportedConfigurations} supportedConfigurations Mutated in place. + */ +function applyV5Overrides (supportedConfigurations) { + const startupLogsEntry = supportedConfigurations.DD_TRACE_STARTUP_LOGS?.[0] + if (startupLogsEntry) { + startupLogsEntry.default = 'false' + } + + const iastEntry = supportedConfigurations.DD_IAST_SECURITY_CONTROLS_CONFIGURATION?.[0] + if (!iastEntry) return + + iastEntry.configurationNames = [ + iastEntry.internalPropertyName || iastEntry.configurationNames?.[0], + `${EXPERIMENTAL_IAST_PREFIX}.securityControlsConfiguration`, + ] + delete iastEntry.internalPropertyName +} + +/** + * @param {import('./helper').SupportedConfigurationEntry[] | undefined} entries + * @param {string | ((alias: string) => boolean)} dropPredicate + */ +function dropAlias (entries, dropPredicate) { + const entry = entries?.[0] + if (entry?.aliases === undefined) return + const matches = typeof dropPredicate === 'string' + ? (alias) => alias === dropPredicate + : dropPredicate + entry.aliases = entry.aliases.filter((alias) => !matches(alias)) + if (entry.aliases.length === 0) { + delete entry.aliases + } +} + +module.exports = applyMajorOverrides diff --git a/packages/dd-trace/src/config/parsers.js b/packages/dd-trace/src/config/parsers.js index bfa89e115ac..91be2fca221 100644 --- a/packages/dd-trace/src/config/parsers.js +++ b/packages/dd-trace/src/config/parsers.js @@ -2,6 +2,7 @@ const fs = require('fs') +const { DD_MAJOR } = require('../../../../version') const tagger = require('../tagger') let warnInvalidValue @@ -9,6 +10,7 @@ function setWarnInvalidValue (fn) { warnInvalidValue = fn } +// `'b3 single header'` is the legacy spelling of `'b3'`; on v6 it is normalised below. const VALID_PROPAGATION_STYLES = new Set([ 'datadog', 'tracecontext', 'b3', 'b3 single header', 'b3multi', 'baggage', 'none', ]) @@ -145,11 +147,15 @@ const transformers = { }, validatePropagationStyles (value, optionName) { value = transformers.toLowerCase(value) - for (const propagator of value) { + for (let index = 0; index < value.length; index++) { + const propagator = value[index] if (!VALID_PROPAGATION_STYLES.has(propagator)) { warnInvalidValue(propagator, optionName, optionName, 'Invalid propagator') return } + if (DD_MAJOR >= 6 && propagator === 'b3 single header') { + value[index] = 'b3' + } } return value }, diff --git a/packages/dd-trace/src/config/supported-configurations.json b/packages/dd-trace/src/config/supported-configurations.json index 157bcd154ae..5fb68244d08 100644 --- a/packages/dd-trace/src/config/supported-configurations.json +++ b/packages/dd-trace/src/config/supported-configurations.json @@ -254,7 +254,8 @@ "appsec.extendedHeadersCollection.redaction", "experimental.appsec.extendedHeadersCollection.redaction" ], - "default": "true" + "default": "true", + "deprecated": true } ], "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML": [ @@ -289,7 +290,8 @@ "appsec.extendedHeadersCollection.maxHeaders", "experimental.appsec.extendedHeadersCollection.maxHeaders" ], - "default": "50" + "default": "50", + "deprecated": true } ], "DD_APPSEC_MAX_STACK_TRACES": [ @@ -597,6 +599,13 @@ "default": "true" } ], + "DD_CUSTOM_PARENT_ID": [ + { + "implementation": "A", + "type": "string", + "default": null + } + ], "DD_CUSTOM_TRACE_ID": [ { "implementation": "A", @@ -780,9 +789,9 @@ ], "DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR": [ { - "implementation": "A", + "implementation": "B", "type": "string", - "default": "${os.tmpdir()}/dd-trace-git-cache" + "default": null } ], "DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED": [ @@ -1062,10 +1071,7 @@ { "implementation": "B", "type": "string", - "configurationNames": [ - "iast.securityControlsConfiguration", - "experimental.iast.securityControlsConfiguration" - ], + "internalPropertyName": "iast.securityControlsConfiguration", "default": null } ], @@ -1934,9 +1940,9 @@ ], "DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_AWS_ENABLED": [ @@ -1948,16 +1954,16 @@ ], "DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_BEDROCKRUNTIME_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_BEDROCKRUNTIME_ENABLED": [ @@ -1969,9 +1975,9 @@ ], "DD_TRACE_AWS_SDK_CLOUDWATCHLOGS_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_CLOUDWATCHLOGS_ENABLED": [ @@ -1983,9 +1989,9 @@ ], "DD_TRACE_AWS_SDK_DYNAMODB_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_DYNAMODB_ENABLED": [ @@ -2004,9 +2010,9 @@ ], "DD_TRACE_AWS_SDK_EVENTBRIDGE_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_EVENTBRIDGE_ENABLED": [ @@ -2018,9 +2024,9 @@ ], "DD_TRACE_AWS_SDK_KINESIS_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_KINESIS_ENABLED": [ @@ -2032,9 +2038,9 @@ ], "DD_TRACE_AWS_SDK_LAMBDA_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_LAMBDA_ENABLED": [ @@ -2053,9 +2059,9 @@ ], "DD_TRACE_AWS_SDK_REDSHIFT_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_REDSHIFT_ENABLED": [ @@ -2067,9 +2073,9 @@ ], "DD_TRACE_AWS_SDK_S3_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_S3_ENABLED": [ @@ -2081,9 +2087,9 @@ ], "DD_TRACE_AWS_SDK_SFN_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_SFN_CLIENT_ENABLED": [ @@ -2109,9 +2115,9 @@ ], "DD_TRACE_AWS_SDK_SNS_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_SNS_ENABLED": [ @@ -2123,9 +2129,9 @@ ], "DD_TRACE_AWS_SDK_SQS_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_SQS_ENABLED": [ @@ -2137,9 +2143,9 @@ ], "DD_TRACE_AWS_SDK_STATES_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_STATES_ENABLED": [ @@ -2151,9 +2157,9 @@ ], "DD_TRACE_AWS_SDK_STEPFUNCTIONS_BATCH_PROPAGATION_ENABLED": [ { - "implementation": "A", + "implementation": "B", "type": "boolean", - "default": "true" + "default": "false" } ], "DD_TRACE_AWS_SDK_STEPFUNCTIONS_ENABLED": [ @@ -2485,6 +2491,13 @@ "default": "true" } ], + "DD_TRACE_ELECTRON_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "true" + } + ], "DD_TRACE_ENABLED": [ { "implementation": "A", diff --git a/packages/dd-trace/src/datastreams/checkpointer.js b/packages/dd-trace/src/datastreams/checkpointer.js index 318bac034a1..8681c88e3f7 100644 --- a/packages/dd-trace/src/datastreams/checkpointer.js +++ b/packages/dd-trace/src/datastreams/checkpointer.js @@ -33,7 +33,7 @@ class DataStreamsCheckpointer { * Examples include kafka, kinesis, sns etc. * @param {string} source - The source of data. This can be a topic, exchange or stream name. * @param {object} carrier - The carrier object to extract context from. - * @param {boolean} [manualCheckpoint=true] - Whether this checkpoint was manually set. Keep true if manually + * @param {boolean} [manualCheckpoint] - Whether this checkpoint was manually set. Keep true if manually * instrumenting. Manual instrumentation always overrides automatic * instrumentation in the case a call is both manually and automatically * instrumented. @@ -60,7 +60,7 @@ class DataStreamsCheckpointer { * Tags the active span (or the provided span) with the transaction ID and checkpoint name. * @param {string} transactionId - The transaction identifier to track. * @param {string} checkpointName - The logical checkpoint name. - * @param {object|null} [span=null] - Span to tag. Defaults to the currently active span. + * @param {object|null} [span] - Span to tag. Defaults to the currently active span. */ trackTransaction (transactionId, checkpointName, span = null) { if (!this.config.dsmEnabled) return diff --git a/packages/dd-trace/src/datastreams/manager.js b/packages/dd-trace/src/datastreams/manager.js index aa75deffde8..7e7c9eeae13 100644 --- a/packages/dd-trace/src/datastreams/manager.js +++ b/packages/dd-trace/src/datastreams/manager.js @@ -26,7 +26,7 @@ class DataStreamsManager { /** * @param {string} transactionId * @param {string} checkpointName - * @param {object|null} [span=null] + * @param {object|null} [span] */ trackTransaction (transactionId, checkpointName, span = null) { this._dataStreamsProcessor.trackTransaction(transactionId, checkpointName, span) diff --git a/packages/dd-trace/src/datastreams/processor.js b/packages/dd-trace/src/datastreams/processor.js index dc6a909685d..ac458bb322c 100644 --- a/packages/dd-trace/src/datastreams/processor.js +++ b/packages/dd-trace/src/datastreams/processor.js @@ -237,7 +237,7 @@ class DataStreamsProcessor { if (this.enabled) { this.timer = setInterval(this.onInterval.bind(this), flushInterval) - this.timer.unref() + this.timer.unref?.() } globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(this.onInterval.bind(this)) } @@ -377,7 +377,7 @@ class DataStreamsProcessor { * * @param {string} transactionId - Truncated to 255 UTF-8 bytes. * @param {string} checkpointName - Mapped to a stable 1-byte ID; silently dropped if registry full. - * @param {import('../opentelemetry/span').Span|null} [span=null] - Active span to tag with DSM transaction metadata. + * @param {import('../opentelemetry/span').Span|null} [span] - Active span to tag with DSM transaction metadata. */ trackTransaction (transactionId, checkpointName, span = null) { if (!this.enabled) { diff --git a/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js b/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js index 445a56a05fe..db5ae5ee602 100644 --- a/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js @@ -72,9 +72,9 @@ module.exports = { * @param {string} objectId - The ID of the object to get the properties of * @param {GetObjectOptions} opts - The options for the snapshot. Also used to track the deadline and communicate the * deadline overrun to the caller using the `deadlineReached` flag. - * @param {number} [depth=0] - The depth of the object. Only used internally by this module to track the current depth + * @param {number} [depth] - The depth of the object. Only used internally by this module to track the current depth * and should not be set by the caller. - * @param {boolean} [collection=false] - Whether the object is a collection. Only used internally by this module to + * @param {boolean} [collection] - Whether the object is a collection. Only used internally by this module to * track the current object type and should not be set by the caller. * @returns {Promise} The properties of the object */ diff --git a/packages/dd-trace/src/debugger/devtools_client/source-maps.js b/packages/dd-trace/src/debugger/devtools_client/source-maps.js index b63a903af02..14bdba8171b 100644 --- a/packages/dd-trace/src/debugger/devtools_client/source-maps.js +++ b/packages/dd-trace/src/debugger/devtools_client/source-maps.js @@ -63,7 +63,7 @@ function setCacheTTL () { // Clear cache a few seconds after it was last used cache.clear() } - }, 5000).unref() + }, 5000).unref?.() } function loadInlineSourceMap (data) { diff --git a/packages/dd-trace/src/debugger/devtools_client/state.js b/packages/dd-trace/src/debugger/devtools_client/state.js index 65214d1146a..6020546946c 100644 --- a/packages/dd-trace/src/debugger/devtools_client/state.js +++ b/packages/dd-trace/src/debugger/devtools_client/state.js @@ -230,7 +230,8 @@ session.on('Debugger.scriptParsed', ({ params }) => { if (reEvaluateProbesTimer === null) { reEvaluateProbesTimer = setTimeout(() => { session.emit('scriptLoadingStabilized') - }, 500).unref() + }, 500) + reEvaluateProbesTimer.unref?.() } else { reEvaluateProbesTimer.refresh() } diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index 9f3e750702e..cacdf6456c6 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -132,13 +132,13 @@ function start (config, rcInstance) { cleanup(error) // Be nice, clean up now that the worker thread encountered an issue and we can't continue }) - worker.unref() - probeChannel.port1.unref() - probeChannel.port2.unref() - logChannel.port1.unref() - logChannel.port2.unref() - configChannel.port1.unref() - configChannel.port2.unref() + worker.unref?.() + probeChannel.port1.unref?.() + probeChannel.port2.unref?.() + logChannel.port1.unref?.() + logChannel.port2.unref?.() + configChannel.port1.unref?.() + configChannel.port2.unref?.() }) } diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index a56256fb4ed..182147cffac 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -161,7 +161,7 @@ class DogStatsDClient { const socket = dgram.createSocket(type) socket.on('error', () => {}) - socket.unref() + socket.unref?.() return socket } @@ -361,7 +361,7 @@ class CustomMetrics { const flush = this.flush.bind(this) // TODO(bengl) this magic number should be configurable - setInterval(flush, 10 * 1000).unref() + setInterval(flush, 10 * 1000).unref?.() globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(flush) } diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index 228675ed237..98e10a7f16b 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -40,14 +40,14 @@ const KEY_TYPE = buildKey('type') const KEY_NAME = buildKey('name') const KEY_RESOURCE = buildKey('resource') const KEY_SERVICE = buildKey('service') +const KEY_ERROR = buildKey('error') +const KEY_START = buildKey('start') +const KEY_DURATION = buildKey('duration') const KEY_SPAN_EVENTS = buildKey('span_events') const KEY_META_STRUCT = buildKey('meta_struct') const KEY_TRACE_ID_PREFIX = buildKeyWithPrefix('trace_id', 0xCF) const KEY_SPAN_ID_PREFIX = buildKeyWithPrefix('span_id', 0xCF) const KEY_PARENT_ID_PREFIX = buildKeyWithPrefix('parent_id', 0xCF) -const KEY_ERROR_PREFIX = buildKeyWithPrefix('error', 0xCE) -const KEY_START_PREFIX = buildKeyWithPrefix('start', 0xCF) -const KEY_DURATION_PREFIX = buildKeyWithPrefix('duration', 0xCF) const KEY_META_PREFIX = buildKeyWithPrefix('meta', 0xDF) const KEY_METRICS_PREFIX = buildKeyWithPrefix('metrics', 0xDF) @@ -344,9 +344,12 @@ class AgentEncoder { cursor += KEY_SERVICE.length target.set(serviceEntry, cursor) - this.#writeIntegerField(bytes, KEY_ERROR_PREFIX, span.error) - this.#writeLongField(bytes, KEY_START_PREFIX, span.start) - this.#writeLongField(bytes, KEY_DURATION_PREFIX, span.duration) + bytes.set(KEY_ERROR) + this._encodeIntOrFloat(bytes, span.error) + bytes.set(KEY_START) + this._encodeIntOrFloat(bytes, span.start) + bytes.set(KEY_DURATION) + this._encodeIntOrFloat(bytes, span.duration) this.#encodeMetaEntries(bytes, KEY_META_PREFIX, span.meta) this.#encodeMetaEntries(bytes, KEY_METRICS_PREFIX, span.metrics) @@ -448,6 +451,8 @@ class AgentEncoder { } // Single pass: reserve the count slot, encode entries while counting, patch the count. + // Subclasses (0.5, CI visibility encoders) inherit this; the wire stays on float64 + // for numeric values to keep their established trace / events intake unchanged. _encodeMap (bytes, value) { const offset = bytes.length bytes.reserve(5) @@ -542,12 +547,9 @@ class AgentEncoder { target.set(keyEntry, writeOffset) target.set(valueEntry, writeOffset + keyEntryLen) } else { - bytes.reserve(keyEntryLen + 9) - const target = bytes.buffer - target.set(keyEntry, writeOffset) - const valueOffset = writeOffset + keyEntryLen - target[valueOffset] = 0xCB - bytes.view.setFloat64(valueOffset + 1, entryValue) + bytes.reserve(keyEntryLen) + bytes.buffer.set(keyEntry, writeOffset) + this._encodeIntOrFloat(bytes, entryValue) } count++ } @@ -588,49 +590,38 @@ class AgentEncoder { } /** + * Emit `value` as the smallest valid msgpack number encoding: compact + * unsigned/signed int when integer, float64 otherwise. Unlike + * `MsgpackEncoder#encodeNumber`, NaN keeps its float64 bits instead of + * coercing to fixint 0. + * + * Underscore-protected so the 0.5 subclass can call it from its own + * `_encode` / `_encodeMap` overrides. + * * @param {MsgpackChunk} bytes - * @param {Buffer} keyPrefix Precomputed `[key, 0xCE]`. * @param {number} value */ - #writeIntegerField (bytes, keyPrefix, value) { - const keyPrefixLen = keyPrefix.length - const offset = bytes.length - bytes.reserve(keyPrefixLen + 4) - - const target = bytes.buffer - target.set(keyPrefix, offset) - - const valueOffset = offset + keyPrefixLen - target[valueOffset] = value >> 24 - target[valueOffset + 1] = value >> 16 - target[valueOffset + 2] = value >> 8 - target[valueOffset + 3] = value - } - - /** - * @param {MsgpackChunk} bytes - * @param {Buffer} keyPrefix Precomputed `[key, 0xCF]`. - * @param {number} value Up to a 53-bit safe integer. - */ - #writeLongField (bytes, keyPrefix, value) { - const high = (value / 2 ** 32) >> 0 - const low = value >>> 0 - const keyPrefixLen = keyPrefix.length - const offset = bytes.length - bytes.reserve(keyPrefixLen + 8) - - const target = bytes.buffer - target.set(keyPrefix, offset) - - const valueOffset = offset + keyPrefixLen - target[valueOffset] = high >> 24 - target[valueOffset + 1] = high >> 16 - target[valueOffset + 2] = high >> 8 - target[valueOffset + 3] = high - target[valueOffset + 4] = low >> 24 - target[valueOffset + 5] = low >> 16 - target[valueOffset + 6] = low >> 8 - target[valueOffset + 7] = low + _encodeIntOrFloat (bytes, value) { + // Fast path: positive fixint (0..127). `value === (value & 0x7F)` is true + // iff `value` is an exact integer in that range — covers `error: 0/1`, + // priority flags, attribute counts, HTTP status codes mapped to numbers, + // and most small metrics. NaN, ±Infinity, negatives, and any non-integer + // float fall through. + if (value === (value & 0x7F)) { + const offset = bytes.length + bytes.reserve(1) + bytes.buffer[offset] = value + return + } + if (Number.isInteger(value)) { + if (value >= 0) { + this.#msgpack.encodeUnsigned(bytes, value) + } else { + this.#msgpack.encodeSigned(bytes, value) + } + } else { + this.#encodeFloat(bytes, value) + } } /** @@ -853,7 +844,7 @@ class AgentEncoder { if (typeof value === 'number') { this._encodeString(bytes, key) bytes.set(Number.isInteger(value) ? ATTR_PREFIX_INT : ATTR_PREFIX_DOUBLE) - this.#encodeFloat(bytes, value) + this._encodeIntOrFloat(bytes, value) return true } if (typeof value === 'boolean') { @@ -923,7 +914,7 @@ class AgentEncoder { } if (typeof value === 'number') { bytes.set(Number.isInteger(value) ? ATTR_PREFIX_INT : ATTR_PREFIX_DOUBLE) - this.#encodeFloat(bytes, value) + this._encodeIntOrFloat(bytes, value) return true } if (typeof value === 'boolean') { diff --git a/packages/dd-trace/src/encode/0.5.js b/packages/dd-trace/src/encode/0.5.js index c14c485412e..ffa926946d3 100644 --- a/packages/dd-trace/src/encode/0.5.js +++ b/packages/dd-trace/src/encode/0.5.js @@ -46,15 +46,46 @@ class AgentEncoder extends BaseEncoder { this._encodeId(bytes, span.trace_id) this._encodeId(bytes, span.span_id) this._encodeId(bytes, span.parent_id) - this._encodeLong(bytes, span.start || 0) - this._encodeLong(bytes, span.duration || 0) - this._encodeInteger(bytes, span.error) + this._encodeIntOrFloat(bytes, span.start || 0) + this._encodeIntOrFloat(bytes, span.duration || 0) + this._encodeIntOrFloat(bytes, span.error) this._encodeMap(bytes, span.meta || {}) this._encodeMap(bytes, span.metrics || {}) this._encodeString(bytes, span.type) } } + // Override the inherited 0.4 `_encodeMap` so the v0.5 wire emits each numeric + // value via `_encodeIntOrFloat` (compact unsigned/signed int when integer, + // float64 otherwise) instead of always float64. The 0.4 base method stays on + // float64 because the CI-visibility encoders inherit it and target a + // different intake. + _encodeMap (bytes, value) { + const offset = bytes.length + bytes.reserve(5) + bytes.buffer[offset] = 0xDF + + let count = 0 + for (const key of Object.keys(value)) { + const entryValue = value[key] + if (typeof entryValue === 'string') { + this._encodeString(bytes, key) + this._encodeString(bytes, entryValue) + count++ + } else if (typeof entryValue === 'number') { + this._encodeString(bytes, key) + this._encodeIntOrFloat(bytes, entryValue) + count++ + } + } + + const target = bytes.buffer + target[offset + 1] = count >>> 24 + target[offset + 2] = count >>> 16 + target[offset + 3] = count >>> 8 + target[offset + 4] = count + } + _encodeString (bytes, value = '') { let index = this._stringMap[value] if (index === undefined) { diff --git a/packages/dd-trace/src/encode/agentless-json.js b/packages/dd-trace/src/encode/agentless-json.js index 3f4ffd039e2..8e3634c0b08 100644 --- a/packages/dd-trace/src/encode/agentless-json.js +++ b/packages/dd-trace/src/encode/agentless-json.js @@ -85,7 +85,7 @@ function spanToJSON (span) { class AgentlessJSONEncoder { /** * @param {object} writer - Writer instance with a flush() method, called when the buffer exceeds the soft limit - * @param {object} [metadata={}] - Shared metadata spread into each trace object (hostname, env, tracerVersion, etc.) + * @param {object} [metadata] - Shared metadata spread into each trace object (hostname, env, tracerVersion, etc.) */ constructor (writer, metadata = {}) { this._writer = writer diff --git a/packages/dd-trace/src/exporter.js b/packages/dd-trace/src/exporter.js index c0a0433ff02..10e9b9730e1 100644 --- a/packages/dd-trace/src/exporter.js +++ b/packages/dd-trace/src/exporter.js @@ -7,6 +7,8 @@ const constants = require('./constants') module.exports = function getExporter (name) { switch (name) { + case exporters.ELECTRON: + return require('./exporters/electron') case exporters.LOG: return require('./exporters/log') case exporters.AGENT: diff --git a/packages/dd-trace/src/exporters/agent/index.js b/packages/dd-trace/src/exporters/agent/index.js index 28a26713ed5..9197397229b 100644 --- a/packages/dd-trace/src/exporters/agent/index.js +++ b/packages/dd-trace/src/exporters/agent/index.js @@ -50,7 +50,8 @@ class AgentExporter { this.#timer = setTimeout(() => { this._writer.flush() this.#timer = undefined - }, flushInterval).unref() + }, flushInterval) + this.#timer.unref?.() } } diff --git a/packages/dd-trace/src/exporters/agentless/index.js b/packages/dd-trace/src/exporters/agentless/index.js index 9cd7a233c86..c3ea8472012 100644 --- a/packages/dd-trace/src/exporters/agentless/index.js +++ b/packages/dd-trace/src/exporters/agentless/index.js @@ -18,7 +18,7 @@ class AgentlessExporter { /** * @param {object} config - Configuration object - * @param {string} [config.site='datadoghq.com'] - The Datadog site + * @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'. * @param {string} [config.url] - Override intake URL * @param {number} [config.flushInterval] - Batch flush interval in ms * @param {string} [config.env] - Environment name @@ -109,7 +109,8 @@ class AgentlessExporter { log.error('Failed to flush traces on timer: %s', err.message) } this.#timer = undefined - }, flushInterval).unref() + }, flushInterval) + this.#timer.unref?.() } } diff --git a/packages/dd-trace/src/exporters/agentless/writer.js b/packages/dd-trace/src/exporters/agentless/writer.js index 10ca6c0477b..c15dfea92db 100644 --- a/packages/dd-trace/src/exporters/agentless/writer.js +++ b/packages/dd-trace/src/exporters/agentless/writer.js @@ -19,8 +19,8 @@ class AgentlessWriter extends BaseWriter { /** * @param {object} options - Writer options * @param {URL} [options.url] - The intake URL. If not provided, constructed from site. - * @param {string} [options.site='datadoghq.com'] - The Datadog site - * @param {object} [options.metadata={}] - Metadata to pass to the encoder (hostname, env, etc.) + * @param {string} [options.site] - The Datadog site + * @param {object} [options.metadata] - Metadata to pass to the encoder (hostname, env, etc.) */ constructor ({ url, site = 'datadoghq.com', metadata = {} }) { super({ url }) diff --git a/packages/dd-trace/src/exporters/common/buffering-exporter.js b/packages/dd-trace/src/exporters/common/buffering-exporter.js index 266cb6f1cf0..a5a6f04a439 100644 --- a/packages/dd-trace/src/exporters/common/buffering-exporter.js +++ b/packages/dd-trace/src/exporters/common/buffering-exporter.js @@ -39,7 +39,8 @@ class BufferingExporter { this[timerKey] = setTimeout(() => { writer.flush() this[timerKey] = undefined - }, flushInterval).unref() + }, flushInterval) + this[timerKey].unref?.() } } diff --git a/packages/dd-trace/src/exporters/common/request.js b/packages/dd-trace/src/exporters/common/request.js index 4da4410f53a..87fa510b60a 100644 --- a/packages/dd-trace/src/exporters/common/request.js +++ b/packages/dd-trace/src/exporters/common/request.js @@ -180,7 +180,7 @@ function request (data, options, callback) { // Unref so a pending retry never keeps the host process alive past // its natural exit point; long-running apps still retry because the // event loop is held open by their own work. - setTimeout(attempt, getRetryDelay(options, attemptIndex), attemptIndex + 1).unref() + setTimeout(attempt, getRetryDelay(options, attemptIndex), attemptIndex + 1).unref?.() } else { callback(error) } diff --git a/packages/dd-trace/src/exporters/electron/index.js b/packages/dd-trace/src/exporters/electron/index.js new file mode 100644 index 00000000000..813c0da2f04 --- /dev/null +++ b/packages/dd-trace/src/exporters/electron/index.js @@ -0,0 +1,49 @@ +'use strict' + +const { channel } = require('dc-polyfill') +const { truncateSpan, normalizeSpan } = require('../../encode/tags-processors') + +const traceChannel = channel('datadog:apm:electron:export') + +class ElectronExporter { + #timer + #traces = [] + + constructor (config) { + this._config = config + + globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(this.flush.bind(this)) + } + + export (spans) { + this.#traces.push(spans) + + const { flushInterval } = this._config + + if (flushInterval === 0) { + this.flush() + } else if (this.#timer === undefined) { + this.#timer = setTimeout(() => { + this.flush() + this.#timer = undefined + }, flushInterval) + this.#timer.unref?.() + } + } + + flush (done = () => {}) { + clearTimeout(this.#timer) + this.#timer = undefined + + const traces = this.#traces.splice(0) + + if (traces.length > 0 && traceChannel.hasSubscribers) { + const formattedTraces = traces.map(spans => spans.map(span => normalizeSpan(truncateSpan(span)))) + traceChannel.publish(formattedTraces) + } + + done() + } +} + +module.exports = ElectronExporter diff --git a/packages/dd-trace/src/external-logger/src/index.js b/packages/dd-trace/src/external-logger/src/index.js index 696790f5732..c00e519f5eb 100644 --- a/packages/dd-trace/src/external-logger/src/index.js +++ b/packages/dd-trace/src/external-logger/src/index.js @@ -26,7 +26,8 @@ class ExternalLogger { } this.timer = setInterval(() => { this.flush() - }, this.interval).unref() + }, this.interval) + this.timer.unref?.() tracerLogger.debug(`started log writer to https://${this.intake}${this.endpoint}`) } diff --git a/packages/dd-trace/src/git_metadata.js b/packages/dd-trace/src/git_metadata.js index 240b41cda93..66c05f41c50 100644 --- a/packages/dd-trace/src/git_metadata.js +++ b/packages/dd-trace/src/git_metadata.js @@ -12,19 +12,21 @@ const { resolveGitHeadSHA, } = require('./config/git_properties') -/** @type {{ commitSHA: string | undefined, repositoryUrl: string | undefined } | undefined} */ -let cached +/** + * @typedef {{ commitSHA: string | undefined, repositoryUrl: string | undefined }} GitMetadata + * @type {{ enabled?: GitMetadata, disabled?: GitMetadata }} + */ +const cache = {} /** * @param {import('./config/config-types').ConfigProperties} config */ function getGitMetadata (config) { - if (cached) return cached - if (!config.DD_TRACE_GIT_METADATA_ENABLED) { - cached = { commitSHA: undefined, repositoryUrl: undefined } - return cached + cache.disabled ??= { commitSHA: undefined, repositoryUrl: undefined } + return cache.disabled } + if (cache.enabled) return cache.enabled let repositoryUrl = removeUserSensitiveInfo(config.DD_GIT_REPOSITORY_URL ?? config.tags[GIT_REPOSITORY_URL]) let commitSHA = config.DD_GIT_COMMIT_SHA ?? config.tags[GIT_COMMIT_SHA] @@ -59,8 +61,8 @@ function getGitMetadata (config) { commitSHA ??= resolveGitHeadSHA(gitFolderPath) - cached = { commitSHA, repositoryUrl } - return cached + cache.enabled = { commitSHA, repositoryUrl } + return cache.enabled } module.exports = getGitMetadata diff --git a/packages/dd-trace/src/lambda/handler-paths.js b/packages/dd-trace/src/lambda/handler-paths.js new file mode 100644 index 00000000000..88aef375822 --- /dev/null +++ b/packages/dd-trace/src/lambda/handler-paths.js @@ -0,0 +1,52 @@ +/** + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Modifications copyright 2022 Datadog, Inc. + * + * Some functions are part of aws-lambda-nodejs-runtime-interface-client + * https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v2.1.0/src/utils/UserFunction.ts + */ +'use strict' + +const path = require('path') + +/** + * Example: `'./api/src/index.nested.handler'` → `['./api/src/', 'index.nested.handler']`. + * + * @param {string} fullHandler + */ +function extractModuleRootAndHandler (fullHandler) { + const handlerString = path.basename(fullHandler) + const moduleRoot = fullHandler.slice(0, Math.max(0, fullHandler.indexOf(handlerString))) + return [moduleRoot, handlerString] +} + +/** + * Example: `'index.nested.handler'` → `['index', 'nested.handler']`. + * + * @param {string} handler + * @throws {Error} When the handler is not of the form `.`. + */ +function extractModuleNameAndHandlerPath (handler) { + const match = handler.match(/^([^.]*)\.(.*)$/) + if (!match || match.length !== 3) { + throw new Error(`Malformed handler name: ${handler}`) + } + return [match[1], match[2]] +} + +/** + * @param {string} lambdaStylePath `LAMBDA_TASK_ROOT` joined with the module root and module name. + */ +function getLambdaFilePaths (lambdaStylePath) { + return [ + `${lambdaStylePath}.js`, + `${lambdaStylePath}.mjs`, + `${lambdaStylePath}.cjs`, + ] +} + +module.exports = { + extractModuleRootAndHandler, + extractModuleNameAndHandlerPath, + getLambdaFilePaths, +} diff --git a/packages/dd-trace/src/lambda/index.js b/packages/dd-trace/src/lambda/index.js index cdd1cdb2592..4b447770dc1 100644 --- a/packages/dd-trace/src/lambda/index.js +++ b/packages/dd-trace/src/lambda/index.js @@ -1,17 +1,65 @@ 'use strict' -const { getValueFromEnvSources } = require('../config/helper') -const { registerLambdaHook } = require('./runtime/ritm') - -/** - * It is safe to do it this way, since customers will never be expected to disable - * this specific instrumentation through the init config object. - */ -const _DD_TRACE_DISABLED_INSTRUMENTATIONS = getValueFromEnvSources('DD_TRACE_DISABLED_INSTRUMENTATIONS') || '' -const _disabledInstrumentations = new Set( - _DD_TRACE_DISABLED_INSTRUMENTATIONS ? _DD_TRACE_DISABLED_INSTRUMENTATIONS.split(',') : [] -) - -if (!_disabledInstrumentations.has('lambda')) { - registerLambdaHook() +const path = require('path') + +const log = require('../log') +const { getEnvironmentVariable, getValueFromEnvSources } = require('../config/helper') +const Hook = require('../../../datadog-instrumentations/src/helpers/hook') +const instrumentations = require('../../../datadog-instrumentations/src/helpers/instrumentations') +const { + filename, + pathSepExpr, +} = require('../../../datadog-instrumentations/src/helpers/register') +const { + extractModuleNameAndHandlerPath, + extractModuleRootAndHandler, + getLambdaFilePaths, +} = require('./handler-paths') + +if (!getValueFromEnvSources('DD_TRACE_DISABLED_INSTRUMENTATIONS')?.split(',').includes('lambda')) { + const lambdaTaskRoot = getEnvironmentVariable('LAMBDA_TASK_ROOT') + const originalLambdaHandler = getValueFromEnvSources('DD_LAMBDA_HANDLER') + + if (originalLambdaHandler !== undefined && lambdaTaskRoot !== undefined) { + const [moduleRoot, moduleAndHandler] = extractModuleRootAndHandler(originalLambdaHandler) + const [moduleName] = extractModuleNameAndHandlerPath(moduleAndHandler) + + const lambdaStylePath = path.resolve(lambdaTaskRoot, moduleRoot, moduleName) + const lambdaFilePaths = getLambdaFilePaths(lambdaStylePath) + + // TODO: Redo this like any other instrumentation. + Hook(lambdaFilePaths, (moduleExports, name, _, moduleVersion) => { + require('./runtime/patch') + + for (const { hook } of instrumentations[name]) { + try { + moduleExports = hook(moduleExports, moduleVersion) ?? moduleExports + } catch (error) { + log.error('Error executing lambda hook', error) + } + } + + return moduleExports + }) + return + } + + const moduleToPatch = 'datadog-lambda-js' + Hook([moduleToPatch], (moduleExports, moduleName, _, moduleVersion) => { + moduleName = moduleName.replace(pathSepExpr, '/') + require('./runtime/patch') + + for (const { file, hook } of instrumentations[moduleToPatch]) { + const fullFilename = filename(moduleToPatch, file) + if (moduleName === fullFilename) { + try { + moduleExports = hook(moduleExports, moduleVersion) ?? moduleExports + } catch (error) { + log.error('Error executing lambda hook for datadog-lambda-js', error) + } + } + } + + return moduleExports + }) } diff --git a/packages/dd-trace/src/lambda/runtime/patch.js b/packages/dd-trace/src/lambda/runtime/patch.js index 55fb0ae7e5f..6b8a1086e52 100644 --- a/packages/dd-trace/src/lambda/runtime/patch.js +++ b/packages/dd-trace/src/lambda/runtime/patch.js @@ -6,54 +6,32 @@ const { datadog } = require('../handler') const { addHook } = require('../../../../datadog-instrumentations/src/helpers/instrument') const shimmer = require('../../../../datadog-shimmer') const { getEnvironmentVariable, getValueFromEnvSources } = require('../../config/helper') -const { _extractModuleNameAndHandlerPath, _extractModuleRootAndHandler, _getLambdaFilePaths } = require('./ritm') - -/** - * Patches a Datadog Lambda module by calling `patchDatadogLambdaHandler` - * with the handler name `datadog`. - * - * @param {object} datadogLambdaModule node module to be patched. - * @returns a Datadog Lambda module with the `datadog` function from - * `datadog-lambda-js` patched. - */ -const patchDatadogLambdaModule = (datadogLambdaModule) => { +const { + extractModuleNameAndHandlerPath, + extractModuleRootAndHandler, + getLambdaFilePaths, +} = require('../handler-paths') + +/** @param {object} datadogLambdaModule */ +function patchDatadogLambdaModule (datadogLambdaModule) { shimmer.wrap(datadogLambdaModule, 'datadog', patchDatadogLambdaHandler) - return datadogLambdaModule } -/** - * Patches a Datadog Lambda handler in order to do - * Datadog instrumentation by getting the Lambda handler from its - * arguments. - * - * @param {Function} datadogHandler the Datadog Lambda handler to destructure. - * @returns the datadogHandler with its arguments patched. - */ +/** @param {Function} datadogHandler */ function patchDatadogLambdaHandler (datadogHandler) { - return (userHandler) => { - return datadogHandler(datadog(userHandler)) - } + return userHandler => datadogHandler(datadog(userHandler)) } -/** - * Patches a Lambda module on the given handler path. - * - * @param {string} handlerPath path of the handler to be patched. - * @returns a module with the given handler path patched. - */ -const patchLambdaModule = (handlerPath) => (lambdaModule) => { - shimmer.wrap(lambdaModule, handlerPath, patchLambdaHandler) - - return lambdaModule +/** @param {string} handlerPath */ +function patchLambdaModule (handlerPath) { + return lambdaModule => { + shimmer.wrap(lambdaModule, handlerPath, patchLambdaHandler) + return lambdaModule + } } -/** - * Patches a Lambda handler in order to do Datadog instrumentation. - * - * @param {Function} lambdaHandler the Lambda handler to be patched. - * @returns a function which patches the given Lambda handler. - */ +/** @param {Function} lambdaHandler */ function patchLambdaHandler (lambdaHandler) { return datadog(lambdaHandler) } @@ -62,16 +40,13 @@ const lambdaTaskRoot = getEnvironmentVariable('LAMBDA_TASK_ROOT') const originalLambdaHandler = getValueFromEnvSources('DD_LAMBDA_HANDLER') if (originalLambdaHandler === undefined) { - // Instrumentation is done manually. addHook({ name: 'datadog-lambda-js' }, patchDatadogLambdaModule) } else { - const [moduleRoot, moduleAndHandler] = _extractModuleRootAndHandler(originalLambdaHandler) - const [_module, handlerPath] = _extractModuleNameAndHandlerPath(moduleAndHandler) - - const lambdaStylePath = path.resolve(lambdaTaskRoot, moduleRoot, _module) - const lambdaFilePaths = _getLambdaFilePaths(lambdaStylePath) + const [moduleRoot, moduleAndHandler] = extractModuleRootAndHandler(originalLambdaHandler) + const [moduleName, handlerPath] = extractModuleNameAndHandlerPath(moduleAndHandler) - for (const lambdaFilePath of lambdaFilePaths) { + const lambdaStylePath = path.resolve(lambdaTaskRoot, moduleRoot, moduleName) + for (const lambdaFilePath of getLambdaFilePaths(lambdaStylePath)) { addHook({ name: lambdaFilePath }, patchLambdaModule(handlerPath)) } } diff --git a/packages/dd-trace/src/lambda/runtime/ritm.js b/packages/dd-trace/src/lambda/runtime/ritm.js deleted file mode 100644 index 40ab513f0d9..00000000000 --- a/packages/dd-trace/src/lambda/runtime/ritm.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * Modifications copyright 2022 Datadog, Inc. - * - * Some functions are part of aws-lambda-nodejs-runtime-interface-client - * https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v2.1.0/src/utils/UserFunction.ts - */ -'use strict' - -const path = require('path') - -const log = require('../../log') -const { getEnvironmentVariable, getValueFromEnvSources } = require('../../config/helper') -const Hook = require('../../../../datadog-instrumentations/src/helpers/hook') -const instrumentations = require('../../../../datadog-instrumentations/src/helpers/instrumentations') -const { - filename, - pathSepExpr, -} = require('../../../../datadog-instrumentations/src/helpers/register') - -/** - * Breaks the full handler string into two pieces: the module root - * and the actual handler string. - * - * @param {string} fullHandler user's lambda handler, commonly stored in `DD_LAMBDA_HANDLER`. - * @returns {string[]} an array containing the module root and the handler string. - * - * ```js - * _extractModuleRootAndHandler('./api/src/index.nested.handler') - * // => ['./api/src', 'index.nested.handler'] - * ``` - */ -function _extractModuleRootAndHandler (fullHandler) { - const handlerString = path.basename(fullHandler) - const moduleRoot = fullHandler.slice(0, Math.max(0, fullHandler.indexOf(handlerString))) - - return [moduleRoot, handlerString] -} - -/** - * Splits the handler string into two pieces: the module name - * and the path to the handler function. - * - * @param {string} handler a handler string containing the module and the handler path. - * @returns {string[]} an array containing the module name and the handler path. - * - * ```js - * _extractModuleNameAndHandlerPath('index.nested.handler') - * // => ['index', 'nested.handler'] - * ``` - */ -function _extractModuleNameAndHandlerPath (handler) { - const FUNCTION_EXPR = /^([^.]*)\.(.*)$/ - const match = handler.match(FUNCTION_EXPR) - if (!match || match.length !== 3) { - throw new Error(`Malformed handler name: ${handler}`) - } - return [match[1], match[2]] // [module, handler-path] -} - -/** - * Returns all possible paths of the files to be patched when required. - * - * @param {string} lambdaStylePath the path comprised of the `LAMBDA_TASK_ROOT`, - * the root of the module of the Lambda handler, and the module name. - * @returns the lambdaStylePath with appropiate extensions for the hook. - */ -function _getLambdaFilePaths (lambdaStylePath) { - return [ - `${lambdaStylePath}.js`, - `${lambdaStylePath}.mjs`, - `${lambdaStylePath}.cjs`, - ] -} - -/** - * Register a hook for the Lambda handler to be executed when - * the file is required. - */ -const registerLambdaHook = () => { - const lambdaTaskRoot = getEnvironmentVariable('LAMBDA_TASK_ROOT') - const originalLambdaHandler = getValueFromEnvSources('DD_LAMBDA_HANDLER') - - if (originalLambdaHandler !== undefined && lambdaTaskRoot !== undefined) { - const [moduleRoot, moduleAndHandler] = _extractModuleRootAndHandler(originalLambdaHandler) - const [_module] = _extractModuleNameAndHandlerPath(moduleAndHandler) - - const lambdaStylePath = path.resolve(lambdaTaskRoot, moduleRoot, _module) - const lambdaFilePaths = _getLambdaFilePaths(lambdaStylePath) - - // TODO: Redo this like any other instrumentation. - Hook(lambdaFilePaths, (moduleExports, name, _, moduleVersion) => { - require('./patch') - - for (const { hook } of instrumentations[name]) { - try { - moduleExports = hook(moduleExports, moduleVersion) ?? moduleExports - } catch (e) { - log.error('Error executing lambda hook', e) - } - } - - return moduleExports - }) - } else { - const moduleToPatch = 'datadog-lambda-js' - Hook([moduleToPatch], (moduleExports, moduleName, _, moduleVersion) => { - moduleName = moduleName.replace(pathSepExpr, '/') - - require('./patch') - - for (const { file, hook } of instrumentations[moduleToPatch]) { - const fullFilename = filename(moduleToPatch, file) - if (moduleName === fullFilename) { - try { - moduleExports = hook(moduleExports, moduleVersion) ?? moduleExports - } catch (e) { - log.error('Error executing lambda hook for datadog-lambda-js', e) - } - } - } - - return moduleExports - }) - } -} - -module.exports = { - _extractModuleRootAndHandler, - _extractModuleNameAndHandlerPath, - _getLambdaFilePaths, - registerLambdaHook, -} diff --git a/packages/dd-trace/src/llmobs/index.js b/packages/dd-trace/src/llmobs/index.js index 1e34a5a71ab..e7669110cf9 100644 --- a/packages/dd-trace/src/llmobs/index.js +++ b/packages/dd-trace/src/llmobs/index.js @@ -108,6 +108,10 @@ function disable () { // since LLMObs traces can extend between services and be the same trace, // we need to propagate the parent id and mlApp. function handleLLMObsParentIdInjection ({ carrier }) { + // Respect the standard propagator's gate: when trace tag propagation is + // disabled, don't write `x-datadog-tags` for LLMObs either. + if (globalTracerConfig.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH === 0) return + const parent = storage.getStore()?.span const mlObsSpanTags = LLMObsTagger.tagMap.get(parent) @@ -118,8 +122,15 @@ function handleLLMObsParentIdInjection ({ carrier }) { parentContext?._trace?.tags?.[PROPAGATED_ML_APP_KEY] || globalTracerConfig.llmobs.mlApp - if (parentId) carrier['x-datadog-tags'] += `,${PROPAGATED_PARENT_ID_KEY}=${parentId}` - if (mlApp) carrier['x-datadog-tags'] += `,${PROPAGATED_ML_APP_KEY}=${mlApp}` + if (!parentId && !mlApp) return + + // `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*` + // tags, so it may be undefined here — coalesce before appending. + const existing = carrier['x-datadog-tags'] + let tags = existing || '' + if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}` + if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}` + if (tags !== existing) carrier['x-datadog-tags'] = tags } function handleFlush () { diff --git a/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js b/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js index db0249b8a6b..912c032e48a 100644 --- a/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +++ b/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js @@ -14,7 +14,17 @@ const llmobsStore = storage('llmobs') const ENABLED_OPERATIONS = new Set(['invokeModel', 'invokeModelWithResponseStream']) -const requestIdsToTokens = {} +/** + * @typedef {{ + * inputTokensFromHeaders?: number, + * outputTokensFromHeaders?: number, + * cacheReadTokensFromHeaders?: number, + * cacheWriteTokensFromHeaders?: number, + * }} HeaderTokens + */ + +/** @type {Map} */ +const pendingTokenHeaders = new Map() class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin { constructor () { @@ -24,33 +34,39 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin { const { response } = ctx const request = response.request const operation = request.operation + + // Release the cached headers even for operations the plugin does not tag, + // so non-LLM Bedrock calls do not leak entries into pendingTokenHeaders. + const tokensFromHeaders = consumeTokenHeaders(response.$metadata?.requestId) + // avoids instrumenting other non supported runtime operations - if (!ENABLED_OPERATIONS.has(operation)) { - return - } + if (!ENABLED_OPERATIONS.has(operation)) return + const { modelProvider, modelName } = parseModelId(request.params.modelId) // avoids instrumenting non llm type - if (modelName.includes('embed')) { - return - } + if (modelName.includes('embed')) return + const span = ctx.currentStore?.span - this.setLLMObsTags({ ctx, request, span, response, modelProvider, modelName }) + this.setLLMObsTags({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders }) }) this.addSub('apm:aws:response:deserialize:bedrockruntime', ({ headers }) => { const requestId = headers['x-amzn-requestid'] + // No request id means no way to correlate with the :complete: event. + if (!requestId) return + const inputTokenCount = headers['x-amzn-bedrock-input-token-count'] const outputTokenCount = headers['x-amzn-bedrock-output-token-count'] const cacheReadTokenCount = headers['x-amzn-bedrock-cache-read-input-token-count'] const cacheWriteTokenCount = headers['x-amzn-bedrock-cache-write-input-token-count'] - requestIdsToTokens[requestId] = { + pendingTokenHeaders.set(requestId, { inputTokensFromHeaders: inputTokenCount && Number.parseInt(inputTokenCount), outputTokensFromHeaders: outputTokenCount && Number.parseInt(outputTokenCount), cacheReadTokensFromHeaders: cacheReadTokenCount && Number.parseInt(cacheReadTokenCount), cacheWriteTokensFromHeaders: cacheWriteTokenCount && Number.parseInt(cacheWriteTokenCount), - } + }) }) this.addSub('apm:aws:response:streamed-chunk:bedrockruntime', ({ ctx, chunk }) => { @@ -60,7 +76,7 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin { }) } - setLLMObsTags ({ ctx, request, span, response, modelProvider, modelName }) { + setLLMObsTags ({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders }) { const isStream = request?.operation?.toLowerCase().includes('stream') telemetry.incrementLLMObsSpanStartCount({ autoinstrumented: true, integration: 'bedrock' }) @@ -97,7 +113,7 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin { // add token metrics const { inputTokens, outputTokens, totalTokens, cacheReadTokens, cacheWriteTokens } = extractTokens({ - requestId: response.$metadata.requestId, + tokensFromHeaders, usage: textAndResponseReason.usage, }) this._tagger.tagMetrics(span, { @@ -110,14 +126,28 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin { } } -function extractTokens ({ requestId, usage }) { +/** + * @param {string | undefined} requestId + * @returns {HeaderTokens | undefined} + */ +function consumeTokenHeaders (requestId) { + const tokens = pendingTokenHeaders.get(requestId) + pendingTokenHeaders.delete(requestId) + return tokens +} + +/** + * Combine response-body usage with header-derived counts, preferring the body. + * + * @param {{ tokensFromHeaders: HeaderTokens | undefined, usage: Record }} options + */ +function extractTokens ({ tokensFromHeaders, usage }) { const { inputTokensFromHeaders, outputTokensFromHeaders, cacheReadTokensFromHeaders, cacheWriteTokensFromHeaders, - } = requestIdsToTokens[requestId] || {} - delete requestIdsToTokens[requestId] + } = tokensFromHeaders ?? {} const inputTokens = usage.inputTokens || inputTokensFromHeaders || 0 const outputTokens = usage.outputTokens || outputTokensFromHeaders || 0 diff --git a/packages/dd-trace/src/llmobs/writers/base.js b/packages/dd-trace/src/llmobs/writers/base.js index 21b70ecb36c..a4ab37f0bae 100644 --- a/packages/dd-trace/src/llmobs/writers/base.js +++ b/packages/dd-trace/src/llmobs/writers/base.js @@ -54,7 +54,8 @@ class BaseLLMObsWriter { this._periodic = setInterval(() => { this.flush() - }, this._interval).unref() + }, this._interval) + this._periodic.unref?.() const destroyer = this.destroy.bind(this) globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(destroyer) diff --git a/packages/dd-trace/src/openfeature/writers/base.js b/packages/dd-trace/src/openfeature/writers/base.js index a7da72be81b..073eef921d0 100644 --- a/packages/dd-trace/src/openfeature/writers/base.js +++ b/packages/dd-trace/src/openfeature/writers/base.js @@ -55,7 +55,8 @@ class BaseFFEWriter { this._periodic = setInterval(() => { this.flush() - }, this._interval).unref() + }, this._interval) + this._periodic.unref?.() const destroyer = this.destroy.bind(this) globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(destroyer) diff --git a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js index 5d3992cc71d..d9395289417 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +++ b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js @@ -167,7 +167,8 @@ class PeriodicMetricReader { this.#timer = setInterval(() => { this.#collectAndExport() - }, this.#exportInterval).unref() + }, this.#exportInterval) + this.#timer.unref?.() } /** diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index e9adeca3599..f3c453de1ba 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -9,6 +9,7 @@ const tags = require('../../../../../ext/tags') const { getConfiguredEnvName } = require('../../config/helper') const { setAllBaggageItems, getAllBaggageItems, removeAllBaggageItems } = require('../../baggage') const telemetryMetrics = require('../../telemetry/metrics') +const { DD_MAJOR } = require('../../../../../version') const { AUTO_KEEP, AUTO_REJECT, USER_KEEP } = require('../../../../../ext/priority') const TraceState = require('./tracestate') @@ -78,10 +79,17 @@ class TextMapPropagator { constructor (config) { this._config = config - // TODO: should match "b3 single header" in next major - const envName = getConfiguredEnvName('DD_TRACE_PROPAGATION_STYLE') - // eslint-disable-next-line eslint-rules/eslint-env-aliases - this.#extractB3Context = envName === 'OTEL_PROPAGATORS' ? this._extractB3SingleContext : this._extractB3MultiContext + // v6: `'b3'` is always single-header. v5: env-name decides — OTEL_PROPAGATORS callers expect + // single, the legacy `DD_TRACE_PROPAGATION_STYLE` callers expect multi. + if (DD_MAJOR >= 6) { + this.#extractB3Context = this._extractB3SingleContext + } else { + const envName = getConfiguredEnvName('DD_TRACE_PROPAGATION_STYLE') + // eslint-disable-next-line eslint-rules/eslint-env-aliases + this.#extractB3Context = envName === 'OTEL_PROPAGATORS' + ? this._extractB3SingleContext + : this._extractB3MultiContext + } } /** @@ -262,9 +270,10 @@ class TextMapPropagator { } _injectB3MultipleHeaders (spanContext, carrier) { - const hasB3 = this._hasPropagationStyle('inject', 'b3') - const hasB3multi = this._hasPropagationStyle('inject', 'b3multi') - if (!(hasB3 || hasB3multi)) return + // v5 also accepts the legacy `'b3'` spelling for multi; v6 routes `'b3'` to single-header. + const hasB3multi = this._hasPropagationStyle('inject', 'b3multi') || + (DD_MAJOR < 6 && this._hasPropagationStyle('inject', 'b3')) + if (!hasB3multi) return carrier[b3TraceKey] = this._getB3TraceId(spanContext) carrier[b3SpanKey] = spanContext._spanId.toString(16) @@ -280,7 +289,9 @@ class TextMapPropagator { } _injectB3SingleHeader (spanContext, carrier) { - const hasB3SingleHeader = this._hasPropagationStyle('inject', 'b3 single header') + // v6 keeps `'b3 single header'` as a back-compat alias for callers that bypass parser normalisation. + const hasB3SingleHeader = this._hasPropagationStyle('inject', 'b3 single header') || + (DD_MAJOR >= 6 && this._hasPropagationStyle('inject', 'b3')) if (!hasB3SingleHeader) return null const traceId = this._getB3TraceId(spanContext) @@ -394,7 +405,7 @@ class TextMapPropagator { case 'tracecontext': extractedContext = this._extractTraceparentContext(carrier) break - case 'b3 single header': // TODO: delete in major after singular "b3" + case 'b3 single header': extractedContext = this._extractB3SingleContext(carrier) break case 'b3': diff --git a/packages/dd-trace/src/payload-tagging/config/index.js b/packages/dd-trace/src/payload-tagging/config/index.js index c103349ca8b..1f6c4267279 100644 --- a/packages/dd-trace/src/payload-tagging/config/index.js +++ b/packages/dd-trace/src/payload-tagging/config/index.js @@ -30,8 +30,8 @@ function getSDKRules (sdk, requestInput, responseInput) { * Appends input rules to all supported SDKs and returns a structure mapping SDK * names to per-service rules. * - * @param {string[]} [requestInput=[]] - * @param {string[]} [responseInput=[]] + * @param {string[]} [requestInput] + * @param {string[]} [responseInput] * @returns {Record} */ function appendRules (requestInput = [], responseInput = []) { diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index d318174bf61..8e11d177428 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -49,6 +49,7 @@ const { getTestSuiteCommonTags, TEST_STATUS, TEST_SKIPPED_BY_ITR, + TEST_ITR_SKIPPING_ENABLED, ITR_CORRELATION_ID, TEST_SOURCE_FILE, TEST_LEVEL_EVENT_TYPES, @@ -64,7 +65,11 @@ const { getModifiedFilesFromDiff, getPullRequestBaseBranch, getSessionRequestErrorTags, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, + getSessionItrSkippingEnabledTags, TEST_IS_TEST_FRAMEWORK_WORKER, TEST_IS_NEW, TEST_IS_RUM_ACTIVE, @@ -74,6 +79,7 @@ const { TEST_IS_MODIFIED, TEST_IS_RETRY, TEST_RETRY_REASON, + DD_CAPABILITIES_TEST_IMPACT_ANALYSIS, } = require('./util/test') const FRAMEWORK_TO_TRIMMED_COMMAND = { @@ -99,6 +105,21 @@ const TEST_FRAMEWORKS_TO_SKIP_GIT_METADATA_EXTRACTION = new Set([ 'cucumber', ]) +function setItrSkippingEnabledTagFromLibraryConfig (plugin, frameworkVersion) { + const libraryCapabilitiesTags = getLibraryCapabilitiesTags(plugin.constructor.id, frameworkVersion) + + if (!libraryCapabilitiesTags[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS] || + !plugin.libraryConfig || + !plugin.testSessionSpan || + !plugin.testModuleSpan) { + return + } + + const skippingEnabled = plugin.libraryConfig.isSuitesSkippingEnabled ? 'true' : 'false' + plugin.testSessionSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled) + plugin.testModuleSpan.setTag(TEST_ITR_SKIPPING_ENABLED, skippingEnabled) +} + function getTestSuiteLevelVisibilityTags (testSuiteSpan, testFramework) { const testSuiteSpanContext = testSuiteSpan.context() @@ -134,9 +155,10 @@ module.exports = class CiPlugin extends Plugin { this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => { if (err) { log.error('Library configuration could not be fetched. %s', err.message) - this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR, err) + this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, err) } else { this.libraryConfig = libraryConfig + setItrSkippingEnabledTagFromLibraryConfig(this, frameworkVersion) } const requestErrorTags = this.testSessionSpan @@ -165,6 +187,7 @@ module.exports = class CiPlugin extends Plugin { this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => { if (err) { log.error('Skippable suites could not be fetched. %s', err.message) + this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, err) } else { this.itrCorrelationId = itrCorrelationId } @@ -225,6 +248,7 @@ module.exports = class CiPlugin extends Plugin { }, integrationName: this.constructor.id, }) + setItrSkippingEnabledTagFromLibraryConfig(this, frameworkVersion) // only for vitest // These are added for the worker threads to use if (this.constructor.id === 'vitest') { @@ -246,6 +270,7 @@ module.exports = class CiPlugin extends Plugin { const testSuiteMetadata = { ...getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, this.constructor.id), ...getSessionRequestErrorTags(this.testSessionSpan), + ...getSessionItrSkippingEnabledTags(this.testSessionSpan), } if (this.itrCorrelationId) { testSuiteMetadata[ITR_CORRELATION_ID] = this.itrCorrelationId @@ -277,6 +302,7 @@ module.exports = class CiPlugin extends Plugin { this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => { if (err) { log.error('Known tests could not be fetched. %s', err.message) + this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, err) if (this.libraryConfig) { this.libraryConfig.isEarlyFlakeDetectionEnabled = false this.libraryConfig.isKnownTestsEnabled = false @@ -297,6 +323,7 @@ module.exports = class CiPlugin extends Plugin { this.tracer._exporter.getTestManagementTests(this.testConfiguration, (err, testManagementTests) => { if (err) { log.error('Test management tests could not be fetched. %s', err.message) + this._addRequestErrorTag(DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, err) if (this.libraryConfig) { this.libraryConfig.isTestManagementEnabled = false } @@ -342,6 +369,9 @@ module.exports = class CiPlugin extends Plugin { if (span.name?.startsWith(`${this.constructor.id}.`)) { span.meta[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true' + if (span.name === `${this.constructor.id}.test` || span.name === `${this.constructor.id}.test_suite`) { + Object.assign(span.meta, getSessionItrSkippingEnabledTags(this.testSessionSpan)) + } // augment with git information (since it will not be available in the worker) for (const key in this.testEnvironmentMetadata) { // CAREFUL: this bypasses the metadata/metrics distinction @@ -372,7 +402,8 @@ module.exports = class CiPlugin extends Plugin { // Jest and Vitest worker test spans are serialized in the worker and may not include // request error tags; add them from the session span in the main process. - if ((span.name === 'jest.test' || span.name === 'vitest.test') && this.testSessionSpan) { + if ((span.name === 'jest.test' || span.name === 'vitest.test' || span.name === 'vitest.test_suite') && + this.testSessionSpan) { Object.assign(span.meta, getSessionRequestErrorTags(this.testSessionSpan)) } } @@ -449,13 +480,16 @@ module.exports = class CiPlugin extends Plugin { * Adds a hidden _dd tag to the test session span when a test-optimization request fails. * If the session span does not exist yet (e.g. library-configuration failed before session:start), * the tag is queued and applied when the span is created. - * @param {string} tag - Tag name (e.g. DD_CI_LIBRARY_CONFIGURATION_ERROR) + * @param {string} tag - Tag name (e.g. DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS) * @param {Error} err - Request error */ _addRequestErrorTag (tag, err) { const value = 'true' if (this.testSessionSpan) { this.testSessionSpan.setTag(tag, value) + if (this.testModuleSpan) { + this.testModuleSpan.setTag(tag, value) + } } else { this._pendingRequestErrorTags.push({ tag, value }) } @@ -469,6 +503,15 @@ module.exports = class CiPlugin extends Plugin { return getSessionRequestErrorTags(this.testSessionSpan) } + /** + * Returns ITR skipping-enabled tags from the test session span for propagation to child events. + * + * @returns {Record} + */ + getSessionItrSkippingEnabledTags () { + return getSessionItrSkippingEnabledTags(this.testSessionSpan) + } + /** * @param {import('../config/config-base')} config - Tracer configuration * @param {boolean} shouldGetEnvironmentData - Whether to get environment data @@ -596,6 +639,8 @@ module.exports = class CiPlugin extends Plugin { } } + Object.assign(testTags, getSessionItrSkippingEnabledTags(this.testSessionSpan)) + this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'test', { hasCodeOwners: !!codeOwners }) const testSpan = this.tracer diff --git a/packages/dd-trace/src/plugins/database.js b/packages/dd-trace/src/plugins/database.js index 9c38ea0306a..3ee4555d105 100644 --- a/packages/dd-trace/src/plugins/database.js +++ b/packages/dd-trace/src/plugins/database.js @@ -1,13 +1,47 @@ 'use strict' +const { LRUCache } = require('../../../../vendor/dist/lru-cache') const { PEER_SERVICE_KEY, PEER_SERVICE_SOURCE_KEY } = require('../constants') const propagationHash = require('../propagation-hash') const StoragePlugin = require('./storage') +// Unreserved RFC 3986 set that `encodeURIComponent` leaves untouched (a conservative subset: +// `! * ' ( )` are also untouched but rarely appear in db / host / service names so we skip them). +const SAFE_ENCODE_RE = /^[\w\-.~]*$/ + +// Cap `#dbmPrefixCache` so a high-cardinality `db.name` (MongoDB sets it to +// `${database}.${collection}`) cannot grow the map without bound. Steady-state working sets +// fit well below the cap; cache misses cost a few hundred nanoseconds, so an evicted entry +// rebuilds cheaply on the next query. +const DBM_PREFIX_CACHE_MAX = 256 + class DatabasePlugin extends StoragePlugin { static operation = 'query' static peerServicePrecursors = ['db.name'] + // dde / ddps / ddpv are tracer-process constants. They are baked in at `configure()` time as + // two pre-templated fragments — `dbmEnvFragment` splices between dddbs and ddh; `dbmEndFragment` + // trails ddh — so per-query work shrinks to encoding dddb / dddbs / ddh and concatenating. + #dbmEnvFragment + #dbmEndFragment + // Cache the rendered prefix per `${db.name}\0${out.host}\0${dbmService}`. The triple is + // connection-stable for a real workload, so the steady state is one `LRUCache.get` plus the + // optional per-call `,ddprs='...'` suffix. + #dbmPrefixCache = new LRUCache({ max: DBM_PREFIX_CACHE_MAX }) + + /** + * @override + * @param {boolean | import('../config/config-base') & {enabled: boolean}} config + */ + configure (config) { + super.configure(config) + // Match the previous shape exactly: `dde` is `encode`d; `ddps` / `ddpv` are template-literal + // coerced so `undefined` renders as the literal `'undefined'` the way the original did. + this.#dbmEnvFragment = `,dde='${encode(this.tracer._env)}',` + this.#dbmEndFragment = `,ddps='${this.tracer._service ?? ''}',ddpv='${this.tracer._version}'` + this.#dbmPrefixCache.clear() + } + /** * @param {string} serviceName * @param {import('../../../..').Span} span @@ -16,20 +50,21 @@ class DatabasePlugin extends StoragePlugin { */ #createDBMPropagationCommentService (serviceName, span, peerData) { const spanTags = span.context()._tags - const encodedDddb = encode(spanTags['db.name']) - const encodedDddbs = encode(serviceName) - const encodedDde = encode(this.tracer._env) - const encodedDdh = encode(spanTags['out.host']) - const encodedDdps = this.tracer._service ?? '' - const encodedDdpv = this.tracer._version - - let dbmComment = `dddb='${encodedDddb}',dddbs='${encodedDddbs}',dde='${encodedDde}',ddh='${encodedDdh}',` + - `ddps='${encodedDdps}',ddpv='${encodedDdpv}'` + const dddb = spanTags['db.name'] + const ddh = spanTags['out.host'] + const cacheKey = `${dddb ?? ''}\0${ddh ?? ''}\0${serviceName ?? ''}` + + let prefix = this.#dbmPrefixCache.get(cacheKey) + if (prefix === undefined) { + prefix = `dddb='${encode(dddb)}',dddbs='${encode(serviceName)}'${this.#dbmEnvFragment}` + + `ddh='${encode(ddh)}'${this.#dbmEndFragment}` + this.#dbmPrefixCache.set(cacheKey, prefix) + } if (peerData !== undefined && peerData[PEER_SERVICE_SOURCE_KEY] === PEER_SERVICE_KEY) { - dbmComment += `,ddprs='${encode(peerData[PEER_SERVICE_KEY])}'` + return `${prefix},ddprs='${encode(peerData[PEER_SERVICE_KEY])}'` } - return dbmComment + return prefix } /** @@ -118,6 +153,13 @@ class DatabasePlugin extends StoragePlugin { } } -const encode = value => value ? encodeURIComponent(value) : '' +/** + * @param {string | number | undefined | null} value + * @returns {string} + */ +function encode (value) { + if (!value) return '' + return SAFE_ENCODE_RE.test(value) ? value : encodeURIComponent(value) +} module.exports = DatabasePlugin diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index d6c2ef7df70..919ddb583cc 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -49,6 +49,7 @@ const plugins = { get dns () { return require('../../../datadog-plugin-dns/src') }, get 'dd-trace-api' () { return require('../../../datadog-plugin-dd-trace-api/src') }, get elasticsearch () { return require('../../../datadog-plugin-elasticsearch/src') }, + get electron () { return require('../../../datadog-plugin-electron/src') }, get express () { return require('../../../datadog-plugin-express/src') }, get fastify () { return require('../../../datadog-plugin-fastify/src') }, get 'find-my-way' () { return require('../../../datadog-plugin-find-my-way/src') }, diff --git a/packages/dd-trace/src/plugins/plugin.js b/packages/dd-trace/src/plugins/plugin.js index 040e7bf689c..1495429b33a 100644 --- a/packages/dd-trace/src/plugins/plugin.js +++ b/packages/dd-trace/src/plugins/plugin.js @@ -160,14 +160,12 @@ module.exports = class Plugin { /** * Enable or disable the plugin and (re)apply its configuration. * - * TODO: Remove the overloading with `enabled` and use the config object directly. - * - * @param {boolean | import('../config/config-base') & {enabled: boolean}} config Either a boolean to + * @param {boolean | Record & {enabled: boolean}} config Either a boolean to * enable/disable or a configuration object containing at least `{ enabled: boolean }`. */ configure (config) { if (typeof config === 'boolean') { - config = /** @type {import('../config/config-base') & {enabled: boolean}} */ ({ enabled: config }) + config = { enabled: config } } this.config = config if (config.enabled) { diff --git a/packages/dd-trace/src/plugins/util/ci.js b/packages/dd-trace/src/plugins/util/ci.js index 17b788d03a4..ef7cf9f3772 100644 --- a/packages/dd-trace/src/plugins/util/ci.js +++ b/packages/dd-trace/src/plugins/util/ci.js @@ -2,7 +2,8 @@ const { readFileSync, readdirSync, existsSync } = require('fs') const path = require('path') -const { getEnvironmentVariable, getEnvironmentVariables, getValueFromEnvSources } = require('../../config/helper') +const getConfig = require('../../config') +const { getEnvironmentVariable, getEnvironmentVariables } = require('../../config/helper') const { GIT_BRANCH, GIT_COMMIT_SHA, @@ -217,7 +218,7 @@ const githubWellKnownDiagnosticDirPatternsWin = ['C:/actions-runner/*/_diag', 'C const githubJobIDRegex = /"job":\s*{[\s\S]*?"v"\s*:\s*(\d+)(?:\.0)?/ function getJobIDFromDiagFile () { - const runnerTemp = getValueFromEnvSources('RUNNER_TEMP') + const runnerTemp = getEnvironmentVariable('RUNNER_TEMP') if (!runnerTemp || !existsSync(runnerTemp)) { return null } const isWin = process.platform === 'win32' @@ -298,7 +299,7 @@ module.exports = { CHANGE_ID, CHANGE_TARGET, } = env - const DD_CUSTOM_TRACE_ID = getValueFromEnvSources('DD_CUSTOM_TRACE_ID') + const { DD_CUSTOM_TRACE_ID, DD_CUSTOM_PARENT_ID } = getConfig() tags = { [CI_PIPELINE_ID]: BUILD_TAG, @@ -308,7 +309,7 @@ module.exports = { [GIT_COMMIT_SHA]: JENKINS_GIT_COMMIT, [GIT_REPOSITORY_URL]: JENKINS_GIT_REPOSITORY_URL || JENKINS_GIT_REPOSITORY_URL_1, [CI_WORKSPACE_PATH]: WORKSPACE, - [CI_ENV_VARS]: JSON.stringify({ DD_CUSTOM_TRACE_ID }), + [CI_ENV_VARS]: JSON.stringify({ DD_CUSTOM_TRACE_ID, DD_CUSTOM_PARENT_ID }), [CI_NODE_NAME]: NODE_NAME, [PR_NUMBER]: CHANGE_ID, [GIT_PULL_REQUEST_BASE_BRANCH]: CHANGE_TARGET, @@ -738,11 +739,11 @@ module.exports = { }), [CI_NODE_NAME]: BUILDKITE_AGENT_ID, [CI_NODE_LABELS]: JSON.stringify(extraTags), - [PR_NUMBER]: BUILDKITE_PULL_REQUEST, [CI_JOB_ID]: BUILDKITE_JOB_ID, } - if (BUILDKITE_PULL_REQUEST) { + if (BUILDKITE_PULL_REQUEST && BUILDKITE_PULL_REQUEST !== 'false') { + tags[PR_NUMBER] = BUILDKITE_PULL_REQUEST tags[GIT_PULL_REQUEST_BASE_BRANCH] = BUILDKITE_PULL_REQUEST_BASE_BRANCH } } @@ -868,8 +869,7 @@ module.exports = { } if (env.CODEBUILD_INITIATOR?.startsWith('codepipeline/')) { - const DD_ACTION_EXECUTION_ID = getValueFromEnvSources('DD_ACTION_EXECUTION_ID') - const DD_PIPELINE_EXECUTION_ID = getValueFromEnvSources('DD_PIPELINE_EXECUTION_ID') + const { DD_ACTION_EXECUTION_ID, DD_PIPELINE_EXECUTION_ID } = getConfig() tags = { [CI_PROVIDER_NAME]: 'awscodepipeline', [CI_PIPELINE_ID]: DD_PIPELINE_EXECUTION_ID, diff --git a/packages/dd-trace/src/plugins/util/git-cache.js b/packages/dd-trace/src/plugins/util/git-cache.js index 5029aa6bdde..b1b9333d87a 100644 --- a/packages/dd-trace/src/plugins/util/git-cache.js +++ b/packages/dd-trace/src/plugins/util/git-cache.js @@ -7,24 +7,31 @@ const crypto = require('crypto') const cp = require('child_process') const log = require('../../log') -const { getValueFromEnvSources } = require('../../config/helper') -const { isTrue } = require('../../util') +const getConfig = require('../../config') -let isGitEnabled = isTrue(getValueFromEnvSources('DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED')) -const GIT_CACHE_DIR = getValueFromEnvSources('DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR') || - path.join(os.tmpdir(), 'dd-trace-git-cache') +let isGitEnabled +let gitCacheDir function ensureCacheDir () { + if (isGitEnabled === undefined) { + const config = getConfig() + isGitEnabled = config.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED + // TODO: Move the default to config/index.js applyCalculated + // when using the config singleton. + gitCacheDir = config.DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR || + path.join(os.tmpdir(), 'dd-trace-git-cache') + } + if (!isGitEnabled) return false try { - if (fs.existsSync(GIT_CACHE_DIR)) { - const stats = fs.statSync(GIT_CACHE_DIR) + if (fs.existsSync(gitCacheDir)) { + const stats = fs.statSync(gitCacheDir) if (!stats.isDirectory()) { - throw new Error(`Cache directory path exists but is not a directory: ${GIT_CACHE_DIR}`) + throw new Error(`Cache directory path exists but is not a directory: ${gitCacheDir}`) } } else { - fs.mkdirSync(GIT_CACHE_DIR, { recursive: true }) + fs.mkdirSync(gitCacheDir, { recursive: true }) } return true } catch (err) { @@ -34,9 +41,6 @@ function ensureCacheDir () { } } -// Initialize cache directory at module load time -ensureCacheDir() - function getCacheKey (cmd, flags) { // Create a hash of the command and flags to use as cache key const commandString = `${cmd} ${flags.join(' ')}` @@ -44,11 +48,12 @@ function getCacheKey (cmd, flags) { } function getCacheFilePath (cacheKey) { - return path.join(GIT_CACHE_DIR, `${cacheKey}.cache`) + ensureCacheDir() + return path.join(gitCacheDir, `${cacheKey}.cache`) } function getCache (cacheKey) { - if (!isGitEnabled) return null + if (!ensureCacheDir()) return null try { const cacheFilePath = getCacheFilePath(cacheKey) @@ -63,9 +68,6 @@ function getCache (cacheKey) { } function setCache (cacheKey, result) { - if (!isGitEnabled) return - - // Ensure cache directory exists if (!ensureCacheDir()) return try { @@ -80,7 +82,7 @@ function cachedExec (cmd, flags, options) { if (options === undefined) { options = { stdio: 'pipe' } } - if (!isGitEnabled) { + if (!ensureCacheDir()) { return cp.execFileSync(cmd, flags, options) } const cacheKey = getCacheKey(cmd, flags) diff --git a/packages/dd-trace/src/plugins/util/stacktrace.js b/packages/dd-trace/src/plugins/util/stacktrace.js index 20303bf1915..17b7e28cc59 100644 --- a/packages/dd-trace/src/plugins/util/stacktrace.js +++ b/packages/dd-trace/src/plugins/util/stacktrace.js @@ -14,7 +14,7 @@ const NODE_MODULES_PATTERN_START = `node_modules${sep}` * In production, these frames are already filtered by isNodeModulesFrame. */ const SHOULD_FILTER_DD_TRACE_INSTRUMENTAION = __filename.endsWith( - join(sep, 'dd-trace-js', 'packages', 'dd-trace', 'src', 'plugins', 'util', 'stacktrace.js') + join('packages', 'dd-trace', 'src', 'plugins', 'util', 'stacktrace.js') ) module.exports = { @@ -46,7 +46,7 @@ function getCallSites (constructorOpt) { * Get stack trace of user-land frames. * * @param {string} stack - The stack trace to parse - * @param {number} [limit=Infinity] - The maximum number of frames to return + * @param {number} [limit] - The maximum number of frames to return * @returns {StackFrame[]} - A list of stack frames from user-land code */ function parseUserLandFrames (stack, limit = Infinity) { diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index faa68997115..f71f6beb6d4 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -163,8 +163,12 @@ const DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE = '_dd.library_capabilities.test_m const DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX = '_dd.library_capabilities.test_management.attempt_to_fix' const DD_CAPABILITIES_FAILED_TEST_REPLAY = '_dd.library_capabilities.failed_test_replay' -// Library configuration request error tag -const DD_CI_LIBRARY_CONFIGURATION_ERROR = '_dd.ci.library_configuration_error' +// Library configuration request error tags +const DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS = '_dd.ci.library_configuration_error.settings' +const DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS = '_dd.ci.library_configuration_error.skippable_tests' +const DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS = '_dd.ci.library_configuration_error.known_tests' +const DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS = + '_dd.ci.library_configuration_error.test_management_tests' const UNSUPPORTED_TIA_FRAMEWORKS = new Set(['playwright', 'vitest']) const MINIMUM_FRAMEWORK_VERSION_FOR_EFD = { @@ -230,10 +234,34 @@ const BASE_LIKE_BRANCH_FILTER = /^(main|master|preprod|prod|dev|development|trun */ function getSessionRequestErrorTags (sessionSpan) { const tags = sessionSpan?.context()._tags + const sessionRequestErrorTags = {} if (!tags || typeof tags !== 'object') return {} - if (tags[DD_CI_LIBRARY_CONFIGURATION_ERROR] === 'true') { + if (tags[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS] === 'true') { + sessionRequestErrorTags[DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS] = 'true' + } + if (tags[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS] === 'true') { + sessionRequestErrorTags[DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS] = 'true' + } + if (tags[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS] === 'true') { + sessionRequestErrorTags[DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS] = 'true' + } + if (tags[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS] === 'true') { + sessionRequestErrorTags[DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS] = 'true' + } + return sessionRequestErrorTags +} + +/** + * Returns ITR skipping-enabled tags from a test session span for propagation to child events. + * @param {{ context: () => { _tags?: Record } } | undefined} sessionSpan + * @returns {Record} + */ +function getSessionItrSkippingEnabledTags (sessionSpan) { + const tags = sessionSpan?.context()._tags + if (!tags || typeof tags !== 'object') return {} + if (tags[TEST_ITR_SKIPPING_ENABLED] !== undefined) { return { - [DD_CI_LIBRARY_CONFIGURATION_ERROR]: 'true', + [TEST_ITR_SKIPPING_ENABLED]: tags[TEST_ITR_SKIPPING_ENABLED], } } return {} @@ -350,7 +378,11 @@ module.exports = { TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, getLibraryCapabilitiesTags, getSessionRequestErrorTags, - DD_CI_LIBRARY_CONFIGURATION_ERROR, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SETTINGS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_SKIPPABLE_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_KNOWN_TESTS, + DD_CI_LIBRARY_CONFIGURATION_ERROR_TEST_MANAGEMENT_TESTS, + getSessionItrSkippingEnabledTags, checkShaDiscrepancies, getPullRequestDiff, getPullRequestBaseBranch, diff --git a/packages/dd-trace/src/plugins/util/user-provided-git.js b/packages/dd-trace/src/plugins/util/user-provided-git.js index 1b98e32963e..25c40e3c927 100644 --- a/packages/dd-trace/src/plugins/util/user-provided-git.js +++ b/packages/dd-trace/src/plugins/util/user-provided-git.js @@ -1,6 +1,6 @@ 'use strict' -const { getValueFromEnvSources } = require('../../config/helper') +const getConfig = require('../../config') const { GIT_COMMIT_SHA, GIT_BRANCH, @@ -46,20 +46,22 @@ function validateGitCommitSha (gitCommitSha) { } function getUserProviderGitMetadata () { - const DD_GIT_COMMIT_SHA = getValueFromEnvSources('DD_GIT_COMMIT_SHA') - const DD_GIT_BRANCH = getValueFromEnvSources('DD_GIT_BRANCH') - const DD_GIT_REPOSITORY_URL = getValueFromEnvSources('DD_GIT_REPOSITORY_URL') - const DD_GIT_TAG = getValueFromEnvSources('DD_GIT_TAG') - const DD_GIT_COMMIT_MESSAGE = getValueFromEnvSources('DD_GIT_COMMIT_MESSAGE') - const DD_GIT_COMMIT_COMMITTER_NAME = getValueFromEnvSources('DD_GIT_COMMIT_COMMITTER_NAME') - const DD_GIT_COMMIT_COMMITTER_EMAIL = getValueFromEnvSources('DD_GIT_COMMIT_COMMITTER_EMAIL') - const DD_GIT_COMMIT_COMMITTER_DATE = getValueFromEnvSources('DD_GIT_COMMIT_COMMITTER_DATE') - const DD_GIT_COMMIT_AUTHOR_NAME = getValueFromEnvSources('DD_GIT_COMMIT_AUTHOR_NAME') - const DD_GIT_COMMIT_AUTHOR_EMAIL = getValueFromEnvSources('DD_GIT_COMMIT_AUTHOR_EMAIL') - const DD_GIT_COMMIT_AUTHOR_DATE = getValueFromEnvSources('DD_GIT_COMMIT_AUTHOR_DATE') - const DD_GIT_PULL_REQUEST_BASE_BRANCH = getValueFromEnvSources('DD_GIT_PULL_REQUEST_BASE_BRANCH') - const DD_GIT_PULL_REQUEST_BASE_BRANCH_SHA = getValueFromEnvSources('DD_GIT_PULL_REQUEST_BASE_BRANCH_SHA') - const DD_GIT_COMMIT_HEAD_SHA = getValueFromEnvSources('DD_GIT_COMMIT_HEAD_SHA') + const { + DD_GIT_COMMIT_SHA, + DD_GIT_BRANCH, + DD_GIT_REPOSITORY_URL, + DD_GIT_TAG, + DD_GIT_COMMIT_MESSAGE, + DD_GIT_COMMIT_COMMITTER_NAME, + DD_GIT_COMMIT_COMMITTER_EMAIL, + DD_GIT_COMMIT_COMMITTER_DATE, + DD_GIT_COMMIT_AUTHOR_NAME, + DD_GIT_COMMIT_AUTHOR_EMAIL, + DD_GIT_COMMIT_AUTHOR_DATE, + DD_GIT_PULL_REQUEST_BASE_BRANCH, + DD_GIT_PULL_REQUEST_BASE_BRANCH_SHA, + DD_GIT_COMMIT_HEAD_SHA, + } = getConfig() const branch = normalizeRef(DD_GIT_BRANCH) let tag = normalizeRef(DD_GIT_TAG) diff --git a/packages/dd-trace/src/priority_sampler.js b/packages/dd-trace/src/priority_sampler.js index 94a0af5a239..983be7376a7 100644 --- a/packages/dd-trace/src/priority_sampler.js +++ b/packages/dd-trace/src/priority_sampler.js @@ -110,7 +110,7 @@ class PrioritySampler { * Assigns a sampling priority to a span if not already set. * * @param {DatadogSpan} span - * @param {boolean} [auto=true] - Whether to use automatic sampling if no manual tags are present. + * @param {boolean} [auto] - Whether to use automatic sampling if no manual tags are present. * @returns {void} */ sample (span, auto = true) { diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 12620149be6..64509eff700 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -260,7 +260,7 @@ class Profiler extends EventEmitter { this.#lastStart = start if (!this.#timer || timeout !== this._timeoutInterval) { this.#timer = setTimeout(() => this._collect(snapshotKinds.PERIODIC), timeout) - this.#timer.unref() + this.#timer.unref?.() } else { this.#timer.refresh() } diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index cc8614ed14a..e66e7df0cdb 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -223,7 +223,7 @@ class NativeWallProfiler { asyncContextsLiveGauge.mark(totalAsyncContextCount) asyncContextsUsedGauge.mark(usedAsyncContextCount) }, this.#telemetryHeartbeatIntervalMillis) - this._contextCountGaugeUpdater.unref() + this._contextCountGaugeUpdater.unref?.() } #enter () { diff --git a/packages/dd-trace/src/profiling/ssi-heuristics.js b/packages/dd-trace/src/profiling/ssi-heuristics.js index e8960b47ed2..4f2abd6876d 100644 --- a/packages/dd-trace/src/profiling/ssi-heuristics.js +++ b/packages/dd-trace/src/profiling/ssi-heuristics.js @@ -39,7 +39,7 @@ class SSIHeuristics { setTimeout(() => { this.shortLived = false this._maybeTriggered() - }, this.longLivedThreshold).unref() + }, this.longLivedThreshold).unref?.() this._onSpanCreated = this._onSpanCreated.bind(this) dc.subscribe('dd-trace:span:start', this._onSpanCreated) diff --git a/packages/dd-trace/src/rate_limiter.js b/packages/dd-trace/src/rate_limiter.js index e1ea1f46a94..7a8f4ebb2f1 100644 --- a/packages/dd-trace/src/rate_limiter.js +++ b/packages/dd-trace/src/rate_limiter.js @@ -5,7 +5,7 @@ const limiter = require('../../../vendor/dist/limiter') class RateLimiter { /** * @param {number} rateLimit - Allowed units per interval. Negative means unlimited, 0 disables. - * @param {'second'|'minute'|'hour'|'day'} [interval='second'] - Time window for the limiter. + * @param {'second'|'minute'|'hour'|'day'} [interval] - Time window for the limiter. */ constructor (rateLimit, interval = 'second') { // TODO: Change rateLimit to integers. Right now these are sometimes strings, sometimes numbers. diff --git a/packages/dd-trace/src/remote_config/scheduler.js b/packages/dd-trace/src/remote_config/scheduler.js index 80fbab33333..500565d88f5 100644 --- a/packages/dd-trace/src/remote_config/scheduler.js +++ b/packages/dd-trace/src/remote_config/scheduler.js @@ -17,7 +17,7 @@ class Scheduler { runAfterDelay (interval = this._interval) { this._timer = setTimeout(this._callback, interval, () => this.runAfterDelay()) - this._timer.unref() + this._timer.unref?.() } stop () { diff --git a/packages/dd-trace/src/ritm.js b/packages/dd-trace/src/ritm.js index 6038c99c83c..afb2d0328bb 100644 --- a/packages/dd-trace/src/ritm.js +++ b/packages/dd-trace/src/ritm.js @@ -31,6 +31,7 @@ const builtinModules = new Set(Module.builtinModules.map(stripNodePrefix)) function isBuiltinModuleName (name) { if (typeof name !== 'string') return false + if (name === 'electron') return true return builtinModules.has(stripNodePrefix(name)) } @@ -101,7 +102,7 @@ function Hook (modules, options, onrequire) { if (cache[moduleId]) { // require.cache was potentially altered externally const cacheEntry = require.cache[filename] - if (cacheEntry && cacheEntry.exports !== cache[filename].original) { + if (cacheEntry && cacheEntry.exports !== cache[moduleId].original) { return cacheEntry.exports } diff --git a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js index 66f402409a8..10875b95c4a 100644 --- a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js @@ -8,14 +8,9 @@ const process = require('process') const { performance, PerformanceObserver, monitorEventLoopDelay } = require('perf_hooks') const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd') const log = require('../log') -const { getValueFromEnvSources } = require('../config/helper') const { NODE_MAJOR } = require('../../../../version') const processTags = require('../process-tags') -// TODO: This environment variable may not be changed, since the agent expects a flush every ten seconds. -// It is only a variable for testing. Think about alternatives. -const DD_RUNTIME_METRICS_FLUSH_INTERVAL = getValueFromEnvSources('DD_RUNTIME_METRICS_FLUSH_INTERVAL') ?? '10000' -const INTERVAL = Number.parseInt(DD_RUNTIME_METRICS_FLUSH_INTERVAL, 10) const eventLoopDelayResolution = 4 @@ -40,6 +35,8 @@ module.exports = { */ start (config) { this.stop() + // The agent expects a flush every ten seconds, so this is for tests only. + const flushIntervalMs = config.DD_RUNTIME_METRICS_FLUSH_INTERVAL const clientConfig = DogStatsDClient.generateClientConfig(config) if (config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED) { @@ -81,7 +78,7 @@ module.exports = { captureNativeMetrics(trackEventLoop, trackGc) captureCommonMetrics(trackEventLoop) client.flush() - }, INTERVAL) + }, flushIntervalMs) } else { lastCpuUsage = process.cpuUsage() @@ -103,10 +100,10 @@ module.exports = { captureEventLoopDelay() } client.flush() - }, INTERVAL) + }, flushIntervalMs) } - interval.unref() + interval.unref?.() }, stop () { diff --git a/packages/dd-trace/src/serverless.js b/packages/dd-trace/src/serverless.js index 1b04348c9c9..56d099c59aa 100644 --- a/packages/dd-trace/src/serverless.js +++ b/packages/dd-trace/src/serverless.js @@ -17,10 +17,13 @@ function getIsGCPFunction () { /** * Enable GCP Pub/Sub PUSH subscription tracing for Cloud Run (K_SERVICE present). * PUSH: GCP sends HTTP POST requests to the service with message data in headers. + * + * Stays on the env helper to avoid closing the + * `config -> serverless -> config` import cycle. */ function enableGCPPubSubPushSubscription () { - const isGCPPubSubPushSubscriptionEnabled = getValueFromEnvSources('DD_TRACE_GCP_PUBSUB_PUSH_ENABLED') - return getEnvironmentVariable('K_SERVICE') !== undefined && !isFalse(isGCPPubSubPushSubscriptionEnabled) + return getEnvironmentVariable('K_SERVICE') !== undefined && + !isFalse(getValueFromEnvSources('DD_TRACE_GCP_PUBSUB_PUSH_ENABLED')) } function getIsAzureFunction () { diff --git a/packages/dd-trace/src/service-naming/schemas/v0/messaging.js b/packages/dd-trace/src/service-naming/schemas/v0/messaging.js index 8d5ec3d1a71..3dd5eaa6435 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/messaging.js @@ -32,6 +32,14 @@ const messaging = { serviceName: ({ tracerService }) => `${tracerService}-azure-service-bus`, serviceSource: integrationSource('azure-service-bus'), }, + 'electron:ipc:main:send': { + opName: () => 'electron.main.send', + serviceName: identityService, + }, + 'electron:ipc:renderer:send': { + opName: () => 'electron.renderer.send', + serviceName: identityService, + }, 'google-cloud-pubsub': { opName: () => 'pubsub.request', serviceName: ({ tracerService }) => `${tracerService}-pubsub`, @@ -79,6 +87,18 @@ const messaging = { serviceName: amqpServiceName, serviceSource: integrationSource('amqp'), }, + 'electron:ipc:main:receive': { + opName: () => 'electron.main.receive', + serviceName: identityService, + }, + 'electron:ipc:main:handle': { + opName: () => 'electron.main.handle', + serviceName: identityService, + }, + 'electron:ipc:renderer:receive': { + opName: () => 'electron.renderer.receive', + serviceName: identityService, + }, 'google-cloud-pubsub': { opName: () => 'pubsub.receive', serviceName: ({ tracerService }) => `${tracerService}-pubsub`, diff --git a/packages/dd-trace/src/service-naming/schemas/v0/web.js b/packages/dd-trace/src/service-naming/schemas/v0/web.js index 89e379772b2..2160b04508f 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/web.js @@ -54,6 +54,10 @@ const web = { serviceName: httpPluginClientService, serviceSource: optionServiceSource, }, + 'electron:net:request': { + opName: () => 'http.request', + serviceName: httpPluginClientService, + }, }, server: { 'apollo.gateway.request': { diff --git a/packages/dd-trace/src/service-naming/schemas/v1/messaging.js b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js index 234ab6ecd99..0e303e87ce5 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js @@ -24,6 +24,14 @@ const messaging = { opName: () => 'azure.eventhubs.send', serviceName: identityService, }, + 'electron:ipc:main:send': { + opName: () => 'electron.main.send', + serviceName: identityService, + }, + 'electron:ipc:renderer:send': { + opName: () => 'electron.renderer.send', + serviceName: identityService, + }, 'google-cloud-pubsub': { opName: () => 'gcp.pubsub.send', serviceName: identityService, @@ -53,6 +61,18 @@ const messaging = { consumer: { amqplib: amqpInbound, amqp10: amqpInbound, + 'electron:ipc:main:receive': { + opName: () => 'electron.main.receive', + serviceName: identityService, + }, + 'electron:ipc:main:handle': { + opName: () => 'electron.main.handle', + serviceName: identityService, + }, + 'electron:ipc:renderer:receive': { + opName: () => 'electron.renderer.receive', + serviceName: identityService, + }, 'google-cloud-pubsub': { opName: () => 'gcp.pubsub.process', serviceName: identityService, diff --git a/packages/dd-trace/src/service-naming/schemas/v1/web.js b/packages/dd-trace/src/service-naming/schemas/v1/web.js index c6d09566481..e03e96ec417 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/web.js @@ -48,6 +48,10 @@ const web = { opName: () => 'undici.request', serviceName: httpPluginClientService, }, + 'electron:net:request': { + opName: () => 'http.client.request', + serviceName: httpPluginClientService, + }, }, server: { 'apollo.gateway.request': { diff --git a/packages/dd-trace/src/span_stats.js b/packages/dd-trace/src/span_stats.js index 6b62ac1f337..f57e231fd73 100644 --- a/packages/dd-trace/src/span_stats.js +++ b/packages/dd-trace/src/span_stats.js @@ -165,7 +165,7 @@ class SpanStatsProcessor { if (this.enabled) { this.timer = setInterval(this.onInterval.bind(this), interval * 1e3) - this.timer.unref() + this.timer.unref?.() } } diff --git a/packages/dd-trace/src/telemetry/dependencies.js b/packages/dd-trace/src/telemetry/dependencies.js index 86890681a6d..dbdac582324 100644 --- a/packages/dd-trace/src/telemetry/dependencies.js +++ b/packages/dd-trace/src/telemetry/dependencies.js @@ -75,7 +75,7 @@ function waitAndSend (config, application, host) { if (savedDependenciesToSend.size > 0) { waitAndSend(config, application, host) } - }).unref() + }).unref?.() } function loadAllTheLoadedModules () { diff --git a/packages/dd-trace/src/telemetry/endpoints.js b/packages/dd-trace/src/telemetry/endpoints.js index 11b887c17e6..cc3c8381789 100644 --- a/packages/dd-trace/src/telemetry/endpoints.js +++ b/packages/dd-trace/src/telemetry/endpoints.js @@ -63,7 +63,7 @@ function scheduleFlush () { flushScheduled = true // this used to be setImmediate() instead, but it was making the system test flaky // don't ask me why - setTimeout(flushAndSend).unref() + setTimeout(flushAndSend).unref?.() } /** diff --git a/packages/dd-trace/src/telemetry/telemetry.js b/packages/dd-trace/src/telemetry/telemetry.js index 90b6d59b70b..06aa8500dea 100644 --- a/packages/dd-trace/src/telemetry/telemetry.js +++ b/packages/dd-trace/src/telemetry/telemetry.js @@ -291,7 +291,7 @@ function heartbeat (config, application) { const { reqType, payload } = createPayload('app-heartbeat') sendData(config, application, host, reqType, payload, updateRetryData) - }, config.telemetry.heartbeatInterval).unref() + }, config.telemetry.heartbeatInterval).unref?.() } /** @param {import('../config/config-base')} config */ @@ -307,7 +307,7 @@ function extendedHeartbeat (config) { heartbeatFailedDependencies = [] } sendData(config, application, host, 'app-extended-heartbeat', appPayload) - }, config.telemetry.extendedHeartbeatInterval).unref() + }, config.telemetry.extendedHeartbeatInterval).unref?.() } /** diff --git a/packages/dd-trace/test/aiguard/index.spec.js b/packages/dd-trace/test/aiguard/index.spec.js index ea8f482d23d..d581ab99a65 100644 --- a/packages/dd-trace/test/aiguard/index.spec.js +++ b/packages/dd-trace/test/aiguard/index.spec.js @@ -14,10 +14,17 @@ const { assertObjectContains } = require('../../../../integration-tests/helpers' const tracerVersion = require('../../../../package.json').version const telemetryMetrics = require('../../src/telemetry/metrics') -const appsecNamespace = telemetryMetrics.manager.namespace('appsec') +const aiguardMetrics = telemetryMetrics.manager.namespace('ai_guard') const { USER_KEEP } = require('../../../../ext/priority') const { SAMPLING_MECHANISM_AI_GUARD, DECISION_MAKER_KEY } = require('../../src/constants') -const { AI_GUARD_EVENT_TAG_KEY } = require('../../src/aiguard/tags') +const { + EVENT_TAG_KEY, + SOURCE_SDK, + INTEGRATION_NONE, + ERROR_TYPE_CLIENT, + ERROR_TYPE_STATUS, + ERROR_TYPE_RESPONSE, +} = require('../../src/aiguard/tags') describe('AIGuard SDK', () => { const config = { @@ -79,10 +86,10 @@ describe('AIGuard SDK', () => { global.fetch = sinon.stub() inc = sinon.spy() - count = sinon.stub(appsecNamespace, 'count').returns({ + count = sinon.stub(aiguardMetrics, 'count').returns({ inc, }) - appsecNamespace.metrics.clear() + aiguardMetrics.metrics.clear() aiguard = new AIGuard(tracer, config) @@ -92,7 +99,7 @@ describe('AIGuard SDK', () => { afterEach(() => { global.fetch = originalFetch sinon.restore() - agent.close() + return agent.close() }) const mockFetch = (options) => { @@ -140,6 +147,8 @@ describe('AIGuard SDK', () => { }, { rejectFirst: true }) } + const sdkTags = { source: SOURCE_SDK, integration: INTEGRATION_NONE } + const assertTelemetry = (metric, tags) => { sinon.assert.calledWith(count, metric, tags) } @@ -171,7 +180,7 @@ describe('AIGuard SDK', () => { await rejects( () => aiguard.evaluate(messages, { block: true }), err => err.name === 'AIGuardAbortError' && err.reason === reason && err.tags === attributes.tags && - err.tagProbabilities === attributes.tag_probs && JSON.stringify(err.sds) === '[]' + err.tagProbabilities === attributes.tag_probs && JSON.stringify(err.sds) === '[]' ) } else { const evaluation = await aiguard.evaluate(messages, { block: true }) @@ -184,7 +193,7 @@ describe('AIGuard SDK', () => { assert.deepStrictEqual(evaluation.sds, []) } - assertTelemetry('ai_guard.requests', { error: false, action, block: shouldBlock }) + assertTelemetry('requests', { action, error: false, block: shouldBlock, ...sdkTags }) assertFetch(messages) await assertAIGuardSpan({ 'ai_guard.target': target, @@ -226,7 +235,7 @@ describe('AIGuard SDK', () => { assert.strictEqual(evaluation.action, 'DENY') } - assertTelemetry('ai_guard.requests', { error: false, action: 'DENY', block: shouldBlock }) + assertTelemetry('requests', { error: false, action: 'DENY', block: shouldBlock, ...sdkTags }) }) } @@ -334,7 +343,8 @@ describe('AIGuard SDK', () => { err.name === 'AIGuardClientError' && JSON.stringify(err.errors) === JSON.stringify(errors) ) - assertTelemetry('ai_guard.requests', { error: true }) + assertTelemetry('requests', { error: true, ...sdkTags }) + assertTelemetry('error', { type: ERROR_TYPE_STATUS, ...sdkTags }) assertFetch(toolCall) await assertAIGuardSpan({ 'ai_guard.target': 'tool', @@ -353,7 +363,8 @@ describe('AIGuard SDK', () => { err.name === 'AIGuardClientError' && err.message === 'Unexpected error calling AI Guard service: Boom!!!', ) - assertTelemetry('ai_guard.requests', { error: true }) + assertTelemetry('requests', { error: true, ...sdkTags }) + assertTelemetry('error', { type: ERROR_TYPE_CLIENT, ...sdkTags }) assertFetch(toolCall) await assertAIGuardSpan({ 'ai_guard.target': 'tool', @@ -369,7 +380,8 @@ describe('AIGuard SDK', () => { err => err.name === 'AIGuardClientError' ) - assertTelemetry('ai_guard.requests', { error: true }) + assertTelemetry('requests', { error: true, ...sdkTags }) + assertTelemetry('error', { type: ERROR_TYPE_RESPONSE, ...sdkTags }) assertFetch(toolCall) await assertAIGuardSpan({ 'ai_guard.target': 'tool', @@ -385,7 +397,8 @@ describe('AIGuard SDK', () => { err => err.name === 'AIGuardClientError' ) - assertTelemetry('ai_guard.requests', { error: true }) + assertTelemetry('requests', { error: true, ...sdkTags }) + assertTelemetry('error', { type: ERROR_TYPE_RESPONSE, ...sdkTags }) assertFetch(toolCall) await assertAIGuardSpan({ 'ai_guard.target': 'tool', @@ -412,7 +425,7 @@ describe('AIGuard SDK', () => { await aiguard.evaluate(messages) - assertTelemetry('ai_guard.truncated', { type: 'messages' }) + assertTelemetry('truncated', { type: 'messages', ...sdkTags }) assertFetch(messages) await assertAIGuardSpan( { 'ai_guard.target': 'prompt', 'ai_guard.action': 'ALLOW' }, @@ -430,7 +443,7 @@ describe('AIGuard SDK', () => { await aiguard.evaluate(messages) - assertTelemetry('ai_guard.truncated', { type: 'content' }) + assertTelemetry('truncated', { type: 'content', ...sdkTags }) assertFetch(messages) await assertAIGuardSpan( { 'ai_guard.target': 'prompt', 'ai_guard.action': 'ALLOW' }, @@ -480,9 +493,9 @@ describe('AIGuard SDK', () => { assert.ok(traces[0].length === 2, 'Trace should contain two spans root + ai_guard') for (const span of traces[0]) { if (span.name === 'root') { - assert.strictEqual(span.meta[AI_GUARD_EVENT_TAG_KEY], 'true') + assert.strictEqual(span.meta[EVENT_TAG_KEY], 'true') } else { - assert.ok(!Object.hasOwn(span.meta, AI_GUARD_EVENT_TAG_KEY)) + assert.ok(!Object.hasOwn(span.meta, EVENT_TAG_KEY)) } } }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js index 51624b957fc..e254e541337 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js @@ -130,11 +130,9 @@ describe('Hardcoded Password Analyzer', () => { beforeEach(() => { const tracer = require('../../../../') const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, }) iast.enable(config, tracer) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-secret-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-secret-analyzer.spec.js index 84ffec8c900..21e2c36839c 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-secret-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-secret-analyzer.spec.js @@ -95,11 +95,9 @@ describe('Hardcoded Secret Analyzer', () => { beforeEach(() => { const tracer = require('../../../../') const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, }) iast.enable(config, tracer) diff --git a/packages/dd-trace/test/appsec/iast/index.spec.js b/packages/dd-trace/test/appsec/iast/index.spec.js index d0db394abc8..553f59a6551 100644 --- a/packages/dd-trace/test/appsec/iast/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/index.spec.js @@ -52,11 +52,9 @@ describe('IAST Index', () => { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) }) @@ -125,11 +123,9 @@ describe('IAST Index', () => { let analyzers const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, }) diff --git a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js index f8ae2c048b3..25709f6487c 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -34,9 +34,7 @@ describe('Overhead controller', () => { oceContextKey = overheadController.OVERHEAD_CONTROLLER_CONTEXT_KEY const config = getConfigFresh({ - experimental: { - iast: true, - }, + iast: true, }) overheadController.configure(config.iast) }) @@ -442,12 +440,10 @@ describe('Overhead controller', () => { it('should detect vulnerabilities only in one if max concurrent is 1', (done) => { const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - maxConcurrentRequests: 1, - }, + iast: { + enabled: true, + requestSampling: 100, + maxConcurrentRequests: 1, }, }) iast.enable(config) @@ -494,13 +490,11 @@ describe('Overhead controller', () => { it('should detect vulnerabilities in both if max concurrent is 2', (done) => { const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - maxConcurrentRequests: 2, - deduplicationEnabled: false, - }, + iast: { + enabled: true, + requestSampling: 100, + maxConcurrentRequests: 2, + deduplicationEnabled: false, }, }) iast.enable(config) @@ -550,13 +544,11 @@ describe('Overhead controller', () => { // 3 in parallel => 2 detects - 1 not detects // on finish the first => launch 2 - should detect 1 more const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - maxConcurrentRequests: 2, - deduplicationEnabled: false, - }, + iast: { + enabled: true, + requestSampling: 100, + maxConcurrentRequests: 2, + deduplicationEnabled: false, }, }) iast.enable(config) @@ -622,12 +614,10 @@ describe('Overhead controller', () => { it('should add _dd.iast.enabled tag even when no vulnerability is detected', (done) => { const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - maxConcurrentRequests: 1, - }, + iast: { + enabled: true, + requestSampling: 100, + maxConcurrentRequests: 1, }, }) iast.enable(config) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/graphql.sources.test-utils.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/graphql.sources.test-utils.js index c1cbad11ff6..5159be5ca72 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/graphql.sources.test-utils.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/graphql.sources.test-utils.js @@ -71,11 +71,9 @@ function graphqlCommonTests (config) { describe('Graphql sources tests', () => { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) vulnerabilityReporter.clearCache() diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.cookie.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.cookie.plugin.spec.js index d83ffa666a0..dcd3d55dbb2 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.cookie.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.cookie.plugin.spec.js @@ -42,11 +42,9 @@ describe('Cookies sourcing with cookies', () => { function tests (config) { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js index 1c5ed720ca0..930798e1447 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js @@ -39,11 +39,9 @@ describe('URI sourcing with express', () => { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) @@ -116,11 +114,9 @@ describe('Path params sourcing with express', () => { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.fastify.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.fastify.plugin.spec.js index 3f002b86421..5e6e4611384 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.fastify.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.fastify.plugin.spec.js @@ -32,11 +32,9 @@ describe('URI sourcing with fastify', () => { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) @@ -90,11 +88,9 @@ describe('Path params sourcing with fastify', () => { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.headers.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.headers.spec.js index efdb8fb810e..fa99fe45287 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.headers.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.headers.spec.js @@ -30,11 +30,9 @@ describe('Headers sourcing', () => { function tests (config) { beforeEach(() => { iast.enable(getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, })) }) diff --git a/packages/dd-trace/test/appsec/iast/utils.js b/packages/dd-trace/test/appsec/iast/utils.js index 7b48e7ec65c..5f0591cdf33 100644 --- a/packages/dd-trace/test/appsec/iast/utils.js +++ b/packages/dd-trace/test/appsec/iast/utils.js @@ -78,11 +78,9 @@ function testOutsideRequestHasVulnerability (fnToTest, vulnerability, plugins, t beforeEach(() => { const tracer = require('../../..') const config = getConfigFresh({ - experimental: { - iast: { - enabled: true, - requestSampling: 100, - }, + iast: { + enabled: true, + requestSampling: 100, }, }) iast.enable(config, tracer) diff --git a/packages/dd-trace/test/azure_metadata.spec.js b/packages/dd-trace/test/azure_metadata.spec.js index 47b38f12fc3..9875e5b507a 100644 --- a/packages/dd-trace/test/azure_metadata.spec.js +++ b/packages/dd-trace/test/azure_metadata.spec.js @@ -4,9 +4,16 @@ const assert = require('node:assert/strict') const os = require('node:os') const { describe, it, afterEach } = require('mocha') +const proxyquire = require('proxyquire') require('./setup/core') -const { getAzureAppMetadata, getAzureTagsFromMetadata, getAzureFunctionMetadata } = require('../src/azure_metadata') +const { + getAzureAppMetadata, + getAzureTagsFromMetadata, + getAzureFunctionMetadata, +} = proxyquire('../src/azure_metadata', { + './config': () => proxyquire.noPreserveCache()('../src/config', {})(), +}) describe('Azure metadata', () => { const AZURE_ENV_KEYS = [ @@ -157,4 +164,39 @@ describe('Azure metadata', () => { const metadata = getAzureFunctionMetadata() assert.strictEqual(metadata.resourceGroup, 'regular_resource_group') }) + + describe('resource group extraction from WEBSITE_OWNER_NAME', () => { + /** @param {string} ownerName */ + function resourceGroupFor (ownerName) { + process.env.WEBSITE_SITE_NAME = 'site' + process.env.WEBSITE_OWNER_NAME = ownerName + return getAzureAppMetadata().resourceGroup + } + + it('strips the -Linux suffix before splitting on the last dash', () => { + assert.strictEqual(resourceGroupFor('sub+rg-regionwebspace-Linux'), 'rg') + }) + + it('preserves dashes inside the resource group', () => { + assert.strictEqual(resourceGroupFor('sub+with-dashes-regionwebspace'), 'with-dashes') + }) + + it('extracts from the shortest accepted form', () => { + assert.strictEqual(resourceGroupFor('a+b-cwebspace'), 'b') + }) + + it('returns undefined when WEBSITE_OWNER_NAME has no plus separator', () => { + // Suffix is otherwise valid; pins the early `plusIdx === -1` guard. + assert.strictEqual(resourceGroupFor('rg-regionwebspace'), undefined) + }) + + it('returns undefined when WEBSITE_OWNER_NAME does not end in webspace', () => { + // Length is chosen so removing the `webspace` guard would otherwise return `'rg'`. + assert.strictEqual(resourceGroupFor('sub+rg-something-region'), undefined) + }) + + it('returns undefined when no dash precedes the region marker', () => { + assert.strictEqual(resourceGroupFor('sub+regionwebspace'), undefined) + }) + }) }) diff --git a/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js b/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js index df0e5648a76..08d066946b3 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js @@ -37,11 +37,16 @@ describe('git_metadata', () => { before(() => { fs.writeFileSync(temporaryPackFile, '') fs.writeFileSync(secondTemporaryPackFile, '') + // Any request that escapes a nock interceptor used to hang up to the per + // request 15 s timeout and blow the mocha wall on slow Windows CI; flip the + // failure into an immediate `NetConnectNotAllowedError` at the call site. + nock.disableNetConnect() }) after(() => { fs.unlinkSync(temporaryPackFile) fs.unlinkSync(secondTemporaryPackFile) + nock.enableNetConnect() }) beforeEach(() => { diff --git a/packages/dd-trace/test/ci-visibility/exporters/test-worker/exporter.spec.js b/packages/dd-trace/test/ci-visibility/exporters/test-worker/exporter.spec.js index 7ab79b297e5..4968b374694 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/test-worker/exporter.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/test-worker/exporter.spec.js @@ -2,10 +2,13 @@ const { describe, it, beforeEach, afterEach } = require('mocha') const context = describe +const proxyquire = require('proxyquire') const sinon = require('sinon') require('../../../../../dd-trace/test/setup/core') -const TestWorkerCiVisibilityExporter = require('../../../../src/ci-visibility/exporters/test-worker') +const TestWorkerCiVisibilityExporter = proxyquire('../../../../src/ci-visibility/exporters/test-worker', { + '../../../config': () => proxyquire.noPreserveCache()('../../../../src/config', {})(), +}) const { JEST_WORKER_TRACE_PAYLOAD_CODE, diff --git a/packages/dd-trace/test/config/index.spec.js b/packages/dd-trace/test/config/index.spec.js index 0c998f17c49..c1461cd4828 100644 --- a/packages/dd-trace/test/config/index.spec.js +++ b/packages/dd-trace/test/config/index.spec.js @@ -68,6 +68,7 @@ describe('Config', () => { './supported-configurations.json': supportedConfigurations, '../log': log, './parsers': parsers, + '../../../../version': { DD_MAJOR: ddMajor }, }) const configHelper = proxyquire.noPreserveCache()('../../src/config/helper', { './supported-configurations.json': supportedConfigurations, @@ -130,7 +131,10 @@ describe('Config', () => { delete process.env.DATADOG_API_KEY }) - it('should log deprecation warning for deprecated configurations', async () => { + const itLegacyAlias = DD_MAJOR < 6 ? it : it.skip + const itV6Filter = DD_MAJOR >= 6 ? it : it.skip + + itLegacyAlias('should log deprecation warning for deprecated configurations', async () => { process.env.DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED = 'true' getEnvironmentVariables() const [warning] = await once(process, 'warning') @@ -142,22 +146,110 @@ describe('Config', () => { assert.strictEqual(warning.code, 'DATADOG_DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED') }) - it('should set new runtimeMetricsRuntimeId from deprecated DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED', async () => { - process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED = 'true' - assert.strictEqual(process.env.DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED, undefined) - const config = getConfig() - assert.strictEqual(config.runtimeMetricsRuntimeId, true) - assert.strictEqual(getEnvironmentVariable('DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED'), 'true') - delete process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED + itLegacyAlias( + 'should set new runtimeMetricsRuntimeId from deprecated DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED', + async () => { + process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED = 'true' + assert.strictEqual(process.env.DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED, undefined) + const config = getConfig() + assert.strictEqual(config.runtimeMetricsRuntimeId, true) + assert.strictEqual(getEnvironmentVariable('DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED'), 'true') + delete process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED + + const [warning] = await once(process, 'warning') + assert.strictEqual(warning.name, 'DeprecationWarning') + assert.match( + warning.message, + /variable DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED .+ DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED instead/ + ) + }) - const [warning] = await once(process, 'warning') - assert.strictEqual(warning.name, 'DeprecationWarning') - assert.match( - warning.message, - /variable DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED .+ DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED instead/ + // Load `helper.js` in isolation so a missing alias filter in helper.js cannot be masked by + // the same overrides applied from `defaults.js` against a shared module cache. + const loadFreshHelper = () => { + const fresh = proxyquire.noPreserveCache()('../../src/config/supported-configurations.json', {}) + return proxyquire.noPreserveCache()('../../src/config/helper', { + './supported-configurations.json': fresh, + }) + } + + itV6Filter('drops the deprecated DD_PROFILING_EXPERIMENTAL_* aliases without rewriting them', () => { + const helper = loadFreshHelper() + const envs = helper.getEnvironmentVariables({ + DD_PROFILING_EXPERIMENTAL_CODEHOTSPOTS_ENABLED: 'true', + DD_PROFILING_EXPERIMENTAL_CPU_ENABLED: 'true', + DD_PROFILING_EXPERIMENTAL_ENDPOINT_COLLECTION_ENABLED: 'true', + DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED: 'true', + }) + for (const canonical of [ + 'DD_PROFILING_CODEHOTSPOTS_ENABLED', + 'DD_PROFILING_CPU_ENABLED', + 'DD_PROFILING_ENDPOINT_COLLECTION_ENABLED', + 'DD_PROFILING_TIMELINE_ENABLED', + ]) { + assert.strictEqual(envs[canonical], undefined, `${canonical} should not be populated from a dropped alias`) + } + }) + + itV6Filter('drops the deprecated DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED alias', () => { + const helper = loadFreshHelper() + const envs = helper.getEnvironmentVariables({ DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED: 'true' }) + assert.strictEqual(envs.DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED, undefined) + }) + + itV6Filter('rejects access to the dropped DD_PROFILING_EXPERIMENTAL_CPU_ENABLED key', () => { + const helper = loadFreshHelper() + assert.throws( + () => helper.getEnvironmentVariable('DD_PROFILING_EXPERIMENTAL_CPU_ENABLED'), + /Missing DD_PROFILING_EXPERIMENTAL_CPU_ENABLED env\/configuration in "supported-configurations.json" file./ ) }) + itV6Filter('applyMajorOverrides is idempotent on the same supportedConfigurations object', () => { + const fresh = proxyquire.noPreserveCache()('../../src/config/supported-configurations.json', {}) + const applyMajorOverrides = proxyquire.noPreserveCache()('../../src/config/major-overrides', {}) + const supported = fresh.supportedConfigurations + assert.ok('DD_PROFILING_EXPERIMENTAL_CPU_ENABLED' in supported) + supported.DD_PROFILING_CPU_ENABLED[0].aliases.push('DD_PROFILING_TEST_ALIAS') + applyMajorOverrides(supported, 6) + assert.strictEqual('DD_PROFILING_EXPERIMENTAL_CPU_ENABLED' in supported, false) + assert.strictEqual('DD_TRACE_EXPERIMENTAL_B3_ENABLED' in supported, false) + assert.strictEqual('DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED' in supported, false) + const cpuEntry = supported.DD_PROFILING_CPU_ENABLED[0] + assert.ok(!cpuEntry.aliases?.some((alias) => alias.startsWith('DD_PROFILING_EXPERIMENTAL_'))) + assert.deepStrictEqual(cpuEntry.aliases, ['DD_PROFILING_TEST_ALIAS']) + const runtimeIdEntry = supported.DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED[0] + assert.strictEqual(runtimeIdEntry.aliases, undefined) + + const beforeKeyCount = Object.keys(supported).length + applyMajorOverrides(supported, 6) + assert.strictEqual(Object.keys(supported).length, beforeKeyCount) + }) + + it('applyMajorOverrides is idempotent for v5 security controls', () => { + const fresh = proxyquire.noPreserveCache()('../../src/config/supported-configurations.json', {}) + const applyMajorOverrides = proxyquire.noPreserveCache()('../../src/config/major-overrides', {}) + const supported = fresh.supportedConfigurations + const iastEntry = supported.DD_IAST_SECURITY_CONTROLS_CONFIGURATION[0] + + applyMajorOverrides(supported, 5) + applyMajorOverrides(supported, 5) + + assert.deepStrictEqual(iastEntry.configurationNames, [ + 'iast.securityControlsConfiguration', + 'experimental.iast.securityControlsConfiguration', + ]) + assert.strictEqual(iastEntry.internalPropertyName, undefined) + }) + + it('loads v5 config repeatedly after security controls are restored', () => { + const firstConfig = getConfig(undefined, { ddMajor: 5 }) + const secondConfig = getConfig(undefined, { ddMajor: 5 }) + + assert.strictEqual(firstConfig.iast.securityControlsConfiguration, undefined) + assert.strictEqual(secondConfig.iast.securityControlsConfiguration, undefined) + }) + it('should pass through random envs', async () => { process.env.FOOBAR = 'true' const { FOOBAR } = getEnvironmentVariables() @@ -536,6 +628,20 @@ describe('Config', () => { assert.strictEqual(config.OTEL_TRACES_EXPORTER, 'none') }) + it('should not disable OTLP traces export when DD_TRACE_AGENT_PROTOCOL_VERSION is unset', () => { + process.env.OTEL_TRACES_EXPORTER = 'otlp' + delete process.env.DD_TRACE_AGENT_PROTOCOL_VERSION + const config = getConfig() + assert.strictEqual(config.OTEL_TRACES_EXPORTER, 'otlp') + }) + + it('should disable OTLP traces export when DD_TRACE_AGENT_PROTOCOL_VERSION is set', () => { + process.env.OTEL_TRACES_EXPORTER = 'otlp' + process.env.DD_TRACE_AGENT_PROTOCOL_VERSION = '0.4' + const config = getConfig() + assert.strictEqual(config.OTEL_TRACES_EXPORTER, 'none') + }) + it('should fall back to http/json when OTEL_EXPORTER_OTLP_TRACES_PROTOCOL is unsupported', () => { process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'grpc' const config = getConfig() @@ -826,6 +932,7 @@ describe('Config', () => { { name: 'DD_SITE', value: 'datadoghq.com', origin: 'default' }, { name: 'DD_TRACE_SPAN_ATTRIBUTE_SCHEMA', value: 'v0', origin: 'default' }, { name: 'DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED', value: false, origin: 'default' }, + { name: 'DD_TRACE_LEGACY_BAGGAGE_ENABLED', value: true, origin: 'default' }, { name: 'DD_TRACE_STARTUP_LOGS', value: DD_MAJOR >= 6, origin: 'default' }, { name: 'DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH', value: 512, origin: 'default' }, { name: 'DD_TELEMETRY_DEBUG', value: false, origin: 'default' }, @@ -1510,22 +1617,22 @@ describe('Config', () => { }, exporter: 'log', enableGetRumData: true, - iast: { - dbRowsToTaint: 2, - deduplicationEnabled: false, - enabled: true, - maxConcurrentRequests: 4, - maxContextOperations: 5, - redactionEnabled: false, - redactionNamePattern: 'REDACTION_NAME_PATTERN', - redactionValuePattern: 'REDACTION_VALUE_PATTERN', - requestSampling: 50, - securityControlsConfiguration: 'SANITIZER:CODE_INJECTION:sanitizer.js:method', - stackTrace: { - enabled: false, - }, - telemetryVerbosity: 'DEBUG', + }, + iast: { + dbRowsToTaint: 2, + deduplicationEnabled: false, + enabled: true, + maxConcurrentRequests: 4, + maxContextOperations: 5, + redactionEnabled: false, + redactionNamePattern: 'REDACTION_NAME_PATTERN', + redactionValuePattern: 'REDACTION_VALUE_PATTERN', + requestSampling: 50, + securityControlsConfiguration: 'SANITIZER:CODE_INJECTION:sanitizer.js:method', + stackTrace: { + enabled: false, }, + telemetryVerbosity: 'DEBUG', }, flushInterval: 5000, flushMinSpans: 500, @@ -1697,19 +1804,23 @@ describe('Config', () => { assert.deepStrictEqual(config.serviceMapping, { a: 'aa', b: 'bb' }) assert.ok(Object.hasOwn(config.tags, 'runtime-id')) assert.match(config.tags['runtime-id'], /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/) - assert.deepStrictEqual(config.tracePropagationStyle.extract, ['datadog', 'b3', 'b3 single header']) - assert.deepStrictEqual(config.tracePropagationStyle.inject, ['datadog', 'b3', 'b3 single header']) - if (DD_MAJOR < 6) { + assert.deepStrictEqual(config.tracePropagationStyle.extract, ['datadog', 'b3', 'b3 single header']) + assert.deepStrictEqual(config.tracePropagationStyle.inject, ['datadog', 'b3', 'b3 single header']) sinon.assert.calledOnce(log.warn) } else { - sinon.assert.calledTwice(log.warn) + // In v6 `experimental.b3` is no longer parsed, so the calculated push doesn't fire and the + // propagation styles stay at the explicit input (`tracePropagationStyle: { inject: ['datadog'], ... }`). + assert.deepStrictEqual(config.tracePropagationStyle.extract, ['datadog']) + assert.deepStrictEqual(config.tracePropagationStyle.inject, ['datadog']) + sinon.assert.calledThrice(log.warn) sinon.assert.calledWithExactly( log.warn, 'Unknown option %s with value %o', - 'experimental.iast.securityControlsConfiguration', + 'iast.securityControlsConfiguration', 'SANITIZER:CODE_INJECTION:sanitizer.js:method', ) + sinon.assert.calledWithExactly(log.warn, 'Unknown option %s with value %o', 'experimental.b3', true) } sinon.assert.calledWithExactly(log.warn, 'Unknown option %s with value %o', 'enabled', false) @@ -1837,6 +1948,20 @@ describe('Config', () => { }) }) + it('should accept legacy "b3 single header" propagation style', () => { + process.env.DD_TRACE_PROPAGATION_STYLE = 'B3 single header' + + const config = getConfig() + + if (DD_MAJOR < 6) { + assert.deepStrictEqual(config.tracePropagationStyle.extract, ['b3 single header']) + assert.deepStrictEqual(config.tracePropagationStyle.inject, ['b3 single header']) + } else { + assert.deepStrictEqual(config.tracePropagationStyle.extract, ['b3']) + assert.deepStrictEqual(config.tracePropagationStyle.inject, ['b3']) + } + }) + it('should prioritize specific propagation style over shared propagation style env vars', () => { process.env.DD_TRACE_PROPAGATION_STYLE_EXTRACT = 'datadog,tracecontext' process.env.DD_TRACE_PROPAGATION_STYLE = 'datadog' @@ -2269,7 +2394,7 @@ describe('Config', () => { assert.strictEqual(config.url.toString(), 'https://agent2:6218/') }) - it('should give priority to non-experimental options', () => { + ;(DD_MAJOR < 6 ? it : it.skip)('should give priority to non-experimental options', () => { const config = getConfig({ appsec: { apiSecurity: { @@ -2419,6 +2544,61 @@ describe('Config', () => { }) }) + ;(DD_MAJOR < 6 ? it.skip : it)( + 'should drop the experimental.appsec programmatic shape in v6 (bare and nested)', + () => { + const config = getConfig({ + experimental: { + appsec: { + enabled: true, + rateLimit: 42, + rules: 'some-rules.json', + standalone: { enabled: true }, + }, + }, + }) + + assert.strictEqual(config.appsec.enabled, undefined) + assert.strictEqual(config.appsec.rateLimit, 100) + assert.strictEqual(config.appsec.rules, undefined) + assert.strictEqual(config.apmTracingEnabled, true) + + sinon.assert.calledWith( + log.warn, + 'Unknown option %s with value %o', + 'experimental.appsec', + sinon.match({ enabled: true, rateLimit: 42 }) + ) + } + ) + + ;(DD_MAJOR < 6 ? it.skip : it)('should drop the bare experimental.appsec boolean alias in v6', () => { + const config = getConfig({ + experimental: { appsec: true }, + }) + + assert.strictEqual(config.appsec.enabled, undefined) + sinon.assert.calledWith(log.warn, 'Unknown option %s with value %o', 'experimental.appsec', true) + }) + + ;(DD_MAJOR < 6 ? it.skip : it)('should drop the ingestion programmatic shape in v6', () => { + const config = getConfig({ + ingestion: { + sampleRate: 0.5, + rateLimit: 500, + }, + }) + + assert.strictEqual(config.sampleRate, undefined) + assert.strictEqual(config.rateLimit, 100) + sinon.assert.calledWith( + log.warn, + 'Unknown option %s with value %o', + 'ingestion', + sinon.match({ sampleRate: 0.5, rateLimit: 500 }) + ) + }) + it('should give priority to the options especially url', () => { process.env.DD_TRACE_AGENT_URL = 'http://agent2:6218' process.env.DD_TRACE_AGENT_HOSTNAME = 'agent' @@ -2745,15 +2925,43 @@ describe('Config', () => { it('should ignore invalid iast.requestSampling', () => { const config = getConfig({ - experimental: { - iast: { - requestSampling: 105, - }, + iast: { + requestSampling: 105, }, }) assert.strictEqual(config.iast.requestSampling, 30) }) + describe('experimental.iast alias gate', () => { + // v5 keeps the `experimental.iast.*` aliases for backports; v6 strips both + // bare and nested forms so user-supplied objects do not leak into + // `iast.enabled` via the bare alias. + for (const { name, iast, v5Field, v5Value } of [ + { name: 'bare alias as boolean', iast: true, v5Field: 'enabled', v5Value: true }, + { name: '.enabled nested key', iast: { enabled: false }, v5Field: 'enabled', v5Value: false }, + { name: '.requestSampling nested key', iast: { requestSampling: 50 }, v5Field: 'requestSampling', v5Value: 50 }, + ]) { + it(`v5 keeps experimental.iast (${name})`, () => { + const config = getConfig({ experimental: { iast } }, { ddMajor: 5 }) + assert.strictEqual(config.iast[v5Field], v5Value) + sinon.assert.notCalled(log.warn) + }) + + it(`v6 rejects experimental.iast (${name}) as Unknown option`, () => { + const config = getConfig({ experimental: { iast } }, { ddMajor: 6 }) + assert.strictEqual(config.iast.enabled, false) + assert.strictEqual(config.iast.requestSampling, 30) + sinon.assert.calledOnce(log.warn) + sinon.assert.calledWithExactly( + log.warn, + 'Unknown option %s with value %o', + 'experimental.iast', + iast, + ) + }) + } + }) + it('should load span sampling rules from json file', () => { const path = '../fixtures/config/span-sampling-rules.json' process.env.DD_SPAN_SAMPLING_RULES_FILE = require.resolve(path) @@ -3314,26 +3522,41 @@ describe('Config', () => { }) context('standalone', () => { - it('should disable apm tracing with legacy DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED', () => { + const itLegacyStandalone = DD_MAJOR < 6 ? it : it.skip + const itV6Standalone = DD_MAJOR < 6 ? it.skip : it + + itLegacyStandalone('should disable apm tracing with legacy DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED', () => { process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = '1' const config = getConfig() assert.strictEqual(config.apmTracingEnabled, false) }) - it('should win DD_APM_TRACING_ENABLED', () => { - process.env.DD_APM_TRACING_ENABLED = '1' - process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = 'true' + itLegacyStandalone('should disable apm tracing with legacy experimental.appsec.standalone.enabled option', () => { + process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = '0' + + const config = getConfig({ experimental: { appsec: { standalone: { enabled: true } } } }) + assert.strictEqual(config.apmTracingEnabled, false) + }) + + itV6Standalone('should ignore legacy DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED in v6', () => { + process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = '1' const config = getConfig() assert.strictEqual(config.apmTracingEnabled, true) }) - it('should disable apm tracing with legacy experimental.appsec.standalone.enabled option', () => { - process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = '0' - + itV6Standalone('should ignore legacy experimental.appsec.standalone.enabled programmatic option in v6', () => { const config = getConfig({ experimental: { appsec: { standalone: { enabled: true } } } }) - assert.strictEqual(config.apmTracingEnabled, false) + assert.strictEqual(config.apmTracingEnabled, true) + }) + + it('should win DD_APM_TRACING_ENABLED', () => { + process.env.DD_APM_TRACING_ENABLED = '1' + process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = 'true' + + const config = getConfig() + assert.strictEqual(config.apmTracingEnabled, true) }) it('should win apmTracingEnabled option', () => { @@ -3341,7 +3564,7 @@ describe('Config', () => { const config = getConfig({ apmTracingEnabled: false, - experimental: { appsec: { standalone: { enabled: true } } }, + ...DD_MAJOR < 6 && { experimental: { appsec: { standalone: { enabled: true } } } }, }) assert.strictEqual(config.apmTracingEnabled, false) }) diff --git a/packages/dd-trace/test/dd-trace.spec.js b/packages/dd-trace/test/dd-trace.spec.js index 58b35ee44eb..db67c5f56d3 100644 --- a/packages/dd-trace/test/dd-trace.spec.js +++ b/packages/dd-trace/test/dd-trace.spec.js @@ -35,8 +35,12 @@ describe('dd-trace', () => { assert.strictEqual(payload[0][0].service, 'test') assert.strictEqual(payload[0][0].name, 'hello') assert.strictEqual(payload[0][0].resource, '/hello/:name') - assert.strictEqual(typeof payload[0][0].start, 'bigint') - assert.strictEqual(typeof payload[0][0].duration, 'bigint') + // Compact-int encoding picks the smallest msgpack int that fits, so + // small `duration` values decode as `Number` and large `start` + // timestamps decode as `BigInt`. Coerce both to BigInt before checking + // the round-trip values so the test is encoding-agnostic. + assert.ok(BigInt(payload[0][0].start) > 0n) + assert.ok(BigInt(payload[0][0].duration) >= 0n) assert.ok(Object.hasOwn(payload[0][0].metrics, SAMPLING_PRIORITY_KEY)) assert.ok(Object.hasOwn(payload[0][0].meta, DECISION_MAKER_KEY)) }) diff --git a/packages/dd-trace/test/debugger/devtools_client/breakpoints.spec.js b/packages/dd-trace/test/debugger/devtools_client/breakpoints.spec.js index 4149a9f7d54..6a7c48e18cd 100644 --- a/packages/dd-trace/test/debugger/devtools_client/breakpoints.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/breakpoints.spec.js @@ -748,9 +748,9 @@ describe('breakpoints', function () { * Generate a probe config * * @param {object} [config] Optional configuration object. - * @param {string} [config.id='probe-1'] The probe ID. - * @param {number} [config.version=1] The probe version. - * @param {object} [config.where = { sourceFile: 'test.js', lines: ['10'] }] The location information. + * @param {string} [config.id] The probe ID. Defaults to `probe-1`. + * @param {number} [config.version] The probe version. Defaults to 1. + * @param {object} [config.where] The location information. Defaults to a test file on line 10. * @param {object} [config.when] The condition for the probe. * { json: { eq: [{ ref: 'foo' }, 42] }, dsl: 'foo = 42' } by default. * @returns {{ id: string; version: number; where: object; when: object; }} diff --git a/packages/dd-trace/test/encode/0.4.spec.js b/packages/dd-trace/test/encode/0.4.spec.js index 0272da1d602..8b5937f5410 100644 --- a/packages/dd-trace/test/encode/0.4.spec.js +++ b/packages/dd-trace/test/encode/0.4.spec.js @@ -68,8 +68,8 @@ describe('encode', () => { assert.strictEqual(trace[0].trace_id.toString(16), data[0].trace_id.toString()) assert.strictEqual(trace[0].span_id.toString(16), data[0].span_id.toString()) assert.strictEqual(trace[0].parent_id.toString(16), data[0].parent_id.toString()) - assert.strictEqual(trace[0].start, 123n) - assert.strictEqual(trace[0].duration, 456n) + assert.strictEqual(trace[0].start, 123) + assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) assert.deepStrictEqual(trace[0].meta, { bar: 'baz' }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) @@ -182,8 +182,8 @@ describe('encode', () => { type: 'foo', error: 0, }) - assert.strictEqual(decodedData.start, 123n) - assert.strictEqual(decodedData.duration, 456n) + assert.strictEqual(decodedData.start, 123) + assert.strictEqual(decodedData.duration, 456) assert.deepStrictEqual(decodedData.meta, { bar: 'baz', }) @@ -302,8 +302,8 @@ describe('encode', () => { assert.strictEqual(trace[0].trace_id.toString(16), data[0].trace_id.toString()) assert.strictEqual(trace[0].span_id.toString(16), data[0].span_id.toString()) assert.strictEqual(trace[0].parent_id.toString(16), data[0].parent_id.toString()) - assert.strictEqual(trace[0].start, 123n) - assert.strictEqual(trace[0].duration, 456n) + assert.strictEqual(trace[0].start, 123) + assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.span_links': encodedLink }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) @@ -324,8 +324,8 @@ describe('encode', () => { assert.strictEqual(trace[0].trace_id.toString(16), data[0].trace_id.toString()) assert.strictEqual(trace[0].span_id.toString(16), data[0].span_id.toString()) assert.strictEqual(trace[0].parent_id.toString(16), data[0].parent_id.toString()) - assert.strictEqual(trace[0].start, 123n) - assert.strictEqual(trace[0].duration, 456n) + assert.strictEqual(trace[0].start, 123) + assert.strictEqual(trace[0].duration, 456) assert.strictEqual(trace[0].name, data[0].name) assert.deepStrictEqual(trace[0].meta, { bar: 'baz', '_dd.span_links': encodedLink }) assert.deepStrictEqual(trace[0].metrics, { example: 1 }) diff --git a/packages/dd-trace/test/encode/0.5.spec.js b/packages/dd-trace/test/encode/0.5.spec.js index a92f0af3bf7..6e31af9fbd1 100644 --- a/packages/dd-trace/test/encode/0.5.spec.js +++ b/packages/dd-trace/test/encode/0.5.spec.js @@ -221,6 +221,29 @@ describe('encode 0.5', () => { assert.strictEqual(payload[11], 0) }) + it('should emit span.error as a compact msgpack int on the wire', () => { + data[0].error = 1 + data[0].start = 0 + data[0].duration = 0 + + encoder.encode(data) + + const buffer = encoder.makePayload() + const decoded = msgpack.decode(buffer, { useBigInt64: true }) + const trace = decoded[1][0] + assert.strictEqual(trace[0][8], 1) + + // The three uint64 IDs are encoded back-to-back; parent_id is the last + // 9-byte `0xCF | ` run. With start = duration = 0 (single-byte + // fixints), the next byte after parent_id+2 is `error`. Pin it to + // fixint 0x01 — emitting 0xCB (float64) here would mean the override + // dropped off the 0.5 wire. + const idMarker = Buffer.from([0xCF, 0x12, 0x34, 0xAB, 0xCD, 0x12, 0x34, 0xAB, 0xCD]) + const parentIdOffset = buffer.lastIndexOf(idMarker) + assert.notStrictEqual(parentIdOffset, -1) + assert.strictEqual(buffer[parentIdOffset + idMarker.length + 2], 0x01) + }) + it('should ignore meta_struct property', () => { data[0].meta_struct = { foo: 'bar' } diff --git a/packages/dd-trace/test/encode/encode-int-or-float.spec.js b/packages/dd-trace/test/encode/encode-int-or-float.spec.js new file mode 100644 index 00000000000..1c661964bf5 --- /dev/null +++ b/packages/dd-trace/test/encode/encode-int-or-float.spec.js @@ -0,0 +1,104 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { describe, it, beforeEach } = require('mocha') +const msgpack = require('@msgpack/msgpack') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +require('../setup/core') +const id = require('../../src/id') + +// `pinkey` is the `metrics` map key the test pins. Its 7-byte fixstr +// encoding (fixstr-6 prefix + ASCII) is unique within the encoded payload, +// so the byte that immediately follows the marker is the value's prefix +// byte emitted by `_encodeIntOrFloat`. +const PIN_KEY = 'pinkey' +const PIN_KEY_BYTES = Buffer.from([0xA6, 0x70, 0x69, 0x6E, 0x6B, 0x65, 0x79]) + +const cases = [ + { label: '0 as positive fixint', value: 0, prefix: 0x00, expected: 0 }, + { label: '1 as positive fixint', value: 1, prefix: 0x01, expected: 1 }, + { label: '127 as positive fixint upper boundary', value: 127, prefix: 0x7F, expected: 127 }, + { label: '128 as uint8', value: 128, prefix: 0xCC, expected: 128 }, + { label: '255 as uint8 upper boundary', value: 255, prefix: 0xCC, expected: 255 }, + { label: '256 as uint16', value: 256, prefix: 0xCD, expected: 256 }, + { label: '0xFFFF as uint16 upper boundary', value: 0xFFFF, prefix: 0xCD, expected: 0xFFFF }, + { label: '0x10000 as uint32', value: 0x10000, prefix: 0xCE, expected: 0x10000 }, + // -1 lands in negative fixint (-32..-1, single byte 0xE0..0xFF), not int8. + // The single byte IS the encoding; -1 cast to a byte reads back as 0xFF. + { label: '-1 as negative fixint', value: -1, prefix: 0xFF, expected: -1 }, + { + label: 'Number.MAX_SAFE_INTEGER as uint64', + value: Number.MAX_SAFE_INTEGER, + prefix: 0xCF, + expected: BigInt(Number.MAX_SAFE_INTEGER), + }, + // `MsgpackEncoder.encodeNumber` would coerce NaN to fixint 0 — `_encodeIntOrFloat` + // keeps it as float64 so the agent sees what the application produced. + { label: 'NaN as float64 (not coerced to fixint 0)', value: Number.NaN, prefix: 0xCB, expectedNaN: true }, + { label: 'Infinity as float64', value: Number.POSITIVE_INFINITY, prefix: 0xCB, expected: Number.POSITIVE_INFINITY }, + { label: '-Infinity as float64', value: Number.NEGATIVE_INFINITY, prefix: 0xCB, expected: Number.NEGATIVE_INFINITY }, + { label: '1.5 as float64', value: 1.5, prefix: 0xCB, expected: 1.5 }, + // -0 takes the fixint fast path: `(-0 & 0x7F)` is 0 and `-0 === 0` in JS, + // so the encoder writes a single 0x00 byte and the wire carries +0. + { label: '-0 collapses to positive fixint zero', value: -0, prefix: 0x00, expected: 0 }, +] + +describe('encode 0.4 _encodeIntOrFloat', () => { + let encoder + + beforeEach(() => { + const getConfig = () => ({ trace: { nativeSpanEvents: false } }) + const { AgentEncoder } = proxyquire('../../src/encode/0.4', { + '../log': { debug: sinon.stub() }, + '../config': getConfig, + }) + encoder = new AgentEncoder({ flush: sinon.spy() }) + }) + + /** + * Encode a span with `metrics.pinkey === value`, then locate the value's + * prefix byte and decode the round-trip value. + * + * @param {number} value + * @returns {{ prefix: number, decoded: number | bigint }} + */ + function encodePinned (value) { + encoder.encode([{ + trace_id: id('1234abcd1234abcd'), + span_id: id('1234abcd1234abcd'), + parent_id: id('1234abcd1234abcd'), + name: 'test', + resource: 'test-r', + service: 'test-s', + error: 0, + meta: { bar: 'baz' }, + metrics: { [PIN_KEY]: value }, + start: 123, + duration: 456, + }]) + + const payload = encoder.makePayload() + const markerOffset = payload.indexOf(PIN_KEY_BYTES) + assert.notStrictEqual(markerOffset, -1, 'pinkey marker not found in payload') + + const prefix = payload[markerOffset + PIN_KEY_BYTES.length] + const decoded = msgpack.decode(payload, { useBigInt64: true })[0][0].metrics[PIN_KEY] + + return { prefix, decoded } + } + + for (const testCase of cases) { + it(`encodes ${testCase.label}`, () => { + const { prefix, decoded } = encodePinned(testCase.value) + assert.strictEqual(prefix, testCase.prefix) + if (testCase.expectedNaN) { + assert.ok(Number.isNaN(decoded), `expected NaN, got ${decoded}`) + } else { + assert.strictEqual(decoded, testCase.expected) + } + }) + } +}) diff --git a/packages/dd-trace/test/exporters/electron/exporter.spec.js b/packages/dd-trace/test/exporters/electron/exporter.spec.js new file mode 100644 index 00000000000..185ba2449a2 --- /dev/null +++ b/packages/dd-trace/test/exporters/electron/exporter.spec.js @@ -0,0 +1,125 @@ +'use strict' + +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire') + +require('../../setup/core') + +describe('ElectronExporter', () => { + let Exporter + let exporter + let traceChannel + let truncateSpan + let normalizeSpan + let span + let config + + beforeEach(() => { + span = { name: 'test', service: 'my-service', meta: {}, metrics: {} } + config = { flushInterval: 1000 } + + traceChannel = { + hasSubscribers: true, + publish: sinon.spy(), + } + + truncateSpan = sinon.stub().callsFake(s => s) + normalizeSpan = sinon.stub().callsFake(s => s) + + Exporter = proxyquire('../../../src/exporters/electron', { + 'dc-polyfill': { channel: sinon.stub().returns(traceChannel) }, + '../../encode/tags-processors': { truncateSpan, normalizeSpan }, + }) + + exporter = new Exporter(config) + }) + + afterEach(() => { + sinon.restore() + }) + + describe('export', () => { + it('should buffer traces and not publish immediately when flushInterval > 0', () => { + exporter.export([span]) + + sinon.assert.notCalled(traceChannel.publish) + }) + + it('should flush immediately when flushInterval is 0', () => { + exporter = new Exporter({ flushInterval: 0 }) + exporter.export([span]) + + sinon.assert.calledOnce(traceChannel.publish) + }) + + it('should not set a second timer when called multiple times before flush', () => { + const setTimeoutSpy = sinon.spy(global, 'setTimeout') + + exporter.export([span]) + exporter.export([span]) + + sinon.assert.calledOnce(setTimeoutSpy) + }) + }) + + describe('flush', () => { + it('should publish all buffered traces', () => { + exporter.export([span]) + exporter.export([span]) + exporter.flush() + + sinon.assert.calledOnce(traceChannel.publish) + const [[traces]] = traceChannel.publish.args + sinon.assert.match(traces.length, 2) + }) + + it('should not publish when there are no buffered traces', () => { + exporter.flush() + + sinon.assert.notCalled(traceChannel.publish) + }) + + it('should not publish when there are no subscribers', () => { + traceChannel.hasSubscribers = false + + exporter.export([span]) + exporter.flush() + + sinon.assert.notCalled(traceChannel.publish) + }) + + it('should normalize and truncate each span', () => { + exporter.export([span]) + exporter.flush() + + sinon.assert.calledWith(truncateSpan, span) + sinon.assert.calledWith(normalizeSpan, span) + }) + + it('should clear the buffer after flushing', () => { + exporter.export([span]) + exporter.flush() + exporter.flush() + + sinon.assert.calledOnce(traceChannel.publish) + }) + + it('should call the done callback', (done) => { + exporter.export([span]) + exporter.flush(done) + }) + + it('should publish each export call as a separate trace', () => { + const span2 = { name: 'other', service: 'svc', meta: {}, metrics: {} } + + exporter.export([span]) + exporter.export([span2]) + exporter.flush() + + const [[traces]] = traceChannel.publish.args + sinon.assert.match(traces[0], [span]) + sinon.assert.match(traces[1], [span2]) + }) + }) +}) diff --git a/packages/dd-trace/test/git_metadata.spec.js b/packages/dd-trace/test/git_metadata.spec.js index 56f6e908494..38f0a7904b3 100644 --- a/packages/dd-trace/test/git_metadata.spec.js +++ b/packages/dd-trace/test/git_metadata.spec.js @@ -183,6 +183,35 @@ describe('git metadata', () => { assert.strictEqual(getGitMetadata(config), getGitMetadata(config)) }) + // Regression: a single shared cache slot returned the disabled-branch + // empty result for any subsequent call once the first call had run with + // `DD_TRACE_GIT_METADATA_ENABLED=false`, even when the next caller passed + // a fresh `Config` with the flag flipped on and the env populated. + it('caches the disabled-branch result independently from the enabled-branch result', () => { + const getGitMetadata = proxyquire.noPreserveCache()('../src/git_metadata', {}) + + const disabledConfig = { DD_TRACE_GIT_METADATA_ENABLED: false, tags: {} } + const enabledConfig = { + DD_TRACE_GIT_METADATA_ENABLED: true, + DD_GIT_COMMIT_SHA: DUMMY_COMMIT_SHA, + DD_GIT_REPOSITORY_URL: DUMMY_REPOSITORY_URL, + tags: {}, + } + + assert.deepStrictEqual(getGitMetadata(disabledConfig), { + commitSHA: undefined, + repositoryUrl: undefined, + }) + assert.deepStrictEqual(getGitMetadata(enabledConfig), { + commitSHA: DUMMY_COMMIT_SHA, + repositoryUrl: DUMMY_REPOSITORY_URL, + }) + assert.deepStrictEqual(getGitMetadata({ DD_TRACE_GIT_METADATA_ENABLED: false, tags: {} }), { + commitSHA: undefined, + repositoryUrl: undefined, + }) + }) + it('returns undefined when no git source is configured and cwd has no git', () => { const originalCwd = process.cwd() const tempDir = fs.mkdtempSync(path.join(TMP_DIR, 'dd-trace-no-git-')) diff --git a/packages/dd-trace/test/lambda/registration.spec.js b/packages/dd-trace/test/lambda/registration.spec.js new file mode 100644 index 00000000000..8de82bf4570 --- /dev/null +++ b/packages/dd-trace/test/lambda/registration.spec.js @@ -0,0 +1,172 @@ +'use strict' + +const assert = require('node:assert/strict') +const path = require('node:path') + +const { afterEach, beforeEach, describe, it } = require('mocha') + +const proxyquire = require('proxyquire').noPreserveCache() + +const instrumentations = require('../../../datadog-instrumentations/src/helpers/instrumentations') + +const oldEnv = process.env + +function loadLambdaWithHookSpy () { + /** @type {Array<{ modules: string[], onrequire: Function }>} */ + const hookCalls = [] + /** + * @param {string[]} modules + * @param {Function} onrequire + */ + const HookSpy = (modules, onrequire) => { + hookCalls.push({ modules, onrequire }) + } + proxyquire('../../src/lambda', { + '../../../datadog-instrumentations/src/helpers/hook': HookSpy, + }) + return { hookCalls } +} + +describe('lambda', () => { + describe('registerLambdaHook', () => { + beforeEach(() => { + process.env = { ...oldEnv } + delete process.env.DD_LAMBDA_HANDLER + delete process.env.DD_TRACE_DISABLED_INSTRUMENTATIONS + // Drop cached `runtime/patch` so its module-level `addHook` calls run + // against the test's current env (each test sets its own + // `DD_LAMBDA_HANDLER`). + delete require.cache[require.resolve('../../src/lambda/runtime/patch')] + }) + + afterEach(() => { + process.env = oldEnv + delete instrumentations['datadog-lambda-js'] + }) + + it('registers a hook on the resolved handler file paths', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + process.env.DD_LAMBDA_HANDLER = './api/src/index.nested.handler' + + const { hookCalls } = loadLambdaWithHookSpy() + + assert.strictEqual(hookCalls.length, 1) + const indexPath = path.resolve('/var/task', './api/src/', 'index') + assert.deepStrictEqual(hookCalls[0].modules, [ + `${indexPath}.js`, + `${indexPath}.mjs`, + `${indexPath}.cjs`, + ]) + assert.strictEqual(typeof hookCalls[0].onrequire, 'function') + }) + + it('registers a hook for a flat handler path', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + process.env.DD_LAMBDA_HANDLER = 'handler.handler' + + const { hookCalls } = loadLambdaWithHookSpy() + + assert.strictEqual(hookCalls.length, 1) + const handlerPath = path.resolve('/var/task', '', 'handler') + assert.deepStrictEqual(hookCalls[0].modules, [ + `${handlerPath}.js`, + `${handlerPath}.mjs`, + `${handlerPath}.cjs`, + ]) + }) + + it('falls back to the datadog-lambda-js branch when DD_LAMBDA_HANDLER is unset', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + + const { hookCalls } = loadLambdaWithHookSpy() + + assert.strictEqual(hookCalls.length, 1) + assert.deepStrictEqual(hookCalls[0].modules, ['datadog-lambda-js']) + }) + + it('throws when DD_LAMBDA_HANDLER is malformed', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + process.env.DD_LAMBDA_HANDLER = 'handler' + + assert.throws(loadLambdaWithHookSpy, { message: 'Malformed handler name: handler' }) + }) + + it('does not register a hook when lambda is in DD_TRACE_DISABLED_INSTRUMENTATIONS', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + process.env.DD_LAMBDA_HANDLER = 'handler.handler' + process.env.DD_TRACE_DISABLED_INSTRUMENTATIONS = 'http,lambda,fs' + + const { hookCalls } = loadLambdaWithHookSpy() + + assert.strictEqual(hookCalls.length, 0) + }) + + it('wraps the registered handler key when the lambda Hook callback fires', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + process.env.DD_LAMBDA_HANDLER = 'handler.myEntry' + + const { hookCalls } = loadLambdaWithHookSpy() + const fixturePath = `${path.resolve('/var/task', '', 'handler')}.js` + + const userHandler = () => 'original' + const fakeModule = { myEntry: userHandler } + hookCalls[0].onrequire(fakeModule, fixturePath, undefined, '0.0.0') + + assert.notStrictEqual(fakeModule.myEntry, userHandler) + delete instrumentations[fixturePath] + }) + + it('wraps the datadog export when the datadog-lambda-js Hook callback fires', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + + const { hookCalls } = loadLambdaWithHookSpy() + + const datadogOriginal = handler => handler + const fakeModule = { datadog: datadogOriginal } + hookCalls[0].onrequire(fakeModule, 'datadog-lambda-js', undefined, '0.0.0') + + assert.notStrictEqual(fakeModule.datadog, datadogOriginal) + // Exercise the inner wrapper produced by `patchDatadogLambdaHandler`. + assert.strictEqual(typeof fakeModule.datadog(() => 'user'), 'function') + }) + + it('logs hook errors without unwinding the loop', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + process.env.DD_LAMBDA_HANDLER = 'handler.myEntry' + + const { hookCalls } = loadLambdaWithHookSpy() + + const fixturePath = `${path.resolve('/var/task', '', 'handler')}.js` + // Replace whatever `runtime/patch` populated with two hooks: one that + // throws (covers the catch / log branch) and one that returns a real + // module object so the loop reaches the trailing return. + instrumentations[fixturePath] = [ + { hook: () => { throw new Error('boom') } }, + { hook: mod => ({ ...mod, ok: true }) }, + ] + + const result = hookCalls[0].onrequire({}, fixturePath, undefined, '0.0.0') + assert.strictEqual(result.ok, true) + + delete instrumentations[fixturePath] + }) + + it('logs datadog-lambda-js hook errors without unwinding the loop', () => { + process.env.LAMBDA_TASK_ROOT = '/var/task' + + const { hookCalls } = loadLambdaWithHookSpy() + + // `file: undefined` so `filename('datadog-lambda-js', file)` resolves to + // the bare name and matches the `moduleName === fullFilename` branch. + instrumentations['datadog-lambda-js'] = [ + { hook: () => { throw new Error('boom') }, file: undefined }, + { hook: mod => ({ ...mod, ok: true }), file: undefined }, + // A non-matching entry to exercise the `false` branch of the inner if. + { hook: () => assert.fail('non-matching entry must not run'), file: 'other.js' }, + ] + + const result = hookCalls[0].onrequire({}, 'datadog-lambda-js', undefined, '0.0.0') + assert.strictEqual(result.ok, true) + }) + }) +}) diff --git a/packages/dd-trace/test/lambda/runtime/ritm.spec.js b/packages/dd-trace/test/lambda/runtime/ritm.spec.js deleted file mode 100644 index 206bb6ca248..00000000000 --- a/packages/dd-trace/test/lambda/runtime/ritm.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict' - -const assert = require('node:assert/strict') - -const { _extractModuleRootAndHandler, _extractModuleNameAndHandlerPath } = require('../../../src/lambda/runtime/ritm') - -describe('runtime', () => { - describe('ritm', () => { - describe('_extractModuleRootAndHandler', () => { - it('breaks nested module and handler correctly', () => { - const fullHandler = './api/src/index.nested.handler' - const [moduleRoot, handlerString] = _extractModuleRootAndHandler(fullHandler) - - const expectedModuleRoot = './api/src/' - const expectedHandler = 'index.nested.handler' - - assert.strictEqual(moduleRoot, expectedModuleRoot) - assert.strictEqual(handlerString, expectedHandler) - }) - - it('breaks module and handler correctly', () => { - const fullHandler = './src/handler.handler' - const [moduleRoot, handlerString] = _extractModuleRootAndHandler(fullHandler) - - const expectedModuleRoot = './src/' - const expectedHandler = 'handler.handler' - - assert.strictEqual(moduleRoot, expectedModuleRoot) - assert.strictEqual(handlerString, expectedHandler) - }) - }) - - describe('_extractModuleNameAndHandlerPath', () => { - it('breaks module name and nested handler path correctly', () => { - const handler = 'index.nested.handler' - const [moduleName, handlerPath] = _extractModuleNameAndHandlerPath(handler) - - const expectedModuleName = 'index' - const expectedHandlerPath = 'nested.handler' - - assert.strictEqual(moduleName, expectedModuleName) - assert.strictEqual(handlerPath, expectedHandlerPath) - }) - - it('breaks module name and handler path correctly', () => { - const handler = 'handler.handler' - const [moduleName, handlerPath] = _extractModuleNameAndHandlerPath(handler) - - const expectedModuleName = 'handler' - const expectedHandlerPath = 'handler' - - assert.strictEqual(moduleName, expectedModuleName) - assert.strictEqual(handlerPath, expectedHandlerPath) - }) - - it('throws an error if the handler is malformed', () => { - const handler = 'handler' - assert.throws(() => _extractModuleNameAndHandlerPath(handler), Error, `Malformed handler name: ${handler}`) - }) - }) - }) -}) diff --git a/packages/dd-trace/test/llmobs/index.spec.js b/packages/dd-trace/test/llmobs/index.spec.js index 21d37393dbe..29e4a9cc3e0 100644 --- a/packages/dd-trace/test/llmobs/index.spec.js +++ b/packages/dd-trace/test/llmobs/index.spec.js @@ -3,12 +3,13 @@ const assert = require('node:assert/strict') const { channel } = require('dc-polyfill') -const { after, afterEach, beforeEach, describe, it } = require('mocha') +const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') const { DD_MAJOR } = require('../../../../version') const { INCOMPATIBLE_INITIALIZATION } = require('../../src/llmobs/constants/text') +const { getConfigFresh } = require('../helpers/config') const { removeDestroyHandler } = require('./util') const spanFinishCh = channel('dd-trace:span:finish') @@ -106,7 +107,7 @@ describe('module', () => { } injectCh.publish({ carrier }) - assert.strictEqual(carrier['x-datadog-tags'], ',_dd.p.llmobs_parent_id=parent-id,_dd.p.llmobs_ml_app=test') + assert.strictEqual(carrier['x-datadog-tags'], '_dd.p.llmobs_parent_id=parent-id,_dd.p.llmobs_ml_app=test') }) it('does not inject LLMObs parent ID info when there is no parent LLMObs span', () => { @@ -116,7 +117,7 @@ describe('module', () => { 'x-datadog-tags': '', } injectCh.publish({ carrier }) - assert.strictEqual(carrier['x-datadog-tags'], ',_dd.p.llmobs_ml_app=test') + assert.strictEqual(carrier['x-datadog-tags'], '_dd.p.llmobs_ml_app=test') }) it('does not inject LLMOBs info when there is no mlApp configured and no parent LLMObs span', () => { @@ -128,6 +129,48 @@ describe('module', () => { injectCh.publish({ carrier }) assert.strictEqual(carrier['x-datadog-tags'], '') }) + + it('does not produce a literal "undefined" prefix when carrier has no x-datadog-tags', () => { + llmobsModule.enable({ llmobs: { mlApp: 'test', agentlessEnabled: false } }) + + const carrier = {} + injectCh.publish({ carrier }) + + assert.strictEqual(carrier['x-datadog-tags'], '_dd.p.llmobs_ml_app=test') + }) + + it('appends to an existing non-empty x-datadog-tags with a single comma separator', () => { + llmobsModule.enable({ llmobs: { mlApp: 'test', agentlessEnabled: false } }) + + const carrier = { + 'x-datadog-tags': '_dd.p.tid=69fe014200000000,_dd.p.dm=-0', + } + injectCh.publish({ carrier }) + + assert.strictEqual( + carrier['x-datadog-tags'], + '_dd.p.tid=69fe014200000000,_dd.p.dm=-0,_dd.p.llmobs_ml_app=test' + ) + }) + + describe('with DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH=0', () => { + let config + + before(() => { + process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH = '0' + config = getConfigFresh({ llmobs: { mlApp: 'test', agentlessEnabled: false } }) + delete process.env.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH + }) + + it('does not write x-datadog-tags', () => { + llmobsModule.enable(config) + + const carrier = {} + injectCh.publish({ carrier }) + + assert.ok(!('x-datadog-tags' in carrier)) + }) + }) }) describe('with agentlessEnabled set to `true`', () => { diff --git a/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.spec.js b/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.spec.js index 73f521d264f..b88b55dfe43 100644 --- a/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.spec.js @@ -2,9 +2,8 @@ const { describe, it, before } = require('mocha') -const { withVersions } = require('../../../setup/mocha') - const { assertLlmObsSpanEvent, useLlmObs } = require('../../util') +const { withAwsSdkVersions } = require('../../../../../datadog-plugin-aws-sdk/test/spec_helpers') const { models, modelConfig, @@ -24,7 +23,7 @@ describe('Plugin', () => { const { getEvents } = useLlmObs({ plugin: 'aws-sdk' }) - withVersions('aws-sdk', ['@aws-sdk/smithy-client', 'aws-sdk'], '>=3', (version, moduleName) => { + withAwsSdkVersions('>=3', (version, moduleName) => { let AWS let bedrockRuntimeClient diff --git a/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.token-cache.spec.js b/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.token-cache.spec.js new file mode 100644 index 00000000000..deb8731c6fe --- /dev/null +++ b/packages/dd-trace/test/llmobs/plugins/aws-sdk/bedrockruntime.token-cache.spec.js @@ -0,0 +1,146 @@ +'use strict' + +const assert = require('node:assert/strict') + +const dc = require('dc-polyfill') +const { afterEach, beforeEach, describe, it } = require('mocha') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +require('../../../setup/core') + +// Drives the diagnostic channels directly so the cleanup behaviour is fast and +// dependency-free to verify. The integration counterpart is in +// `bedrockruntime.spec.js` (real SDK via VCR). +describe('BedrockRuntime LLMObs plugin pending token headers', () => { + const deserializeCh = dc.channel('apm:aws:response:deserialize:bedrockruntime') + const completeCh = dc.channel('apm:aws:request:complete:bedrockruntime') + + let BedrockRuntimePlugin + let plugin + let tagMetricsSpy + + beforeEach(() => { + tagMetricsSpy = sinon.spy() + + // `usage: {}` keeps the response body free of tokens, so the only source + // of token counts is the header cache. That makes the assertions sensitive + // to whether a previous entry was correctly evicted. + BedrockRuntimePlugin = proxyquire('../../../../src/llmobs/plugins/bedrockruntime', { + '../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils': { + parseModelId (modelId) { + if (modelId.includes('embed')) return { modelProvider: 'amazon', modelName: 'embed' } + return { modelProvider: 'amazon', modelName: 'titan' } + }, + extractRequestParams: () => ({ temperature: 0, maxTokens: 0, prompt: '' }), + extractTextAndResponseReason: () => ({ message: '', role: '', usage: {} }), + extractTextAndResponseReasonFromStream: () => ({ message: '', role: '', usage: {} }), + }, + }) + + plugin = new BedrockRuntimePlugin({}, { + llmobs: { enabled: true, mlApp: 'test' }, + service: 'test', + }) + plugin._tagger = { + registerLLMObsSpan () {}, + tagMetadata () {}, + tagLLMIO () {}, + tagMetrics: tagMetricsSpy, + } + plugin.configure({ enabled: true }) + }) + + afterEach(() => { + plugin.configure({ enabled: false }) + }) + + it('drops pending headers when complete fires for a non-LLM operation', () => { + publishDeserialize('req-non-llm', { input: 5, output: 3 }) + + completeCh.publish({ + response: { + request: { operation: 'getFoundationModel', params: {} }, + $metadata: { requestId: 'req-non-llm' }, + }, + }) + + sinon.assert.notCalled(tagMetricsSpy) + + // Reusing the request id surfaces a leak: zero header tokens means the + // previous :complete: cleaned the cache entry up. + completeCh.publish(buildLlmComplete('req-non-llm', 'amazon.titan')) + + sinon.assert.calledOnce(tagMetricsSpy) + assert.deepStrictEqual(tagMetricsSpy.firstCall.args[1], emptyMetrics()) + }) + + it('drops pending headers when complete fires for an embedding model', () => { + publishDeserialize('req-embed', { input: 5, output: 0 }) + + completeCh.publish(buildLlmComplete('req-embed', 'amazon.embed-text')) + + sinon.assert.notCalled(tagMetricsSpy) + + completeCh.publish(buildLlmComplete('req-embed', 'amazon.titan')) + + sinon.assert.calledOnce(tagMetricsSpy) + assert.deepStrictEqual(tagMetricsSpy.firstCall.args[1], emptyMetrics()) + }) + + it('passes the pending headers through to the matching LLM span', () => { + publishDeserialize('req-llm', { input: 7, output: 11, cacheRead: 2, cacheWrite: 1 }) + + completeCh.publish(buildLlmComplete('req-llm', 'amazon.titan')) + + sinon.assert.calledOnce(tagMetricsSpy) + assert.deepStrictEqual(tagMetricsSpy.firstCall.args[1], { + // Input tokens are normalized to also count cached tokens. + inputTokens: 7 + 2 + 1, + outputTokens: 11, + totalTokens: (7 + 2 + 1) + 11, + cacheReadTokens: 2, + cacheWriteTokens: 1, + }) + }) + + it('ignores deserialize without an x-amzn-requestid header', () => { + deserializeCh.publish({ + headers: { 'x-amzn-bedrock-input-token-count': '5' }, + }) + + completeCh.publish(buildLlmComplete('not-the-leaked-id', 'amazon.titan')) + + sinon.assert.calledOnce(tagMetricsSpy) + assert.deepStrictEqual(tagMetricsSpy.firstCall.args[1], emptyMetrics()) + }) + + function publishDeserialize (requestId, { input, output, cacheRead, cacheWrite } = {}) { + const headers = { 'x-amzn-requestid': requestId } + if (input != null) headers['x-amzn-bedrock-input-token-count'] = String(input) + if (output != null) headers['x-amzn-bedrock-output-token-count'] = String(output) + if (cacheRead != null) headers['x-amzn-bedrock-cache-read-input-token-count'] = String(cacheRead) + if (cacheWrite != null) headers['x-amzn-bedrock-cache-write-input-token-count'] = String(cacheWrite) + deserializeCh.publish({ headers }) + } + + function buildLlmComplete (requestId, modelId) { + return { + currentStore: { span: {} }, + response: { + request: { operation: 'invokeModel', params: { modelId } }, + $metadata: { requestId }, + }, + } + } + + function emptyMetrics () { + return { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + } + } +}) diff --git a/packages/dd-trace/test/llmobs/sdk/index.spec.js b/packages/dd-trace/test/llmobs/sdk/index.spec.js index ee5b64b996a..74e7c5925a6 100644 --- a/packages/dd-trace/test/llmobs/sdk/index.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/index.spec.js @@ -1619,7 +1619,7 @@ describe('sdk', () => { injectCh.publish({ carrier }) }) - assert.strictEqual(carrier['x-datadog-tags'], `,_dd.p.llmobs_parent_id=${parentId},_dd.p.llmobs_ml_app=mlApp`) + assert.strictEqual(carrier['x-datadog-tags'], `_dd.p.llmobs_parent_id=${parentId},_dd.p.llmobs_ml_app=mlApp`) }) }) }) diff --git a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js index 7fd1355d457..ffa8e77ba8c 100644 --- a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js @@ -10,6 +10,7 @@ const { channel } = require('dc-polyfill') const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../setup/core') const { getConfigFresh } = require('../../helpers/config') +const { DD_MAJOR } = require('../../../../../version') const id = require('../../../src/id') const SpanContext = require('../../../src/opentracing/span_context') const TraceState = require('../../../src/opentracing/propagation/tracestate') @@ -17,6 +18,9 @@ const { setBaggageItem, getBaggageItem, getAllBaggageItems, removeAllBaggageItem const { AUTO_KEEP, AUTO_REJECT, USER_KEEP } = require('../../../../../ext/priority') const { SAMPLING_MECHANISM_MANUAL } = require('../../../src/constants') +// v5 spells single-header B3 propagation as `'b3 single header'`; v6 reuses `'b3'` for it. +const B3_SINGLE_STYLE = DD_MAJOR >= 6 ? 'b3' : 'b3 single header' + const injectCh = channel('dd-trace:span:inject') const extractCh = channel('dd-trace:span:extract') @@ -325,7 +329,7 @@ describe('TextMapPropagator', () => { }, }) - config.tracePropagationStyle.inject = ['b3'] + config.tracePropagationStyle.inject = ['b3multi'] propagator.inject(spanContext, carrier) @@ -352,7 +356,7 @@ describe('TextMapPropagator', () => { }, }) - config.tracePropagationStyle.inject = ['b3'] + config.tracePropagationStyle.inject = ['b3multi'] propagator.inject(spanContext, carrier) @@ -371,7 +375,7 @@ describe('TextMapPropagator', () => { }, }) - config.tracePropagationStyle.inject = ['b3'] + config.tracePropagationStyle.inject = ['b3multi'] propagator.inject(spanContext, carrier) @@ -421,6 +425,73 @@ describe('TextMapPropagator', () => { assert.ok(!('x-b3-traceid' in carrier)) }) + it(`should inject the b3 single header when style is "${B3_SINGLE_STYLE}"`, () => { + const carrier = {} + const spanContext = createContext({ + traceId: id('0000000000000123'), + spanId: id('0000000000000456'), + sampling: { priority: USER_KEEP }, + }) + + config.tracePropagationStyle.inject = [B3_SINGLE_STYLE] + + propagator.inject(spanContext, carrier) + + assert.strictEqual(carrier.b3, '0000000000000123-0000000000000456-1') + assert.ok(!('x-b3-traceid' in carrier)) + }) + + if (DD_MAJOR >= 6) { + it('should treat inject:["b3"] as the single-header form on v6', () => { + const carrier = {} + const spanContext = createContext({ + traceId: id('0000000000000123'), + spanId: id('0000000000000456'), + sampling: { priority: USER_KEEP }, + }) + + config.tracePropagationStyle.inject = ['b3'] + + propagator.inject(spanContext, carrier) + + assert.strictEqual(carrier.b3, '0000000000000123-0000000000000456-1') + assert.ok(!('x-b3-traceid' in carrier)) + }) + + it('should treat inject:["b3multi"] as the multi-header form on v6', () => { + const carrier = {} + const spanContext = createContext({ + traceId: id('0000000000000123'), + spanId: id('0000000000000456'), + sampling: { priority: USER_KEEP }, + }) + + config.tracePropagationStyle.inject = ['b3multi'] + + propagator.inject(spanContext, carrier) + + assert.strictEqual(carrier['x-b3-traceid'], '0000000000000123') + assert.strictEqual(carrier['x-b3-spanid'], '0000000000000456') + assert.ok(!('b3' in carrier)) + }) + + it('should treat inject:["b3 single header"] as the single-header form on v6', () => { + const carrier = {} + const spanContext = createContext({ + traceId: id('0000000000000123'), + spanId: id('0000000000000456'), + sampling: { priority: USER_KEEP }, + }) + + config.tracePropagationStyle.inject = ['b3 single header'] + + propagator.inject(spanContext, carrier) + + assert.strictEqual(carrier.b3, '0000000000000123-0000000000000456-1') + assert.ok(!('x-b3-traceid' in carrier)) + }) + } + it('should skip injection of traceparent header without the feature flag', () => { const carrier = {} const spanContext = createContext({ @@ -1355,7 +1426,10 @@ describe('TextMapPropagator', () => { }) }) - describe('with B3 propagation from DD_TRACE_PROPAGATION_STYLE', () => { + // v6 routes `'b3'` to the single-header path regardless of source, so the v5-only + // dispatch-by-source distinction tested below has nothing left to assert on v6. + const describeOrSkip = DD_MAJOR < 6 ? describe : describe.skip + describeOrSkip('with B3 propagation from DD_TRACE_PROPAGATION_STYLE', () => { beforeEach(() => { config.tracePropagationStyle.extract = ['b3'] config.getOrigin = sinon.stub().withArgs('tracePropagationStyle.extract').returns('env_var') @@ -1427,7 +1501,7 @@ describe('TextMapPropagator', () => { describe('with B3 propagation as a single header', () => { beforeEach(() => { - config.tracePropagationStyle.extract = ['b3 single header'] + config.tracePropagationStyle.extract = [B3_SINGLE_STYLE] delete textMap['x-datadog-trace-id'] delete textMap['x-datadog-parent-id'] @@ -1572,7 +1646,7 @@ describe('TextMapPropagator', () => { sinon.assert.called(log.debug) assert.strictEqual( log.debug.firstCall.args[0](), - `Extract from carrier (b3 single header): {"b3":"${textMap.b3}"}.` + `Extract from carrier (${B3_SINGLE_STYLE}): {"b3":"${textMap.b3}"}.` ) }) }) diff --git a/packages/dd-trace/test/plugins/agent.js b/packages/dd-trace/test/plugins/agent.js index e11d258a6f6..d191b8cec1d 100644 --- a/packages/dd-trace/test/plugins/agent.js +++ b/packages/dd-trace/test/plugins/agent.js @@ -289,7 +289,7 @@ module.exports = { * @overload * @param {string | string[]} pluginNames - Name or list of names of plugins to load * @param {Record} [config] - * @param {Record} [tracerConfig={}] + * @param {Record} [tracerConfig] * @returns Promise */ /** @@ -298,7 +298,7 @@ module.exports = { * @overload * @param {string[]} pluginNames - Name or list of names of plugins to load * @param {Record[]} config - * @param {Record} [tracerConfig={}] + * @param {Record} [tracerConfig] * @returns Promise */ async load (pluginNames, config, tracerConfig = {}) { @@ -403,7 +403,7 @@ module.exports = { const promise = /** @type {Promise} */ (new Promise((resolve, _reject) => { listener = server.listen(0, () => { - const port = listener.address().port + const port = this.port = listener.address().port tracer.init({ service: 'test', @@ -570,9 +570,9 @@ module.exports = { * - wipe: false * * @param {object} [options] - * @param {boolean} [options.ritmReset=true] - Resets the Require In The Middle cache. You probably don't need this. - * @param {boolean} [options.wipe=false] - Wipes tracer and non-native modules from require cache. You probably don't - * need this. + * @param {boolean} [options.ritmReset] - Resets the Require In The Middle cache. + * @param {boolean} [options.wipe] - Wipes tracer and non-native modules from require cache. + * You probably don't need this. * @returns */ close ({ ritmReset = true, wipe = false } = {}) { @@ -607,6 +607,7 @@ module.exports = { return /** @type {Promise} */ (new Promise((resolve, reject) => { this.server.on('close', () => { this.server = null + this.port = null resolve() }) diff --git a/packages/dd-trace/test/plugins/database-dbm-cache.spec.js b/packages/dd-trace/test/plugins/database-dbm-cache.spec.js new file mode 100644 index 00000000000..0ed75c5560b --- /dev/null +++ b/packages/dd-trace/test/plugins/database-dbm-cache.spec.js @@ -0,0 +1,142 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') + +require('../setup/core') + +const DatabasePlugin = require('../../src/plugins/database') + +function makeSpan (tags = {}) { + return { + context: () => ({ _tags: tags }), + setTag () {}, + _spanContext: { toTraceparent: () => '00-aaa-bbb-01' }, + _processor: { sample () {} }, + } +} + +describe('DatabasePlugin DBM caching', () => { + let plugin + let tracer + let encodeSpy + + beforeEach(() => { + tracer = { + _service: 'svc', + _env: 'tester', + _version: '1.0.0', + } + plugin = new DatabasePlugin(tracer, {}) + plugin._tracerConfig = {} + plugin.configure({ dbmPropagationMode: 'service', enabled: true }) + }) + + afterEach(() => { + encodeSpy?.restore() + encodeSpy = undefined + }) + + it('captures dde / ddps / ddpv at configure time, not on every query', () => { + const span = makeSpan({ 'db.name': 'mydb', 'out.host': 'host1' }) + const first = plugin.createDbmComment(span, 'svc') + + assert.strictEqual( + first, + "dddb='mydb',dddbs='svc',dde='tester',ddh='host1',ddps='svc',ddpv='1.0.0'" + ) + + // Mutating the tracer's globals after configure must not affect the comment — the + // immutable suffix is baked in once. The original implementation re-read every field + // on every query. + tracer._env = 'shifted-env' + tracer._service = 'shifted-svc' + tracer._version = '2.0.0' + + assert.strictEqual(plugin.createDbmComment(span, 'svc'), first) + }) + + it('reuses the cached prefix across queries on the same (db, host, dbmService)', () => { + // Non-ASCII forces the `encodeURIComponent` path; the fast path skips ASCII inputs + // entirely, so we'd otherwise observe zero calls regardless of caching. + encodeSpy = sinon.spy(globalThis, 'encodeURIComponent') + const span = makeSpan({ 'db.name': 'müll', 'out.host': 'höst' }) + + plugin.createDbmComment(span, 'sërvice') + const callsAfterMiss = encodeSpy.callCount + assert.ok(callsAfterMiss >= 3, 'first call encodes db.name, host, and serviceName') + + plugin.createDbmComment(span, 'sërvice') + plugin.createDbmComment(span, 'sërvice') + assert.strictEqual(encodeSpy.callCount, callsAfterMiss, + 'cache hit must not encode again on identical (db, host, dbmService)') + + plugin.createDbmComment(makeSpan({ 'db.name': 'andërer', 'out.host': 'höst' }), 'sërvice') + assert.ok(encodeSpy.callCount > callsAfterMiss, + 'cache miss when db.name changes must re-encode') + }) + + it('skips encodeURIComponent for unreserved RFC 3986 characters', () => { + encodeSpy = sinon.spy(globalThis, 'encodeURIComponent') + const span = makeSpan({ 'db.name': 'safe-name.~_db', 'out.host': '127.0.0.1' }) + + const comment = plugin.createDbmComment(span, 'safe-svc') + assert.match(comment, /dddb='safe-name\.~_db'/) + assert.match(comment, /ddh='127\.0\.0\.1'/) + assert.match(comment, /dddbs='safe-svc'/) + assert.strictEqual(encodeSpy.callCount, 0, 'fast path bypasses encodeURIComponent') + }) + + it('falls back to encodeURIComponent on reserved characters', () => { + const span = makeSpan({ 'db.name': 'a&b', 'out.host': 'h$' }) + const comment = plugin.createDbmComment(span, 'sv c') + + assert.match(comment, /dddb='a%26b'/) + assert.match(comment, /ddh='h%24'/) + assert.match(comment, /dddbs='sv%20c'/) + }) + + it('configure() rebuilds the immutable suffix so reconfigured tracer fields take effect', () => { + const span = makeSpan({ 'db.name': 'mydb', 'out.host': 'host1' }) + assert.match(plugin.createDbmComment(span, 'svc'), /dde='tester'/) + + tracer._env = 'newenv' + plugin.configure({ dbmPropagationMode: 'service', enabled: true }) + + assert.match(plugin.createDbmComment(span, 'svc'), /dde='newenv'/) + }) + + it('appends ddprs= when peer-service is in scope', () => { + const span = makeSpan({ 'db.name': 'mydb', 'out.host': 'host1' }) + plugin.getPeerService = () => ({ + 'peer.service': 'downstream-svc', + '_dd.peer.service.source': 'peer.service', + }) + + const comment = plugin.createDbmComment(span, 'svc') + assert.match(comment, /,ddprs='downstream-svc'$/) + }) + + it('bounds the prefix cache so high-cardinality db.name cannot grow it without bound', () => { + // Boundary test for the LRU cap (DBM_PREFIX_CACHE_MAX = 256 in database.js). Insert + // CAP + 1 unique keys without re-accessing intermediate ones, so the first-inserted key + // is the LRU and gets evicted when the CAP-th unique insertion lands. + const CAP = 256 + encodeSpy = sinon.spy(globalThis, 'encodeURIComponent') + + for (let i = 0; i <= CAP; i++) { + plugin.createDbmComment(makeSpan({ 'db.name': `dëb-${i}`, 'out.host': 'höst' }), 'svc') + } + const callsAfterFill = encodeSpy.callCount + + plugin.createDbmComment(makeSpan({ 'db.name': `dëb-${CAP}`, 'out.host': 'höst' }), 'svc') + assert.strictEqual(encodeSpy.callCount, callsAfterFill, + 'most-recently inserted key must remain cached') + + plugin.createDbmComment(makeSpan({ 'db.name': 'dëb-0', 'out.host': 'höst' }), 'svc') + assert.ok(encodeSpy.callCount > callsAfterFill, + 'least-recently-used key must have been evicted at the cap') + }) +}) diff --git a/packages/dd-trace/test/plugins/util/ci-env/jenkins.json b/packages/dd-trace/test/plugins/util/ci-env/jenkins.json index 722a1a90d0d..12e753fe9cb 100644 --- a/packages/dd-trace/test/plugins/util/ci-env/jenkins.json +++ b/packages/dd-trace/test/plugins/util/ci-env/jenkins.json @@ -4,6 +4,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -14,7 +15,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -30,6 +31,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -39,7 +41,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -55,6 +57,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -64,7 +67,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -80,6 +83,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -90,7 +94,7 @@ "WORKSPACE": "foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -107,6 +111,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -117,7 +122,7 @@ "WORKSPACE": "/foo/bar~" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -134,6 +139,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -144,7 +150,7 @@ "WORKSPACE": "/foo/~/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -161,6 +167,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -173,7 +180,7 @@ "WORKSPACE": "~/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -190,6 +197,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -202,7 +210,7 @@ "WORKSPACE": "~foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -219,6 +227,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -231,7 +240,7 @@ "WORKSPACE": "~" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -248,6 +257,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -258,7 +268,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -275,6 +285,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "refs/heads/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -285,7 +296,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -302,6 +313,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "refs/heads/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -312,7 +324,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName/another", "ci.pipeline.number": "jenkins-pipeline-number", @@ -329,6 +341,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "refs/heads/feature/one", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -339,7 +352,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -356,6 +369,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "refs/heads/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -366,7 +380,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -383,6 +397,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "refs/heads/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -393,7 +408,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -410,6 +425,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/tags/0.1.0", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -419,7 +435,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -435,6 +451,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "refs/heads/tags/0.1.0", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -444,7 +461,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -460,6 +477,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -470,7 +488,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -487,6 +505,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -497,7 +516,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -514,6 +533,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -524,7 +544,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -541,6 +561,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -551,7 +572,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -568,6 +589,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_BRANCH": "origin/master", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -578,7 +600,7 @@ "WORKSPACE": "/foo/bar" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.name": "jobName", "ci.pipeline.number": "jenkins-pipeline-number", @@ -595,6 +617,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "DD_GIT_BRANCH": "user-supplied-branch", "DD_GIT_COMMIT_AUTHOR_DATE": "usersupplied-authordate", @@ -611,7 +634,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -633,6 +656,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "DD_GIT_COMMIT_AUTHOR_DATE": "usersupplied-authordate", "DD_GIT_COMMIT_AUTHOR_EMAIL": "usersupplied-authoremail", @@ -649,7 +673,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -671,6 +695,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "DD_TEST_CASE_NAME": "http-repository-url-no-git-suffix", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -679,7 +704,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -693,6 +718,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "DD_TEST_CASE_NAME": "ssh-repository-url-no-git-suffix", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", @@ -701,7 +727,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -715,6 +741,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "https://user:password@github.com/DataDog/dogweb.git", @@ -722,7 +749,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -736,6 +763,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "https://user@github.com/DataDog/dogweb.git", @@ -743,7 +771,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -757,6 +785,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "https://user:password@github.com:1234/DataDog/dogweb.git", @@ -764,7 +793,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -778,6 +807,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "https://user:password@1.1.1.1/DataDog/dogweb.git", @@ -785,7 +815,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -799,6 +829,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "https://user:password@1.1.1.1:1234/DataDog/dogweb.git", @@ -806,7 +837,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -820,6 +851,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "https://user:password@1.1.1.1:1234/DataDog/dogweb_with_@_yeah.git", @@ -827,7 +859,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -841,6 +873,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "ssh://user@host.xz:54321/path/to/repo.git/", @@ -848,7 +881,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -862,6 +895,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "GIT_URL_1": "ssh://user:password@host.xz:54321/path/to/repo.git/", @@ -869,7 +903,7 @@ "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", @@ -883,6 +917,7 @@ "BUILD_NUMBER": "jenkins-pipeline-number", "BUILD_TAG": "jenkins-pipeline-id", "BUILD_URL": "https://jenkins.com/pipeline", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "JENKINS_URL": "jenkins", @@ -891,7 +926,7 @@ "NODE_NAME": "my-node" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.node.labels": "[\"built-in\",\"linux\"]", "ci.node.name": "my-node", "ci.pipeline.id": "jenkins-pipeline-id", @@ -908,13 +943,14 @@ "BUILD_URL": "https://jenkins.com/pipeline", "CHANGE_ID": 42, "CHANGE_TARGET": "target-branch", + "DD_CUSTOM_PARENT_ID": "jenkins-custom-parent-id", "DD_CUSTOM_TRACE_ID": "jenkins-custom-trace-id", "GIT_COMMIT": "b9f0fb3fdbb94c9d24b2c75b49663122a529e123", "JENKINS_URL": "jenkins", "JOB_URL": "https://jenkins.com/job" }, { - "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\"}", + "_dd.ci.env_vars": "{\"DD_CUSTOM_TRACE_ID\":\"jenkins-custom-trace-id\",\"DD_CUSTOM_PARENT_ID\":\"jenkins-custom-parent-id\"}", "ci.pipeline.id": "jenkins-pipeline-id", "ci.pipeline.number": "jenkins-pipeline-number", "ci.pipeline.url": "https://jenkins.com/pipeline", diff --git a/packages/dd-trace/test/plugins/util/stacktrace.spec.js b/packages/dd-trace/test/plugins/util/stacktrace.spec.js index 54b74749b0c..6c2983549f3 100644 --- a/packages/dd-trace/test/plugins/util/stacktrace.spec.js +++ b/packages/dd-trace/test/plugins/util/stacktrace.spec.js @@ -1,7 +1,7 @@ 'use strict' const assert = require('node:assert') -const { join } = require('node:path') +const { join, sep } = require('node:path') const { describe, it } = require('mocha') @@ -409,6 +409,19 @@ describe('stacktrace utils', () => { }) }) + describe('dd-trace instrumentation frames', () => { + it('should filter instrumentation frames regardless of the repo directory name', () => { + // Regression: previously only filtered when directory was named exactly 'dd-trace-js' + for (const repoName of ['dd-trace-js', 'dd-trace-js-1', 'my-dd-trace-fork', 'tracer']) { + const instrFile = join(`${sep}${repoName}`, 'packages', 'datadog-instrumentations', 'src', 'express.js') + const instrumentationFrame = ` at wrappedUse (${instrFile}:144:16)` + const userFrame = ' at testCase (/user/app/test.js:10:5)' + const stack = `Error: test\n${instrumentationFrame}\n${userFrame}` + assert.deepStrictEqual(parseUserLandFrames(stack).length, 1, `failed for repo name: ${repoName}`) + } + }) + }) + describe('user-land frame', () => { it('should should only return user-land frames', () => { const stack = genStackTraceWithManyNonUserLandFramesAnd( diff --git a/packages/dd-trace/test/plugins/util/test-environment.spec.js b/packages/dd-trace/test/plugins/util/test-environment.spec.js index 78970b2fb95..d0fe21d89be 100644 --- a/packages/dd-trace/test/plugins/util/test-environment.spec.js +++ b/packages/dd-trace/test/plugins/util/test-environment.spec.js @@ -13,24 +13,36 @@ require('../../setup/core') const cachedExecStub = sinon.stub().returns('') -const { getCIMetadata, expandGlobPattern, getJobIDFromDiagFile } = require('../../../src/plugins/util/ci') +const { expandGlobPattern, getJobIDFromDiagFile } = require('../../../src/plugins/util/ci') const { CI_ENV_VARS, CI_NODE_LABELS, GIT_PULL_REQUEST_BASE_BRANCH, GIT_PULL_REQUEST_BASE_BRANCH_HEAD_SHA, GIT_COMMIT_HEAD_SHA, + PR_NUMBER, } = require('../../../src/plugins/util/tags') +const freshConfig = () => proxyquire.noPreserveCache()('../../../src/config', {})() + const { getGitMetadata } = proxyquire('../../../src/plugins/util/git', { './git-cache': { cachedExec: cachedExecStub, }, }) +const ciModule = proxyquire('../../../src/plugins/util/ci', { + '../../config': freshConfig, +}) +const { getCIMetadata } = ciModule +const userProvidedGit = proxyquire('../../../src/plugins/util/user-provided-git', { + '../../config': freshConfig, +}) const { getTestEnvironmentMetadata } = proxyquire('../../../src/plugins/util/test', { + './ci': ciModule, './git': { getGitMetadata, }, + './user-provided-git': userProvidedGit, }) describe('test environment data', () => { @@ -111,6 +123,27 @@ describe('test environment data', () => { }) }) }) + + it('does not set pr.number on Buildkite when BUILDKITE_PULL_REQUEST is the literal string false', () => { + process.env = { + BUILDKITE: 'true', + BUILDKITE_BUILD_ID: 'buildkite-pipeline-id', + BUILDKITE_JOB_ID: 'buildkite-job-id', + BUILDKITE_PIPELINE_SLUG: 'buildkite-pipeline-name', + BUILDKITE_BUILD_NUMBER: 'buildkite-pipeline-number', + BUILDKITE_BUILD_URL: 'https://buildkite-build-url.com', + BUILDKITE_BRANCH: 'gh-readonly-queue/main/pr-1234-abcdef0123456789abcdef0123456789abcdef01', + BUILDKITE_COMMIT: 'b9f0fb3fdbb94c9d24b2c75b49663122a529e123', + BUILDKITE_REPO: 'http://hostname.com/repo.git', + BUILDKITE_PULL_REQUEST: 'false', + BUILDKITE_PULL_REQUEST_BASE_BRANCH: 'main', + } + + const tags = getCIMetadata() + + assert.strictEqual(tags[PR_NUMBER], undefined) + assert.strictEqual(tags[GIT_PULL_REQUEST_BASE_BRANCH], undefined) + }) }) describe('expandGlobPattern', () => { diff --git a/packages/dd-trace/test/plugins/versions/package.json b/packages/dd-trace/test/plugins/versions/package.json index d0910154f18..13a3a52766b 100644 --- a/packages/dd-trace/test/plugins/versions/package.json +++ b/packages/dd-trace/test/plugins/versions/package.json @@ -27,10 +27,11 @@ "durable-functions": "3.3.0", "@azure/service-bus": "7.9.5", "@confluentinc/kafka-javascript": "1.8.0", - "@cucumber/cucumber": "12.5.0", + "@cucumber/cucumber": "12.8.2", "@datadog/openfeature-node-server": "0.3.1", "@elastic/elasticsearch": "9.3.2", "@elastic/transport": "9.3.3", + "@electron/packager": "19.1.0", "@fast-check/jest": "2.1.1", "@fastify/cookie": "11.0.2", "@fastify/multipart": "9.4.0", @@ -44,11 +45,11 @@ "@hapi/hapi": "21.4.4", "@happy-dom/jest-environment": "20.3.1", "@hono/node-server": "1.19.9", - "@jest/core": "30.2.0", - "@jest/globals": "30.2.0", - "@jest/reporters": "30.2.0", - "@jest/test-sequencer": "30.2.0", - "@jest/transform": "30.2.0", + "@jest/core": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/reporters": "30.4.1", + "@jest/test-sequencer": "30.4.1", + "@jest/transform": "30.4.1", "@koa/router": "15.2.0", "@langchain/anthropic": "1.3.10", "@langchain/classic": "1.0.9", @@ -70,7 +71,7 @@ "@opentelemetry/instrumentation-express": "0.58.0", "@opentelemetry/instrumentation-http": "0.210.0", "@opentelemetry/sdk-node": "0.210.0", - "@playwright/test": "1.57.0", + "@playwright/test": "1.59.1", "@prisma/client": "7.2.0", "@prisma/adapter-pg": "7.2.0", "@prisma/adapter-mariadb": "7.2.0", @@ -78,9 +79,9 @@ "@redis/client": "5.10.0", "@smithy/smithy-client": "4.10.9", "@types/node": "25.0.9", - "@vitest/coverage-istanbul": "4.1.0", - "@vitest/coverage-v8": "4.1.0", - "@vitest/runner": "4.1.0", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/runner": "4.1.5", "aerospike": "6.5.2", "ai": "6.0.39", "amqp10": "3.6.0", @@ -91,7 +92,7 @@ "avsc": "5.7.9", "aws-sdk": "2.1693.0", "axios": "1.13.2", - "babel-jest": "30.2.0", + "babel-jest": "30.4.1", "azure-functions-core-tools": "4.6.0", "bluebird": "3.7.2", "body-parser": "2.2.2", @@ -104,11 +105,12 @@ "cookie": "1.1.1", "cookie-parser": "1.4.7", "couchbase": "4.6.0", - "cypress": "15.13.0", + "cypress": "15.14.2", "cypress-fail-fast": "7.1.1", "dd-trace-api": "1.0.0", "ejs": "4.0.1", "elasticsearch": "16.7.3", + "electron": "39.2.4", "esbuild": "0.27.2", "express": "5.2.1", "express-mongo-sanitize": "2.2.0", @@ -126,15 +128,15 @@ "hono": "4.11.7", "ioredis": "5.9.2", "iovalkey": "0.3.3", - "jest": "30.2.0", - "jest-circus": "30.2.0", - "jest-config": "30.2.0", - "jest-environment-jsdom": "30.2.0", - "jest-environment-node": "30.2.0", + "jest": "30.4.1", + "jest-circus": "30.4.1", + "jest-config": "30.4.1", + "jest-environment-jsdom": "30.4.1", + "jest-environment-node": "30.4.1", "jest-image-snapshot": "6.5.1", - "jest-jasmine2": "30.2.0", - "jest-runtime": "30.2.0", - "jest-worker": "30.2.0", + "jest-jasmine2": "30.4.1", + "jest-runtime": "30.4.1", + "jest-worker": "30.4.1", "kafkajs": "2.2.4", "knex": "3.1.0", "koa": "3.1.1", @@ -179,8 +181,8 @@ "pg-query-stream": "4.11.1", "pino": "10.2.0", "pino-pretty": "13.1.3", - "playwright": "1.57.0", - "playwright-core": "1.57.0", + "playwright": "1.59.1", + "playwright-core": "1.59.1", "pnpm": "10.28.0", "prisma": "7.2.0", "promise": "8.3.0", @@ -205,7 +207,7 @@ "tinypool": "2.1.0", "typescript": "6.0.2", "undici": "7.18.2", - "vitest": "4.1.0", + "vitest": "4.1.5", "when": "3.7.8", "winston": "3.19.0", "workerpool": "10.0.1", diff --git a/packages/dd-trace/test/ritm.spec.js b/packages/dd-trace/test/ritm.spec.js index 7cabc9e60c8..81022fc0e6f 100644 --- a/packages/dd-trace/test/ritm.spec.js +++ b/packages/dd-trace/test/ritm.spec.js @@ -124,4 +124,29 @@ describe('Ritm', () => { assert.equal(startListener.callCount, 1) assert.equal(endListener.callCount, 1) }) + + it('should use moduleId as cache key for node:-prefixed built-ins', () => { + // Populate RITM cache keyed by normalized moduleId 'util' + require('util') + + // Simulate require.cache having an entry for the node:-prefixed filename. + // In Node.js 18+, Module._resolveFilename('node:util') returns 'node:util', + // so filename differs from moduleId ('util'). Before the fix, the cache + // comparison accessed cache[filename] ('node:util') which was undefined, + // causing: TypeError: undefined is not an object (evaluating 'cache[filename].original') + const prefixedKey = 'node:util' + const saved = require.cache[prefixedKey] + require.cache[prefixedKey] = { exports: { patched: true } } + + try { + const result = require('node:util') + assert.deepStrictEqual(result, { patched: true }) + } finally { + if (saved) { + require.cache[prefixedKey] = saved + } else { + delete require.cache[prefixedKey] + } + } + }) }) diff --git a/packages/dd-trace/test/runtime_metrics.spec.js b/packages/dd-trace/test/runtime_metrics.spec.js index 966f627ce6a..fe921910cc4 100644 --- a/packages/dd-trace/test/runtime_metrics.spec.js +++ b/packages/dd-trace/test/runtime_metrics.spec.js @@ -212,6 +212,7 @@ function createGarbage (count = 50) { obj: {}, invalid: 't{e*s#t5-:./', }, + DD_RUNTIME_METRICS_FLUSH_INTERVAL: 10000, getOrigin: () => { return 'default' }, diff --git a/packages/dd-trace/test/serverless.spec.js b/packages/dd-trace/test/serverless.spec.js new file mode 100644 index 00000000000..2220a393833 --- /dev/null +++ b/packages/dd-trace/test/serverless.spec.js @@ -0,0 +1,38 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { describe, it, afterEach } = require('mocha') + +require('./setup/core') + +const { enableGCPPubSubPushSubscription } = require('../src/serverless') + +describe('enableGCPPubSubPushSubscription', () => { + const originalKService = process.env.K_SERVICE + const originalGcpPubsubPush = process.env.DD_TRACE_GCP_PUBSUB_PUSH_ENABLED + + afterEach(() => { + if (originalKService === undefined) delete process.env.K_SERVICE + else process.env.K_SERVICE = originalKService + if (originalGcpPubsubPush === undefined) delete process.env.DD_TRACE_GCP_PUBSUB_PUSH_ENABLED + else process.env.DD_TRACE_GCP_PUBSUB_PUSH_ENABLED = originalGcpPubsubPush + }) + + it('is false when K_SERVICE is not set', () => { + delete process.env.K_SERVICE + assert.strictEqual(enableGCPPubSubPushSubscription(), false) + }) + + it('is true when K_SERVICE is set and the env var defaults to true', () => { + process.env.K_SERVICE = 'svc' + delete process.env.DD_TRACE_GCP_PUBSUB_PUSH_ENABLED + assert.strictEqual(enableGCPPubSubPushSubscription(), true) + }) + + it('is false when the user opts out via DD_TRACE_GCP_PUBSUB_PUSH_ENABLED=false', () => { + process.env.K_SERVICE = 'svc' + process.env.DD_TRACE_GCP_PUBSUB_PUSH_ENABLED = 'false' + assert.strictEqual(enableGCPPubSubPushSubscription(), false) + }) +}) diff --git a/packages/dd-trace/test/setup/core.js b/packages/dd-trace/test/setup/core.js index e553fa8824e..133c0647d6a 100644 --- a/packages/dd-trace/test/setup/core.js +++ b/packages/dd-trace/test/setup/core.js @@ -49,6 +49,17 @@ if (!globalThis[Symbol.for('dd-trace')]) { // Override per-test, if absolutely necessary. require('events').defaultMaxListeners = 6 +// Patch timer unref() to always return null, simulating Electron behavior +// where unref() does not return the timer. Ensures code uses unref?.() not unref(). +const _t = setTimeout(() => {}, 0) +const _TimeoutProto = Object.getPrototypeOf(_t) +clearTimeout(_t) +const _origUnref = _TimeoutProto.unref +_TimeoutProto.unref = function () { + _origUnref.call(this) + return null +} + // Warnings that should not be thrown const warningExceptions = new Set([ // Node.js core warnings. Ignore them. diff --git a/packages/dd-trace/test/setup/helpers/load-inst.js b/packages/dd-trace/test/setup/helpers/load-inst.js index 6ee8971ae83..4f1a2ad912e 100644 --- a/packages/dd-trace/test/setup/helpers/load-inst.js +++ b/packages/dd-trace/test/setup/helpers/load-inst.js @@ -2,21 +2,43 @@ const fs = require('fs') const path = require('path') -const proxyquire = require('proxyquire') + +const INSTRUMENT_HELPER_PATH = path.join( + __dirname, '../../../../datadog-instrumentations/src/helpers/instrument' +) function loadInstFile (file, instrumentations) { - const instrument = { - addHook (instrumentation) { + const instPath = path.join(__dirname, `../../../../datadog-instrumentations/src/${file}`) + + // Patch `addHook` for the duration of this load and filter to the SUT's own + // call sites; addHook calls from transitively-loaded siblings (e.g. + // `router.js` from `express.js`) are dropped — each caller has its own + // `getInstrumentation(name)` that captures them. + const realInstrument = require(INSTRUMENT_HELPER_PATH) + const originalAddHook = realInstrument.addHook + realInstrument.addHook = (instrumentation) => { + const callerFrame = new Error().stack?.split('\n', 4)[2] ?? '' + if (callerFrame.includes(instPath)) { instrumentations.push(instrumentation) - }, + } } - const instPath = path.join(__dirname, `../../../../datadog-instrumentations/src/${file}`) + // Snapshot `require.cache` and drop everything this load adds, so production's + // `helpers/register.js` re-evaluation finds an empty cache and re-runs the + // integration's top-level `addHook` calls. + const cacheBefore = new Set(Object.keys(require.cache)) - proxyquire.noPreserveCache()(instPath, { - './helpers/instrument': instrument, - '../helpers/instrument': instrument, - }) + try { + delete require.cache[instPath] + require(instPath) + } finally { + realInstrument.addHook = originalAddHook + for (const id of Object.keys(require.cache)) { + if (!cacheBefore.has(id)) { + delete require.cache[id] + } + } + } } function loadOneInst (name) { diff --git a/packages/dd-trace/test/standalone/index.spec.js b/packages/dd-trace/test/standalone/index.spec.js index eec6d638cf0..9d3e9ad9ab9 100644 --- a/packages/dd-trace/test/standalone/index.spec.js +++ b/packages/dd-trace/test/standalone/index.spec.js @@ -36,7 +36,7 @@ describe('Disabled APM Tracing or Standalone', () => { apmTracingEnabled: false, tracePropagationStyle: { - inject: ['datadog', 'tracecontext', 'b3'], + inject: ['datadog', 'tracecontext', 'b3multi'], extract: ['datadog'], }, } diff --git a/scripts/install_plugin_modules.js b/scripts/install_plugin_modules.js index 0e3ad14af7a..d475c22afdb 100644 --- a/scripts/install_plugin_modules.js +++ b/scripts/install_plugin_modules.js @@ -17,7 +17,7 @@ const exec = require('./helpers/exec') const requirePackageJsonPath = require.resolve('../packages/dd-trace/src/require-package-json') // Can remove aerospike after removing support for aerospike < 5.2.0 (for Node.js 22, v5.12.1 is required) -// Can remove couchbase after removing support for couchbase <= 3.2.0 +// Can remove couchbase after removing support for couchbase < 3.2.2 const excludeList = arch() === 'arm64' ? ['aerospike', 'couchbase', 'grpc', 'oracledb'] : [] const workspaces = new Set() const externalDeps = new Map() @@ -304,7 +304,7 @@ async function assertWorkspaces () { } /** - * @param {boolean} [retry=true] + * @param {boolean} [retry] */ function install (retry = true) { try { diff --git a/scripts/mocha-parallel-files.js b/scripts/mocha-parallel-files.js index edba4611bac..74bf1fa285f 100644 --- a/scripts/mocha-parallel-files.js +++ b/scripts/mocha-parallel-files.js @@ -288,7 +288,7 @@ async function main () { child.kill('SIGKILL') } process.exit() - }, 1000).unref() + }, 1000).unref?.() } process.once('SIGINT', () => onSignal('SIGINT')) process.once('SIGTERM', () => onSignal('SIGTERM')) diff --git a/scripts/run-ci-on-community-pr.sh b/scripts/run-ci-on-community-pr.sh index 0ccb28accb9..65b6a320b07 100755 --- a/scripts/run-ci-on-community-pr.sh +++ b/scripts/run-ci-on-community-pr.sh @@ -33,7 +33,10 @@ if [ -z "$PR_ID" ]; then echo echo "This will:" echo "1. Rebase the PR with its target branch, with each contained commit signed." - echo "2. Create a temporary PR to allow running CI on the community PR." + echo "2. Create or update a temporary PR to allow running CI on the community PR." + echo + echo "Re-running with the same picks up any new commits on the source" + echo "branch and force-pushes them onto the existing temporary branch and PR." echo echo "Options:" echo " --check, -c Verify GitHub CLI installation and authentication, then exit." @@ -50,7 +53,24 @@ check_gh GITHUB_USER=$(gh api user --jq .login) BASE_COMMIT=$(gh pr view $PR_ID --json baseRefOid --jq '.baseRefOid') -TEMP_BRANCH=$GITHUB_USER/tmp-ci-run-$PR_ID-$(date +%s) +PR_BRANCH=$(gh pr view $PR_ID --json headRefName --jq '.headRefName') +TEMP_BRANCH_PREFIX=$GITHUB_USER/tmp-ci-run-$PR_ID- + +EXISTING_TEMP_BRANCH=$(gh pr list \ + --repo DataDog/dd-trace-js \ + --state open \ + --author "@me" \ + --json headRefName \ + --jq ".[] | select(.headRefName | startswith(\"$TEMP_BRANCH_PREFIX\")) | .headRefName" \ + | head -n1) + +if [ -n "$EXISTING_TEMP_BRANCH" ]; then + TEMP_BRANCH=$EXISTING_TEMP_BRANCH + echo "Updating existing temporary branch from a previous run: $TEMP_BRANCH" +else + TEMP_BRANCH=${TEMP_BRANCH_PREFIX}$(date +%s) +fi + WORKTREE_DIR=$(mktemp -d) ORIGINAL_DIR=$(pwd) @@ -59,6 +79,9 @@ cleanup() { cd "$ORIGINAL_DIR" 2>/dev/null || true git worktree remove "$WORKTREE_DIR" 2>/dev/null || true fi + if [ -n "${PR_BRANCH:-}" ]; then + git branch -D "$PR_BRANCH" 2>/dev/null || true + fi } trap cleanup EXIT ERR INT @@ -67,16 +90,18 @@ git worktree add $WORKTREE_DIR cd $WORKTREE_DIR git fetch origin -gh pr checkout $PR_ID --repo DataDog/dd-trace-js +gh pr checkout $PR_ID --repo DataDog/dd-trace-js --force git rebase --gpg-sign $BASE_COMMIT git push --force -git push origin HEAD:$TEMP_BRANCH +git push --force origin HEAD:$TEMP_BRANCH -gh pr create \ - --repo DataDog/dd-trace-js \ - --head $TEMP_BRANCH \ - --draft \ - --label "semver-patch" \ - --title "Temp: Run CI on community PR #$PR_ID" \ - --body "This is a temporary PR to allow running CI on the community PR #$PR_ID. It should be closed after CI has completed running." +if [ -z "$EXISTING_TEMP_BRANCH" ]; then + gh pr create \ + --repo DataDog/dd-trace-js \ + --head $TEMP_BRANCH \ + --draft \ + --label "semver-patch" \ + --title "Temp: Run CI on community PR #$PR_ID" \ + --body "This is a temporary PR to allow running CI on the community PR #$PR_ID. It should be closed after CI has completed running." +fi gh pr view $TEMP_BRANCH --web --repo DataDog/dd-trace-js diff --git a/supported_versions_output.json b/supported_versions_output.json index 933bb2c7a15..ee7c7f617ec 100644 --- a/supported_versions_output.json +++ b/supported_versions_output.json @@ -52,7 +52,7 @@ "dependency": "@cucumber/cucumber", "integration": "cucumber", "minimum_tracer_supported": "7.0.0", - "max_tracer_supported": "12.5.0", + "max_tracer_supported": "12.8.2", "auto-instrumented": "True" }, { @@ -115,21 +115,21 @@ "dependency": "@jest/core", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "@jest/test-sequencer", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "@jest/transform", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { @@ -199,7 +199,7 @@ "dependency": "@vitest/runner", "integration": "vitest", "minimum_tracer_supported": "1.6.0", - "max_tracer_supported": "4.1.0", + "max_tracer_supported": "4.1.5", "auto-instrumented": "True" }, { @@ -282,7 +282,7 @@ { "dependency": "couchbase", "integration": "couchbase", - "minimum_tracer_supported": "2.6.12", + "minimum_tracer_supported": "3.0.7", "max_tracer_supported": "4.6.0", "auto-instrumented": "True" }, @@ -290,7 +290,7 @@ "dependency": "cypress", "integration": "cypress", "minimum_tracer_supported": "12.0.0", - "max_tracer_supported": "15.13.0", + "max_tracer_supported": "15.14.2", "auto-instrumented": "True" }, { @@ -314,6 +314,13 @@ "max_tracer_supported": "16.7.3", "auto-instrumented": "True" }, + { + "dependency": "electron", + "integration": "electron", + "minimum_tracer_supported": "37.0.0", + "max_tracer_supported": "39.2.4", + "auto-instrumented": "True" + }, { "dependency": "express", "integration": "express", @@ -395,42 +402,42 @@ "dependency": "jest-circus", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "jest-config", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "jest-environment-jsdom", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "jest-environment-node", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "jest-runtime", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { "dependency": "jest-worker", "integration": "jest", "minimum_tracer_supported": "28.0.0", - "max_tracer_supported": "30.2.0", + "max_tracer_supported": "30.4.1", "auto-instrumented": "True" }, { @@ -626,7 +633,7 @@ "dependency": "playwright", "integration": "playwright", "minimum_tracer_supported": "1.38.0", - "max_tracer_supported": "1.57.0", + "max_tracer_supported": "1.59.1", "auto-instrumented": "True" }, { @@ -703,7 +710,7 @@ "dependency": "vitest", "integration": "vitest", "minimum_tracer_supported": "1.6.0", - "max_tracer_supported": "4.1.0", + "max_tracer_supported": "4.1.5", "auto-instrumented": "True" }, { diff --git a/supported_versions_table.csv b/supported_versions_table.csv index 3fa98ead944..3d2c32d4709 100644 --- a/supported_versions_table.csv +++ b/supported_versions_table.csv @@ -6,7 +6,7 @@ dependency,integration,minimum_tracer_supported,max_tracer_supported,auto-instru @azure/functions,azure-functions,4.0.0,4.11.0,True @azure/service-bus,azure-service-bus,7.9.2,7.9.5,True @confluentinc/kafka-javascript,confluentinc-kafka-javascript,1.0.0,1.8.0,True -@cucumber/cucumber,cucumber,7.0.0,12.5.0,True +@cucumber/cucumber,cucumber,7.0.0,12.8.2,True @elastic/elasticsearch,elasticsearch,5.6.16,9.3.2,True @elastic/transport,elasticsearch,8.0.0,9.3.3,True @google-cloud/pubsub,google-cloud-pubsub,1.2.0,5.2.2,True @@ -15,9 +15,9 @@ dependency,integration,minimum_tracer_supported,max_tracer_supported,auto-instru @grpc/grpc-js,grpc,1.0.3,1.14.3,True @hapi/hapi,hapi,17.9.0,21.4.4,True @happy-dom/jest-environment,jest,10.0.0,20.3.1,True -@jest/core,jest,28.0.0,30.2.0,True -@jest/test-sequencer,jest,28.0.0,30.2.0,True -@jest/transform,jest,28.0.0,30.2.0,True +@jest/core,jest,28.0.0,30.4.1,True +@jest/test-sequencer,jest,28.0.0,30.4.1,True +@jest/transform,jest,28.0.0,30.4.1,True @koa/router,koa,8.0.0,15.2.0,True @langchain/core,langchain,0.1.0,1.1.16,True @langchain/langgraph,langgraph,1.1.2,1.1.2,True @@ -27,7 +27,7 @@ dependency,integration,minimum_tracer_supported,max_tracer_supported,auto-instru @prisma/client,prisma,6.1.0,7.2.0,True @redis/client,redis,1.1.0,5.10.0,True @smithy/smithy-client,aws-sdk,1.0.3,4.10.9,True -@vitest/runner,vitest,1.6.0,4.1.0,True +@vitest/runner,vitest,1.6.0,4.1.5,True aerospike,aerospike,4.0.0,6.5.2,True ai,ai,4.0.0,6.0.39,True amqp10,amqp10,3.0.0,3.6.0,True @@ -39,11 +39,12 @@ bunyan,bunyan,1.0.0,2.0.5,True cassandra-driver,cassandra-driver,3.0.0,4.8.0,True child_process,child_process,18.0.0,25.9.0,True connect,connect,2.2.2,3.7.0,True -couchbase,couchbase,2.6.12,4.6.0,True -cypress,cypress,12.0.0,15.13.0,True +couchbase,couchbase,3.0.7,4.6.0,True +cypress,cypress,12.0.0,15.14.2,True dns,dns,18.0.0,25.9.0,True durable-functions,azure-durable-functions,3.0.0,3.3.0,True elasticsearch,elasticsearch,10.0.0,16.7.3,True +electron,electron,37.0.0,39.2.4,True express,express,4.0.0,5.2.1,True fastify,fastify,1.0.0,5.7.1,True find-my-way,find-my-way,1.0.0,9.4.0,True @@ -55,12 +56,12 @@ http2,http2,18.0.0,25.9.0,True https,http,18.0.0,25.9.0,True ioredis,ioredis,2.0.0,5.9.2,True iovalkey,iovalkey,0.0.1,0.3.3,True -jest-circus,jest,28.0.0,30.2.0,True -jest-config,jest,28.0.0,30.2.0,True -jest-environment-jsdom,jest,28.0.0,30.2.0,True -jest-environment-node,jest,28.0.0,30.2.0,True -jest-runtime,jest,28.0.0,30.2.0,True -jest-worker,jest,28.0.0,30.2.0,True +jest-circus,jest,28.0.0,30.4.1,True +jest-config,jest,28.0.0,30.4.1,True +jest-environment-jsdom,jest,28.0.0,30.4.1,True +jest-environment-node,jest,28.0.0,30.4.1,True +jest-runtime,jest,28.0.0,30.4.1,True +jest-worker,jest,28.0.0,30.4.1,True kafkajs,kafkajs,1.4.0,2.2.4,True koa,koa,2.0.0,3.1.1,True koa-router,koa,7.0.0,14.0.0,True @@ -88,7 +89,7 @@ oracledb,oracledb,5.0.0,6.10.0,True pg,pg,8.0.3,8.17.1,True pino,pino,2.0.0,10.2.0,True pino-pretty,pino,1.0.0,13.1.3,True -playwright,playwright,1.38.0,1.57.0,True +playwright,playwright,1.38.0,1.59.1,True protobufjs,protobufjs,6.8.0,8.0.0,True redis,redis,0.12.0,5.10.0,True restify,restify,3.0.0,11.1.0,True @@ -99,7 +100,7 @@ sharedb,sharedb,1.0.0,5.2.2,True tedious,tedious,1.0.0,19.2.0,True tinypool,vitest,0.8.0,2.1.0,True undici,undici,4.4.1,7.18.2,True -vitest,vitest,1.6.0,4.1.0,True +vitest,vitest,1.6.0,4.1.5,True winston,winston,1.0.0,3.19.0,True workerpool,mocha,6.0.0,10.0.1,True ws,ws,8.0.0,8.19.0,True diff --git a/yarn.lock b/yarn.lock index 313591b646c..c7b8ffc5cf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -216,15 +216,15 @@ dependencies: node-gyp-build "^3.9.0" -"@datadog/native-metrics@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.1.tgz#4e5c9775751af13e353e64e573ab724104538cee" - integrity sha512-MU1gHrolwryrU4X9g+fylA1KPH3S46oqJPEtVyrO+3Kh29z80fegmtyrU22bNt8LigPUK/EdPCnSbMe88QbnxQ== +"@datadog/native-metrics@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.2.tgz#9dc269bdbc6f5b5c9a30dc6d99bab44d17dd5a37" + integrity sha512-7AEWt0ZLr/ogR/9if1DmFBDTg3y67xM+gdhXUXKs+UQMxK0lnjrOHgN7fkpEmUG1uL+EkX2BDE3ENDlQ23J7OQ== dependencies: node-addon-api "^6.1.0" node-gyp-build "^3.9.0" -"@datadog/openfeature-node-server@^1.1.2": +"@datadog/openfeature-node-server@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@datadog/openfeature-node-server/-/openfeature-node-server-1.1.2.tgz#fd5ec4ba80e60464ff2aacae251bb500887956e0" integrity sha512-fr+zCaKoCSdizX22H7eA9Z3QaZrOMu5qU0yK0M2aWtUxokSAKPlLz3+9HdmqwBWU/ikqI+lbiAK0oNdvmUKeQA== @@ -446,6 +446,18 @@ resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.3.tgz#c4bff2b9539faf0882f3ee03537a7e9a4b3a7864" integrity sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA== +"@mswjs/interceptors@^0.41.0": + version "0.41.8" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.41.8.tgz#85ef74560f15d401dfe13798e6dabac15e457d6a" + integrity sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A== + dependencies: + "@open-draft/deferred-promise" "^2.2.0" + "@open-draft/logger" "^0.3.0" + "@open-draft/until" "^2.0.0" + is-node-process "^1.2.0" + outvariant "^1.4.3" + strict-event-emitter "^0.5.1" + "@napi-rs/wasm-runtime@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz#a46bbfedc29751b7170c5d23bc1d8ee8c7e3c1e1" @@ -670,6 +682,24 @@ "@octokit/request-error" "^7.0.0" "@octokit/webhooks-methods" "^6.0.0" +"@open-draft/deferred-promise@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" + integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== + +"@open-draft/logger@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" + integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ== + dependencies: + is-node-process "^1.2.0" + outvariant "^1.4.0" + +"@open-draft/until@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" + integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== + "@openfeature/core@^1.10.0": version "1.10.0" resolved "https://registry.yarnpkg.com/@openfeature/core/-/core-1.10.0.tgz#6fdcc2e00909bc0de45eae602c6650604bff1ed0" @@ -752,114 +782,114 @@ resolved "https://registry.yarnpkg.com/@oven/bun-windows-x64/-/bun-windows-x64-1.3.13.tgz#50c46e195061cd559edf49cc30d91c9e856b8249" integrity sha512-vqDEFX63ZZQF3YstPSpPD+RxNm5AILPdUuuKpNwsj7ld4NjhdHUYkAmLXDtKNWt9JMRL10bop//W8faY/LV+RQ== -"@oxc-parser/binding-android-arm-eabi@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.128.0.tgz#347b05c79052fa0030415bace1fda47de933d73d" - integrity sha512-aca6ZvzmCBUGOANQRiRQRZuRKYI3ENhcit6GisnknOOmcezfQc7xJ4dxlPU7MV7mOvrC7RNR1u3LAD7xyaiCxA== - -"@oxc-parser/binding-android-arm64@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.128.0.tgz#5a3e41daf46d3661dfd1467fc6f3e0efdac3727f" - integrity sha512-BbeDmuohoJ7Rz/it5wnkj69i/OsCPS3Z51nLEzwO/Y6YshtC4JU+15oNwhY8v4LRKRYclRc7ggOikwrsJ/eOEQ== - -"@oxc-parser/binding-darwin-arm64@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.128.0.tgz#899b60a6cddb62172746dc2c0e6ed9d0ba7e8aa3" - integrity sha512-tRUHPt80417QmvNpoSslJT1VY8NUbWdrWR+L14Zn+RbOTcaqB8E6PYE/ZGN8jjWBzqporiA/H4MfO50ew/NCNA== - -"@oxc-parser/binding-darwin-x64@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.128.0.tgz#aad294a1ebf1396d28769278cce6f57b48b157ab" - integrity sha512-rWI2Hb1Nt3U/vKsjyNvZzDC8i/l144U20DKjhzaTmwIhIiSRGeroPWWiImwypmKLqrw8GuIixbWJkpGWLbkzrQ== - -"@oxc-parser/binding-freebsd-x64@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.128.0.tgz#8e2ae04d394b59f25742f81d91d5fef6533839fc" - integrity sha512-hhpdVMaNCLgQxjgNPeeFzSeJMmZPc5lKfv0NGSI3egZq9EdnEGqeC8JsYsQjK7PoQgbvZ17xlj0SO5ziH5Obkg== - -"@oxc-parser/binding-linux-arm-gnueabihf@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.128.0.tgz#2124e24ef523d0c687f2c028f15d9a4b8be14276" - integrity sha512-093zNw0zZ/e/obML+rhlSdmnzR0mVZluPcAkxunEc5E3F0yBVsFn24Y1ILfsEte11Ud041qn/gp2OJ1jxNqUng== - -"@oxc-parser/binding-linux-arm-musleabihf@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.128.0.tgz#7090830ef706900049ed8c2bb9173912a46c4a4a" - integrity sha512-fq7DmKmfC+dvD97IXrgbph6Jzwe0EDu+PYMofmzZ6fv5X1k9vtaqLpDGMuICO9MmUnyKAQmVl+wIv2RNy4Dz8g== - -"@oxc-parser/binding-linux-arm64-gnu@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.128.0.tgz#5a8d73c8c2b601de36135083bd4cd66dc31fee90" - integrity sha512-Xvm48jJah8TlIrURIjNOP/gNiGe6aKvCB+r06VliflFo8Kq7VOLE8PxtgShJzZIqubrgdMdYfvuPPozn7F6MbQ== - -"@oxc-parser/binding-linux-arm64-musl@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.128.0.tgz#cbbb2c51ef29c36a58103d0865d6c8902c21bcd8" - integrity sha512-M7iwBGmYJTx+pKOYFjI0buop4gJvlmcVzFGaXPt21DKpQkbQZG1f63Yg7LloIYT/t9yLxCw0Lhfx/RFlAlMSjA== - -"@oxc-parser/binding-linux-ppc64-gnu@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.128.0.tgz#834c0e315c0f15ff70438a4872382d7f93d6d239" - integrity sha512-21LGNIZb1Pcfk5/EGsqabrxv4yqQOWis1407JJrClS7XpFCrbvr74YAB1V+m54cYbwvO6UWwQqS4WecxiyfCRg== - -"@oxc-parser/binding-linux-riscv64-gnu@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.128.0.tgz#25059863fd75e78fe47653ba14ff90dd9f06896c" - integrity sha512-gyHjOTFpg9bTTYjxPmQirvufb89+VdZwVfcMtAUyPr6F5H8ZswvCQshK4qOW+Q+2Xyb33hduRgY/eFHJQjU/vQ== - -"@oxc-parser/binding-linux-riscv64-musl@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.128.0.tgz#b0fd29102fa47d2789111bbb1578baae8ae4ee96" - integrity sha512-X6Q2oKUrP5GyDd2xniuEBLk6aFQCZ97W2+aVXGgJXdjx5t4/oFuA9ri0wLOUrBIX+qdSuK581snMBio4z910eA== - -"@oxc-parser/binding-linux-s390x-gnu@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.128.0.tgz#87012dd9503950907151d8fbf38a0f2038f5a0e4" - integrity sha512-BdzTmqxfxoYkpgokoLaSnOX6T+R3/goL42klre2tnG+kHbG2TXS0VN+P5BPofH1axdKOHy5ei4ENZrjmCOt2lA== - -"@oxc-parser/binding-linux-x64-gnu@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.128.0.tgz#fa994d4cb8b488f2bee62e676fbfadf9b5d6669b" - integrity sha512-OO1nW2Q7sSYYvJZpDHdvyFSdRaVcQqRijZSSmWVMqFxPYy8cEF45zJ9fcdIYuzIT3jYq6YRhEFm/VMWNWhE22Q== - -"@oxc-parser/binding-linux-x64-musl@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.128.0.tgz#55fa9fcdbc9518a9db0588cd998a2644ebeaa916" - integrity sha512-4NehAe404MRdoZVS9DW8C5XbJwbXIc/KfVlYdpi5vE4081zc9Y0YzKVqyOYj/Puye7/Do+ohaONBFWlEHYl9hw== - -"@oxc-parser/binding-openharmony-arm64@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.128.0.tgz#b6f1a698d7f554d883c1b9aad3e024676dcd38c8" - integrity sha512-kVbqgW9xLL8bh8oc7aYOJilRKXE5G33+tE0jan+duo/9OriaFRpijcCwT2waWs2oqYROYq0GlE7/p3ywoshVeg== - -"@oxc-parser/binding-wasm32-wasi@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.128.0.tgz#5cc54e54bf70ab9af20b737ae137fab55ba430ec" - integrity sha512-L38ojghJYHmgiz6fJd7jwLB/ESDBpB02NdFxh+smqVM6P2anCEvHn0jhaSrt5eVNR1Ak8+moOeftUlofeyvniA== +"@oxc-parser/binding-android-arm-eabi@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.129.0.tgz#fd038240307fa37e42bc6a98a24768aff817bb0b" + integrity sha512-sG37CfXLlYXdDrggAFO/mKcO4w36piwf862xAZXIuf3nzKhWK1FvW4dqie+06++z+mDto2QeOQSvhyzBeK5jsQ== + +"@oxc-parser/binding-android-arm64@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.129.0.tgz#adbb4f9df3348ac3b30fd90452533161c2323676" + integrity sha512-DVyLFN2+S0VOhT6lm5++tFqlu3x2Njiby6y5DhTzjV5uRsZWpifsBn6+yjtwAxl105peEjs5BHE3ToBJuQjLTg== + +"@oxc-parser/binding-darwin-arm64@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.129.0.tgz#7323773e34eb29dc37170733f3df831bf27ec897" + integrity sha512-QeqThtB8qax4IL+NFBWgshudyKkj5c076L8vyd8PCEx7U1wHyIbH49MEQ5J5iURFhUW5jiFmdnLKEwyOo0GAJA== + +"@oxc-parser/binding-darwin-x64@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.129.0.tgz#35182155699eb47093971020180cd706c8310b8f" + integrity sha512-zn5+7nv4DlK4uFgblmhKm6xRV0QUHXOHyIDkjmhxJ53xSA9ahkb3pHNiHesNPXn/nK/VWU+C+Z6JYHdatZBh7g== + +"@oxc-parser/binding-freebsd-x64@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.129.0.tgz#fff993c77bb1b6c1d82749643d133174d77c9676" + integrity sha512-SPTcDBiHWlgRpWFC1jnoi0sBWqCw4DFR+4b8+dV+NAhUu2ONERWyIVIOCfcE9a8BlvZsDCuXf3l/x7wQUs1Rsw== + +"@oxc-parser/binding-linux-arm-gnueabihf@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.129.0.tgz#7ec5e22206fddbb9d8cbdb98a1b5476ca81282a7" + integrity sha512-Rgc9+WNKLbc+chyDTXyyJ7gbgLo+ve27CrRnmIwGgucGflrBZbutge5jdPPegcgf46RrR4dkBbMCp0/x16mdig== + +"@oxc-parser/binding-linux-arm-musleabihf@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.129.0.tgz#dec55e83c690b38ff1392a8980a44b0504cbc3b3" + integrity sha512-YtSsJ51VysXqlO8Cs2mWTyXvxBRemTHj4WDQjXwKl0SAxh+CVrEdXrdH+RnjxLj3JCUMFeYuHs5c+/DImfbKkg== + +"@oxc-parser/binding-linux-arm64-gnu@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.129.0.tgz#f1e7a509ff5c8fc1aa8df64fc7af69382fcd6e61" + integrity sha512-9oK8iQr9KtgI5JhaJ+5IwiQsXEo6NuasFgovtJGrdK/RxbA0bO4YKRvVY7M+8lozUCVz1U7XrFFODv3emIOPRA== + +"@oxc-parser/binding-linux-arm64-musl@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.129.0.tgz#021cfae1197d0210c24dc273ff436d61dd550f8d" + integrity sha512-GghE/bf9ZqgqZFxLacgP0ImVD6UiLKQOpvpgUoIsqjopu2ms/+p1L0d0Dv2Sck+8p0FbKS2WE3IjqmIlLbxJgA== + +"@oxc-parser/binding-linux-ppc64-gnu@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.129.0.tgz#f9638c67c5b97d13f5f387395346b43fe40c79f2" + integrity sha512-A2PW0UbERzKGV6fKX1zoe2Tkc1zVcEJSSPW9IUSKbZAPuPe+M5/5hTA+6fQbWmevabe2B3IDky66a1lFGjpBKA== + +"@oxc-parser/binding-linux-riscv64-gnu@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.129.0.tgz#6b3ed8958b403f75ac68fcb919d9a8447057e70b" + integrity sha512-omwxd9H+jrl1T72RI666k4ho7Eli2iHdELzf+dL0D+uXThNZXYJCbKjm5rK2hrHmDy4O+NWv7+khBrEkorLsgw== + +"@oxc-parser/binding-linux-riscv64-musl@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.129.0.tgz#a8a1be098d37f68ba1321e6cb76830a73200fc26" + integrity sha512-v2hi8id+M8C0uY8uuG2t2a5vr8H9XyHXiHL12yMdMNtgn04nnM/8hlOGuoJuxVc07PhClNiaoSaY2eXehSRa7w== + +"@oxc-parser/binding-linux-s390x-gnu@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.129.0.tgz#8ab51fa869894900f242be4682007dada48d704c" + integrity sha512-UXrdDyLh1Obgj5X+IVVXWoo5/FJbFsU8/uLQ/M9lkVUwBUKpRFxNEhzwBNv21qqdKgAh+pr2CCVD8J1JfRPsIQ== + +"@oxc-parser/binding-linux-x64-gnu@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.129.0.tgz#df221a378d150ca4b168d99d68f1bd6fee120651" + integrity sha512-hsL/3/kdX9FGLqOj8DR3Eki4Y6zO1i3+ZHhiPwX0hDt4n+18abkfUzePCv3h8SShprwCmwdxPnlrebZ5+MZ+cw== + +"@oxc-parser/binding-linux-x64-musl@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.129.0.tgz#4b11eec4ed01d93981965b3ca0a58b2e51b2475a" + integrity sha512-kdXvJ4crOeRld3vWl0J0VU4nmnT4pZ3lKGA5tZ1y0UPWsBtElDYd+jsz4lE36tpAbCiWm0M0PG0laUNBSE+Wlw== + +"@oxc-parser/binding-openharmony-arm64@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.129.0.tgz#d2faff426b739323e734f032d404cfa3db4f0c0b" + integrity sha512-DusJfcK7EGwf9TEakB+z6SXCLdHGvDZ8U8882bzWb4oVrORHpbkFl9npS7cN3YC2axcVKoktbxZevS1nxVCKFw== + +"@oxc-parser/binding-wasm32-wasi@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.129.0.tgz#7c31ffa9902a5d3e0b81bddc88d2db959efe9d56" + integrity sha512-Iie9CcII+ELSinKFnxTR15xhI9qriVivEhbFh3driRNbzms/5ioDAU0fwe8Mf1FEaz3n2FtiUVX0h0nwKLYk0A== dependencies: "@emnapi/core" "1.10.0" "@emnapi/runtime" "1.10.0" "@napi-rs/wasm-runtime" "^1.1.4" -"@oxc-parser/binding-win32-arm64-msvc@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.128.0.tgz#b86bfb654ce712d26e8d164c1026a411b3772ee2" - integrity sha512-xgvO35GyHBtjlQ5AEpaYr7Rll1rvY7zqIhT6ty8E3ezBW2J1SFLjIDEvI/tcgDg6oaseDAqVcM+jU1HuCekgZw== +"@oxc-parser/binding-win32-arm64-msvc@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.129.0.tgz#41bc115de494a18557cebe3bee2706b1103def34" + integrity sha512-99kH1udLyrts+wGm+u0VhPbogkb2wxc/6J1XMKOpS6Kx5DjBWGRZZfBjfCGI3xKSInpYbZn4TLWLX1Q1GURYwg== -"@oxc-parser/binding-win32-ia32-msvc@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.128.0.tgz#f06da85efa4a7bf5409ad1a812d4d32b7ff4e154" - integrity sha512-OY+3eM2SN72prHKRB22mPz8o5A/7dJ+f5DFLBVvggyZhEaNDAH9IB+ElMjmOkOIwf5MDCUAowCK7pAncNxzpBA== +"@oxc-parser/binding-win32-ia32-msvc@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.129.0.tgz#d9fee9485c71e99bcf1fa14a80ff6cd79a80ddf2" + integrity sha512-tmSBR1A4yH697qV291xKyDe4OAWFchJ+cXf2wuipx/vK3n5d5Ej9MVLRtXlIcZ38n8qAjsF0/AnskaYgxM151A== -"@oxc-parser/binding-win32-x64-msvc@0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.128.0.tgz#6928f6d9a32e09dd94db7f5211e09b1a9c816898" - integrity sha512-NE9ny+cPUCCObXa0IKLfj0tCdPd7pe/dz9ZpkxpUOymB3miNeMPybdlYYTBSGJUalMWeBM85/4JcCErCNTqOXw== +"@oxc-parser/binding-win32-x64-msvc@0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.129.0.tgz#5def2348809f09d0e353f6c3d1a3d4840fdd7a2e" + integrity sha512-Z1PbJvkPeLASIUxa3AnrQ5H+vv1K9zC0IGnQqoKfM0ZvsvCSe0d3u5m7d9iuy+HB7GrcElHuwKb0d0qFdtG0iA== -"@oxc-project/types@^0.128.0": - version "0.128.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.128.0.tgz#efc7524f948ff9e8ab1404ecad1823849c6fe149" - integrity sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ== +"@oxc-project/types@^0.129.0": + version "0.129.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.129.0.tgz#8e6362388ce6092feafd14f3a73ae6407b1285d9" + integrity sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg== "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -883,10 +913,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^15.3.2": - version "15.3.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz#afecc36681e26aab9e0fe809fd9ad578096a3058" - integrity sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw== +"@sinonjs/fake-timers@^15.4.0": + version "15.4.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz#5d40c151a9e66075fe4520bec40bccfe54931962" + integrity sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA== dependencies: "@sinonjs/commons" "^3.0.1" @@ -1153,12 +1183,12 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@^1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.2.tgz#eb8fb6d30349abace6ade5b4cb4d9e8a0dc23e5b" - integrity sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A== +axios@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.0.tgz#f8e5dd931cef2a5f8c32216d5784eda2f8750eb7" + integrity sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w== dependencies: - follow-redirects "^1.15.11" + follow-redirects "^1.16.0" form-data "^4.0.5" proxy-from-env "^2.1.0" @@ -1225,7 +1255,7 @@ brace-expansion@^2.0.2: dependencies: balanced-match "^1.0.0" -brace-expansion@^5.0.2, brace-expansion@^5.0.5: +brace-expansion@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb" integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ== @@ -1551,10 +1581,10 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -dc-polyfill@^0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.10.tgz#6f2ada1a9e449587c363ca98cfb79b443cf74b70" - integrity sha512-9iSbB8XZ7aIrhUtWI5ulEOJ+IyUN+axquodHK+bZO4r7HfY/xwmo6I4fYYf+aiDom+WMcN/wnzCz+pKvHDDCug== +dc-polyfill@^0.1.11: + version "0.1.11" + resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.11.tgz#3efa792147f3b5224b8a9274905b1e98fe82a856" + integrity sha512-TyyeGcjx0YeThAI9fTFtgsvj5qd4R+aGfVmXiUhevbgzWFDr7IK4tv4YjE6jaGzLHQTchk4h7DHdr5q4WGgaZw== debug@^3.2.7: version "3.2.7" @@ -1625,10 +1655,10 @@ diff@^7.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== -diff@^8.0.4: - version "8.0.4" - resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.4.tgz#4f5baf3188b9b2431117b962eb20ba330fadf696" - integrity sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw== +diff@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-9.0.0.tgz#297c31cd7c280f13dfe335791ec2063bd4a73a6f" + integrity sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw== doctrine@^2.1.0: version "2.1.0" @@ -1835,12 +1865,12 @@ eslint-module-utils@^2.12.1: dependencies: debug "^3.2.7" -eslint-plugin-cypress@^6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-6.4.0.tgz#50ed33b4e18d9910ae34fffc9411a6f1690a380a" - integrity sha512-zdGXFSmTH4w2A+2aU/Abna2s8Vcz5mDxDsnAXP5Vl+pERWRQxlSE2qrrmriNUEKZ62tlOA87b9AOnJX49tOhAQ== +eslint-plugin-cypress@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-6.4.1.tgz#2aa64e5a2dcc56b8b916fdf8398a81b31a61c462" + integrity sha512-8mnfR3q0Lr41Fu9SYZGeZ5nbSBgtS44+bbtSs7k0KvfoQ2pPmK43IvaTP6FGTfwBTN6S7w027CpqjrLAd65AYA== dependencies: - globals "^17.5.0" + globals "^17.6.0" eslint-plugin-es-x@^7.8.0: version "7.8.0" @@ -1904,10 +1934,10 @@ eslint-plugin-mocha@^11.2.0: "@eslint-community/eslint-utils" "^4.4.1" globals "^15.14.0" -eslint-plugin-n@^17.23.2: - version "17.24.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz#b66fa05f7a6c1ba16768f0921b8974147dddd060" - integrity sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw== +eslint-plugin-n@^18.0.1: + version "18.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-18.0.1.tgz#f21f6848648d0fcf11e6d9260ebf1c80994b99f7" + integrity sha512-q3ARhk+eZRc7myR0KHx+R3/GJeOHF+Ir6PK95Pu2tEX8Sl/4BIpmmVLva2kPrjC2gCmn6WHlHm+3yeo6Rxhycw== dependencies: "@eslint-community/eslint-utils" "^4.5.0" enhanced-resolve "^5.17.1" @@ -1917,7 +1947,6 @@ eslint-plugin-n@^17.23.2: globrex "^0.1.2" ignore "^5.3.2" semver "^7.6.3" - ts-declaration-location "^1.0.6" eslint-plugin-promise@^7.3.0: version "7.3.0" @@ -2210,7 +2239,7 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== -follow-redirects@^1.15.11: +follow-redirects@^1.16.0: version "1.16.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== @@ -2389,10 +2418,10 @@ globals@^15.11.0, globals@^15.14.0: resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== -globals@^17.2.0, globals@^17.4.0, globals@^17.5.0: - version "17.5.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-17.5.0.tgz#a82c641d898f8dfbe0e81f66fdff7d0de43f88c6" - integrity sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g== +globals@^17.2.0, globals@^17.4.0, globals@^17.6.0: + version "17.6.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-17.6.0.tgz#0f0be018d5cca8690e6375ead1f65c4bb96191fc" + integrity sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA== globalthis@^1.0.4: version "1.0.4" @@ -2418,9 +2447,9 @@ graceful-fs@^4.1.15, graceful-fs@^4.2.4: integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphql@*: - version "16.13.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.13.2.tgz#4d2b73df5796b201f1bc2765f5d7067f689cb55f" - integrity sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig== + version "16.14.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.14.0.tgz#f1128a74b16a34d1245c8436bb07b488d87b83e1" + integrity sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q== has-bigints@^1.0.2: version "1.1.0" @@ -2694,6 +2723,11 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== +is-node-process@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== + is-number-object@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" @@ -3245,12 +3279,12 @@ negotiator@^1.0.0: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== -nock@^13.5.6: - version "13.5.6" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.6.tgz#5e693ec2300bbf603b61dae6df0225673e6c4997" - integrity sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ== +nock@^14.0.15: + version "14.0.15" + resolved "https://registry.yarnpkg.com/nock/-/nock-14.0.15.tgz#23f9978fb20d8b3607dc263f4978cb89648f550b" + integrity sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg== dependencies: - debug "^4.1.0" + "@mswjs/interceptors" "^0.41.0" json-stringify-safe "^5.0.1" propagate "^2.0.0" @@ -3418,6 +3452,11 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +outvariant@^1.4.0, outvariant@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" + integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== + own-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" @@ -3427,33 +3466,33 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" -oxc-parser@^0.128.0: - version "0.128.0" - resolved "https://registry.yarnpkg.com/oxc-parser/-/oxc-parser-0.128.0.tgz#4d1592db9f0b9e519ae7c1996c647b9e66ee1506" - integrity sha512-XkOw3eiIxAgQ19WRew/Bq9wc5Ga/guaWIzDBzq80z1PyuDNGvWBpPby9k6YGwV8A8uMw+Nlq3xqlzuDYmUFYUw== +oxc-parser@^0.129.0: + version "0.129.0" + resolved "https://registry.yarnpkg.com/oxc-parser/-/oxc-parser-0.129.0.tgz#a62b2402c57d4ebfe7960de439b1da24ee53e65d" + integrity sha512-S6eFI+VLkpyA+/Lf8z6qURjDV6Mgo74SLNznNopHTlQW3hedv2MB/z31kBRuBCCTqZN9HHdva0ojljEhPnBKFA== dependencies: - "@oxc-project/types" "^0.128.0" + "@oxc-project/types" "^0.129.0" optionalDependencies: - "@oxc-parser/binding-android-arm-eabi" "0.128.0" - "@oxc-parser/binding-android-arm64" "0.128.0" - "@oxc-parser/binding-darwin-arm64" "0.128.0" - "@oxc-parser/binding-darwin-x64" "0.128.0" - "@oxc-parser/binding-freebsd-x64" "0.128.0" - "@oxc-parser/binding-linux-arm-gnueabihf" "0.128.0" - "@oxc-parser/binding-linux-arm-musleabihf" "0.128.0" - "@oxc-parser/binding-linux-arm64-gnu" "0.128.0" - "@oxc-parser/binding-linux-arm64-musl" "0.128.0" - "@oxc-parser/binding-linux-ppc64-gnu" "0.128.0" - "@oxc-parser/binding-linux-riscv64-gnu" "0.128.0" - "@oxc-parser/binding-linux-riscv64-musl" "0.128.0" - "@oxc-parser/binding-linux-s390x-gnu" "0.128.0" - "@oxc-parser/binding-linux-x64-gnu" "0.128.0" - "@oxc-parser/binding-linux-x64-musl" "0.128.0" - "@oxc-parser/binding-openharmony-arm64" "0.128.0" - "@oxc-parser/binding-wasm32-wasi" "0.128.0" - "@oxc-parser/binding-win32-arm64-msvc" "0.128.0" - "@oxc-parser/binding-win32-ia32-msvc" "0.128.0" - "@oxc-parser/binding-win32-x64-msvc" "0.128.0" + "@oxc-parser/binding-android-arm-eabi" "0.129.0" + "@oxc-parser/binding-android-arm64" "0.129.0" + "@oxc-parser/binding-darwin-arm64" "0.129.0" + "@oxc-parser/binding-darwin-x64" "0.129.0" + "@oxc-parser/binding-freebsd-x64" "0.129.0" + "@oxc-parser/binding-linux-arm-gnueabihf" "0.129.0" + "@oxc-parser/binding-linux-arm-musleabihf" "0.129.0" + "@oxc-parser/binding-linux-arm64-gnu" "0.129.0" + "@oxc-parser/binding-linux-arm64-musl" "0.129.0" + "@oxc-parser/binding-linux-ppc64-gnu" "0.129.0" + "@oxc-parser/binding-linux-riscv64-gnu" "0.129.0" + "@oxc-parser/binding-linux-riscv64-musl" "0.129.0" + "@oxc-parser/binding-linux-s390x-gnu" "0.129.0" + "@oxc-parser/binding-linux-x64-gnu" "0.129.0" + "@oxc-parser/binding-linux-x64-musl" "0.129.0" + "@oxc-parser/binding-openharmony-arm64" "0.129.0" + "@oxc-parser/binding-wasm32-wasi" "0.129.0" + "@oxc-parser/binding-win32-arm64-msvc" "0.129.0" + "@oxc-parser/binding-win32-ia32-msvc" "0.129.0" + "@oxc-parser/binding-win32-x64-msvc" "0.129.0" p-limit@^2.2.0: version "2.3.0" @@ -3587,7 +3626,7 @@ picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^4.0.2, picomatch@^4.0.3: +picomatch@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== @@ -4056,15 +4095,15 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -sinon@^21.1.2: - version "21.1.2" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-21.1.2.tgz#2404a6003853e6fc30430825fd21fe87675f29bf" - integrity sha512-FS6mN+/bx7e2ajpXkEmOcWB6xBzWiuNoAQT18/+a20SS4U7FSYl8Ms7N6VTUxN/1JAjkx7aXp+THMC8xdpp0gA== +sinon@^22.0.0: + version "22.0.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-22.0.0.tgz#01cea95c919468f6ef01e21d406397e5c02aad9b" + integrity sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q== dependencies: "@sinonjs/commons" "^3.0.1" - "@sinonjs/fake-timers" "^15.3.2" + "@sinonjs/fake-timers" "^15.4.0" "@sinonjs/samsam" "^10.0.2" - diff "^8.0.4" + diff "^9.0.0" source-map@^0.6.1: version "0.6.1" @@ -4135,6 +4174,11 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +strict-event-emitter@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" + integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -4310,13 +4354,6 @@ ts-api-utils@^2.5.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== -ts-declaration-location@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz#d4068fe9975828b3b453b3ab112b4711d8267688" - integrity sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA== - dependencies: - picomatch "^4.0.2" - tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -4654,10 +4691,10 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^2.8.3: - version "2.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" - integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== +yaml@^2.8.4: + version "2.8.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.4.tgz#4b5f411dd25f9544914d8673d4da7f29248e5e2e" + integrity sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog== yargs-parser@^18.1.2: version "18.1.3"