diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 0df0c61e82b..3f4549dff10 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -9,7 +9,7 @@ runs: using: composite steps: - id: yarn-cache - uses: actions/cache@v4 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: key: yarn-cache-${{ github.workflow }}-${{ github.job }}-${{ hashFiles('yarn.lock') }}-v2 path: node_modules.tar diff --git a/.github/actions/install/branch-diff/action.yml b/.github/actions/install/branch-diff/action.yml index b40833aa3a5..e877ee646ed 100644 --- a/.github/actions/install/branch-diff/action.yml +++ b/.github/actions/install/branch-diff/action.yml @@ -7,7 +7,7 @@ inputs: runs: using: composite steps: - - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.npm key: ${{ github.workflow }}-branch-diff-3.1.1 diff --git a/.github/actions/instrumentations/test/action.yml b/.github/actions/instrumentations/test/action.yml index 37d16040cc2..44691f07c27 100644 --- a/.github/actions/instrumentations/test/action.yml +++ b/.github/actions/instrumentations/test/action.yml @@ -14,7 +14,7 @@ runs: - uses: ./.github/actions/node/latest - run: yarn test:instrumentations:ci shell: bash - - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - uses: DataDog/junit-upload-github-action@762867566348d59ac9bcf479ebb4ec040db8940a # v2.0.0 if: always() && github.actor != 'dependabot[bot]' with: diff --git a/.github/actions/plugins/test-and-upstream/action.yml b/.github/actions/plugins/test-and-upstream/action.yml index a16e66421f2..6ef1bb003ed 100644 --- a/.github/actions/plugins/test-and-upstream/action.yml +++ b/.github/actions/plugins/test-and-upstream/action.yml @@ -19,7 +19,7 @@ runs: shell: bash - run: yarn test:plugins:upstream shell: bash - - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - if: always() uses: ./.github/actions/testagent/logs with: diff --git a/.github/actions/plugins/test/action.yml b/.github/actions/plugins/test/action.yml index c71c1d74d85..94560f4ece4 100644 --- a/.github/actions/plugins/test/action.yml +++ b/.github/actions/plugins/test/action.yml @@ -15,7 +15,7 @@ runs: - uses: ./.github/actions/node/latest - run: yarn test:plugins:ci shell: bash - - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - if: always() uses: ./.github/actions/testagent/logs with: diff --git a/.github/actions/plugins/upstream/action.yml b/.github/actions/plugins/upstream/action.yml index f2bcd199a16..0408675b029 100644 --- a/.github/actions/plugins/upstream/action.yml +++ b/.github/actions/plugins/upstream/action.yml @@ -15,7 +15,7 @@ runs: - uses: ./.github/actions/node/latest - run: yarn test:plugins:upstream shell: bash - - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - if: always() uses: ./.github/actions/testagent/logs with: diff --git a/.github/actions/testagent/logs/action.yml b/.github/actions/testagent/logs/action.yml index 38845935e1c..2b7746436e1 100644 --- a/.github/actions/testagent/logs/action.yml +++ b/.github/actions/testagent/logs/action.yml @@ -28,7 +28,7 @@ runs: shell: bash - name: Archive Test Agent Logs if: runner.debug == '1' # only create test agent log artifacts if the github action has been re-run with debug mode - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: "test_agent_logs_${{ inputs.suffix }}" path: "./artifacts/logs" @@ -48,7 +48,7 @@ runs: rm "$headers" shell: bash - name: Archive Test Agent Tested Versions Artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: supported-integrations-${{ inputs.suffix }} path: ./artifacts diff --git a/.github/chainguard/dependabot-automation.sts.yaml b/.github/chainguard/dependabot-automation.sts.yaml index 107ec3ee508..619d5ad3ad8 100644 --- a/.github/chainguard/dependabot-automation.sts.yaml +++ b/.github/chainguard/dependabot-automation.sts.yaml @@ -3,7 +3,7 @@ issuer: https://token.actions.githubusercontent.com subject: repo:DataDog/dd-trace-js:pull_request claim_pattern: - event_name: pull_request + event_name: pull_request_target ref: refs/heads/master ref_protected: "true" job_workflow_ref: DataDog/dd-trace-js/.github/workflows/dependabot-automation.yml@refs/heads/master diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 508a6232c72..8e921210702 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -73,9 +73,6 @@ updates: # Thus, we ignore them and update them manually. - dependency-name: "@opentelemetry/api" - dependency-name: "@opentelemetry/api-logs" - - dependency-name: "tap" - # Contain breaking changes that are incompatible with our test usage - update-types: ["version-update:semver-major"] groups: dev-minor-and-patch-dependencies: dependency-type: "development" diff --git a/.github/workflows/all-green.yml b/.github/workflows/all-green.yml index be817d17fb1..85dbb22124c 100644 --- a/.github/workflows/all-green.yml +++ b/.github/workflows/all-green.yml @@ -19,7 +19,7 @@ jobs: checks: read contents: read steps: - - uses: wechuli/allcheckspassed@f423b273f5fdf73582e41f8f6f0f204d69c27379 # v2.1.0 + - uses: wechuli/allcheckspassed@b0eaef5be76003932c9520e2b3dd03c85cc43d87 # v2.2.0 with: retries: 30 # once per minute, some checks take up to 15 min, retries are possible checks_exclude: devflow.* diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index 9cae2656ce1..4833b0caaef 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -152,6 +152,22 @@ jobs: with: dd_api_key: ${{ secrets.DD_API_KEY }} + bullmq: + runs-on: ubuntu-latest + services: + redis: + image: redis:7.0-alpine + ports: + - 6379:6379 + env: + PLUGINS: bullmq + SERVICES: redis + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: ./.github/actions/plugins/test + with: + dd_api_key: ${{ secrets.DD_API_KEY }} + bunyan: runs-on: ubuntu-latest env: @@ -1036,6 +1052,15 @@ jobs: dd_api_key: ${{ secrets.DD_API_KEY }} prisma: + strategy: + matrix: + include: + - node-version: 18 + range: '>=6.1.0 <7.0.0' + range_clean: gte.6.16.0.and.lt.7.0.0 + - node-version: 'latest' + range: '' + range_clean: all runs-on: ubuntu-latest services: postgres: @@ -1047,11 +1072,26 @@ jobs: env: PLUGINS: prisma SERVICES: prisma + PACKAGE_VERSION_RANGE: ${{ matrix.range }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: ./.github/actions/plugins/test + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node with: - dd_api_key: ${{ secrets.DD_API_KEY }} + version: ${{ matrix.node-version }} + - uses: ./.github/actions/install + - name: Run tests + run: yarn test:plugins:ci + - if: always() + uses: ./.github/actions/testagent/logs + with: + suffix: plugins-${{ github.job }}-${{ matrix.node-version }}-${{ matrix.range_clean }} + - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + - uses: DataDog/junit-upload-github-action@762867566348d59ac9bcf479ebb4ec040db8940a # v2.0.0 + if: always() && github.actor != 'dependabot[bot]' + with: + api_key: ${{ secrets.DD_API_KEY }} + service: dd-trace-js-tests process: runs-on: ubuntu-latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 810e94d2b8c..f43cd8c1f7d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: languages: ${{ matrix.language }} config-file: .github/codeql_config.yml @@ -48,7 +48,7 @@ jobs: # queries: ./path/to/local/query, your-org/your-repo/queries@main - name: Autobuild - uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/dependabot-automation.yml b/.github/workflows/dependabot-automation.yml index 39c16942556..bc4f71b579d 100644 --- a/.github/workflows/dependabot-automation.yml +++ b/.github/workflows/dependabot-automation.yml @@ -22,7 +22,7 @@ jobs: policy: dependabot-automation - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # 2.4.0 + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # 2.5.0 with: github-token: "${{ steps.octo-sts.outputs.token }}" - name: Enable auto-merge for Dependabot PRs @@ -47,7 +47,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # 2.4.0 + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # 2.5.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 if: steps.metadata.outputs.dependency-group == 'vendor-minor-and-patch-dependencies' - run: yarn @@ -64,7 +64,7 @@ jobs: echo "commit=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT if: steps.metadata.outputs.dependency-group == 'vendor-minor-and-patch-dependencies' - name: Push commits - uses: DataDog/commit-headless@5a0f3876e0fbdd3a86b3e008acf4ec562db59eee # action/v2.0.1 + uses: DataDog/commit-headless@583489e08d78037e7fa256c14adf998d5463f6a0 # action/v2.0.2 with: branch: ${{ github.ref_name }} command: push diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 8a22d3fe57c..00000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,123 +0,0 @@ -on: - workflow_dispatch: - inputs: - cache_entries: - description: 'JSON array of cache entries, e.g., [{"key":"abc","version":"123"},{"key":"def","version":"456"}]' - required: true - type: string - default: '[{"key":"runner_venv-Linux-/opt/hostedtoolcache/Python/3.12.12/x64-dbe133b0a76747625e6c8139943b9dc2dbf5814e9ccf9f4a8ed5b7fa66dd2976","version":"f488f6875b536eb746450004e6f18bf74d514ad2cad78c86d6d5147ddbf9f9b0"},{"key":"PFxRDTsQC2CBRTRk3TMxWNYXnd0=","version":"4793076103aa823b0a4c97942d7385d4346f77a3c30a0bad6e0f1d748becbab5"},{"key":"playwright-browsers-oldest-dd6","version":"4f7514040e3a28a321a9658d8e3a51fd52c29410f6844d0d51bcb9b20ca5e3a6"},{"key":"PFxRDTsQC2CBRTRk3TMxWNYXnd0=","version":"c95311a28e589c62f14651d2e6001da3e2afe6e855380a412a583dabc576b960"},{"key":"yarn-cache-AI Guard-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"Aktlxw4hnyBVd/vZJbkdxGmq8Tw=","version":"3dbcc4f8dfd5fbbab9759602b7adb19c466cf9edfc277687f97ba9efbdc86d90"},{"key":"yarn-cache-APM Capabilities-tracing-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"yarn-cache-AppSec-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"yarn-cache-OpenFeature-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"cypress-binary-14.5.4","version":"09038f5f1eb279f921ac07849ccf00ea0cadd76685c868eab628a2e3f47ef0b7"},{"key":"n71Gg/JormzoitmBpVjBCZCcL6Y=","version":"0c867ee6264758fbca938e6c6d38a3160cb478f2770da2f831e22e4c9e3720d8"},{"key":"playwright-browsers-Linux-1.57.0","version":"b11b119dfd10565044882f81f06d3a75b1602bec6f8658ac905dd63583b2a885"},{"key":"cypress-binary-10.2.0","version":"09038f5f1eb279f921ac07849ccf00ea0cadd76685c868eab628a2e3f47ef0b7"},{"key":"yarn-cache-Profiling-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"actionlint-1.7.9-Linux-X64","version":"2c5831b0ad32de99a644bc2cdd9a58421edce007c9b47f4c1c7d25220f9d6578"},{"key":"yarn-cache-AppSec-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"PFxRDTsQC2CBRTRk3TMxWNYXnd0=","version":"c95311a28e589c62f14651d2e6001da3e2afe6e855380a412a583dabc576b960"},{"key":"playwright-browsers-oldest-dd6","version":"4f7514040e3a28a321a9658d8e3a51fd52c29410f6844d0d51bcb9b20ca5e3a6"},{"key":"yarn-cache-Profiling-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"cypress-binary-14.5.4","version":"09038f5f1eb279f921ac07849ccf00ea0cadd76685c868eab628a2e3f47ef0b7"},{"key":"yarn-cache-APM Capabilities-tracing-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"cypress-binary-10.2.0","version":"09038f5f1eb279f921ac07849ccf00ea0cadd76685c868eab628a2e3f47ef0b7"},{"key":"yarn-cache-AI Guard-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"Aktlxw4hnyBVd/vZJbkdxGmq8Tw=","version":"3dbcc4f8dfd5fbbab9759602b7adb19c466cf9edfc277687f97ba9efbdc86d90"},{"key":"yarn-cache-OpenFeature-windows-b4e1dc4e46ec063c714cab2e6a48b2f1c42c37b4546d1f2592a517159bbd52d8-v2","version":"dfeaf94a5df020ad999e6dd827afacf94f5a2ddf5418288305ef5b91f639b181"},{"key":"playwright-browsers-Linux-1.57.0","version":"b11b119dfd10565044882f81f06d3a75b1602bec6f8658ac905dd63583b2a885"},{"key":"n71Gg/JormzoitmBpVjBCZCcL6Y=","version":"0c867ee6264758fbca938e6c6d38a3160cb478f2770da2f831e22e4c9e3720d8"},{"key":"actionlint-1.7.9-Linux-X64","version":"2c5831b0ad32de99a644bc2cdd9a58421edce007c9b47f4c1c7d25220f9d6578"},{"key":"PFxRDTsQC2CBRTRk3TMxWNYXnd0=","version":"c95311a28e589c62f14651d2e6001da3e2afe6e855380a412a583dabc576b960"},{"key":"playwright-browsers-oldest-dd6","version":"4f7514040e3a28a321a9658d8e3a51fd52c29410f6844d0d51bcb9b20ca5e3a6"}]' - -jobs: - extract-cache: - runs-on: ubuntu-latest - - steps: - - name: Checkout (minimal) - uses: actions/checkout@v6 - with: - sparse-checkout: | - .github - sparse-checkout-cone-mode: false - - - name: Extract caches - run: | - mkdir -p cache-artifacts - - echo '${{ inputs.cache_entries }}' | jq -c '.[]' | while read -r entry; do - key=$(echo "$entry" | jq -r '.key') - version=$(echo "$entry" | jq -r '.version') - - echo "================================================" - echo "Attempting to restore: $key (version: $version)" - echo "================================================" - - safe_name=$(echo "$key-$version" | tr '/' '_' | tr ':' '_') - cache_dir="cache-artifacts/$safe_name" - mkdir -p "$cache_dir" - - echo "Restoring to: $cache_dir" - - cat > "$cache_dir/metadata.json" </dev/null; then - echo "✓ Successfully restored $key" - restored="true" - else - echo "✗ Failed to restore $key" - fi - - echo "$restored" > "$cache_dir/restore_status.txt" - echo "::endgroup::" - done - env: - GH_TOKEN: ${{ github.token }} - - - name: Create file hashes - run: | - echo "# File Hashes (SHA256)" > cache-artifacts/HASHES.txt - find cache-artifacts -type f -not -name "HASHES.txt" -exec sha256sum {} \; >> cache-artifacts/HASHES.txt - - - name: Prepare artifacts for upload - run: | - echo '${{ inputs.cache_entries }}' | jq -c '.[]' | while read -r entry; do - key=$(echo "$entry" | jq -r '.key') - version=$(echo "$entry" | jq -r '.version') - safe_name=$(echo "$key-$version" | tr '/' '_' | tr ':' '_') - cache_dir="cache-artifacts/$safe_name" - - if [ -d "$cache_dir" ]; then - tar -czf "${safe_name}.tar.gz" -C "cache-artifacts" "$(basename "$cache_dir")" - artifact_name="${key}:${version}" - safe_artifact_name=$(echo "$artifact_name" | sed 's/[^a-zA-Z0-9:_-]/_/g') - echo "$safe_artifact_name|${safe_name}.tar.gz" >> artifact_list.txt - fi - done - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - if: always() - with: - name: cache-forensics-batch-${{ github.run_number }} - path: | - *.tar.gz - cache-artifacts/HASHES.txt - retention-days: 30 - if-no-files-found: warn - - - name: Display summary - if: always() - run: | - echo "## Cache Extraction Complete 🔍" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -f artifact_list.txt ]; then - echo "### Extracted Artifacts" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Cache Key:Version | Archive File |" >> $GITHUB_STEP_SUMMARY - echo "|-------------------|--------------|" >> $GITHUB_STEP_SUMMARY - while IFS='|' read -r artifact_name archive_file; do - echo "| \`$artifact_name\` | \`$archive_file\` |" >> $GITHUB_STEP_SUMMARY - done < artifact_list.txt - fi diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index 62b4e6bb46c..91ce670a8a3 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -442,6 +442,7 @@ jobs: # We'll run these separately for earlier (i.e. unsupported) versions integration-guardrails: strategy: + fail-fast: false matrix: version: [14.0.0, 14, 16.0.0, 18.0.0, 20.0.0, 22.0.0, 24.0.0] runs-on: ubuntu-latest @@ -462,6 +463,7 @@ jobs: integration-guardrails-unsupported: strategy: + fail-fast: false matrix: version: ['0.8', '0.10', '0.12', '4', '6', '8', '10', '12'] runs-on: ubuntu-latest diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index c70ec3bd421..e2dda350b32 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -26,7 +26,7 @@ jobs: - name: Pack dd-trace-js run: mkdir -p ./binaries && echo /binaries/$(npm pack --pack-destination ./binaries ./dd-trace-js) > ./binaries/nodejs-load-from-npm - name: Upload artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: system_tests_binaries path: ./binaries/**/* diff --git a/.github/workflows/test-optimization.yml b/.github/workflows/test-optimization.yml index c13d47ae7c2..7eb1872dbf6 100644 --- a/.github/workflows/test-optimization.yml +++ b/.github/workflows/test-optimization.yml @@ -54,7 +54,7 @@ jobs: echo "version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT echo "Latest Playwright version: $PLAYWRIGHT_VERSION" - name: Cache Playwright browsers - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.cache/ms-playwright key: playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} @@ -96,7 +96,7 @@ jobs: echo "dd-trace major version: $MAJOR" - name: Cache Playwright browsers if: matrix.playwright-version == 'oldest' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: /github/home/.cache/ms-playwright key: playwright-browsers-oldest-dd${{ steps.dd-version.outputs.major }} @@ -229,7 +229,7 @@ jobs: # as that changes frequently and would have a low cache hit rate - name: Cache Cypress binary if: matrix.cypress-version != 'latest' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.cache/Cypress key: cypress-binary-${{ matrix.cypress-version }} diff --git a/.github/workflows/update-3rdparty-licenses.yml b/.github/workflows/update-3rdparty-licenses.yml index d6875c39fe2..feef0743d7d 100644 --- a/.github/workflows/update-3rdparty-licenses.yml +++ b/.github/workflows/update-3rdparty-licenses.yml @@ -51,6 +51,8 @@ jobs: EOF - name: Regenerate LICENSE-3rdparty.csv + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | dd-license-attribution generate-sbom-csv \ --use-mirrors=mirrors.json \ diff --git a/.gitignore b/.gitignore index 89f79f20e16..415ebc176a1 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,10 @@ packages/datadog-plugin-azure-functions/test/integration-test/fixtures/node_modu __azurite_db_queue__.json __azurite_db_queue_extent__.json __queuestorage__/AzuriteConfig + + +# ignore apm-instrumentation-toolkit analysis results dirs +.analysis/ + +# ignore claude settings +.claude/ diff --git a/.gitlab/one-pipeline.locked.yml b/.gitlab/one-pipeline.locked.yml index a217c7b0e73..bae60301537 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/04f6a88e3db67cb88821632d138a2a5c3105ba59760bd3dfc60b54733501ecc3/one-pipeline.yml + - remote: https://gitlab-templates.ddbuild.io/libdatadog/one-pipeline/ca/c6ea92df19738f28d6e7c3e4fea86d030fa801ac3361427d6acb6f99a917c28a/one-pipeline.yml diff --git a/.taprc b/.taprc deleted file mode 100644 index f97c470b512..00000000000 --- a/.taprc +++ /dev/null @@ -1,5 +0,0 @@ -check-coverage: false -color: true -coverage: false -node-arg: --expose-gc -reporter: spec diff --git a/.vscode/settings.json b/.vscode/settings.json index a8e5bc0bfe7..a03d2085f73 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,31 @@ // Use the workspace version of TypeScript instead of VSCode's bundled version "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, + "cSpell.words": [ + "aerospike", + "appsec", + "backportability", + "kafkajs", + "llmobs", + "microbenchmarks", + "oracledb", + "rabbitmq", + "rspack", + "sirun" + ], + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true, + "eslint.format.enable": true, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + } + } } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..c81a0777a1a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,296 @@ +# AGENTS.md + +## Prerequisites + +- Node.js >= 18 +- yarn 1.x +- Docker + docker-compose (for running service dependencies in tests) + +## Setup + +- Install dependencies: `yarn install` + +**This project uses yarn, not npm. Always use `yarn` commands instead of `npm` commands.** + +## Project Overview + +dd-trace is the Datadog client library for Node.js. + +**Key Directories:** +- `packages/dd-trace/` - Main library (APM, profiling, debugger, appsec, llmobs, CI visibility, etc) +- `packages/datadog-core/` - Async context storage, shared utilities +- `packages/datadog-instrumentations/` - Instrumentation implementations +- `packages/datadog-plugin-*/` - 100+ plugins for third-party integrations +- `integration-tests/` - E2E integration tests +- `benchmark/` - Performance benchmarks + +**Package Structure:** + +Each package under `packages/` follows a consistent structure: +- `src/` - Source code for the package +- `test/` - Unit tests for the package +- Unit test files always follow the `*.spec.js` naming convention +- Test directories may also contain helper files + +## Testing Instructions + +### Running Individual Tests + +**IMPORTANT**: Never run `yarn test` directly. Use `mocha` directly on test files. + +**Mocha unit tests:** +```bash +./node_modules/.bin/mocha -r "packages/dd-trace/test/setup/mocha.js" path/to/test.spec.js +``` + +**Integration tests:** +```bash +./node_modules/.bin/mocha --timeout 60000 -r "packages/dd-trace/test/setup/core.js" path/to/test.spec.js +``` + +**Target specific tests:** +- Add `--grep "test name pattern"` flag + +**Enable debug logging:** +- Prefix with `DD_TRACE_DEBUG=true` + +### Plugin Tests + +**Use `PLUGINS` env var:** +```bash +PLUGINS="amqplib" yarn test:plugins +PLUGINS="amqplib|bluebird" yarn test:plugins # pipe-delimited for multiple +./node_modules/.bin/mocha -r "packages/dd-trace/test/setup/mocha.js" packages/datadog-plugin-amqplib/test/index.spec.js +``` + +**With external services** (check `.github/workflows/apm-integrations.yml` for `SERVICES`): +```bash +export SERVICES="rabbitmq" PLUGINS="amqplib" +docker compose up -d $SERVICES +yarn services && yarn test:plugins +``` + +**ARM64 incompatible:** `aerospike`, `couchbase`, `grpc`, `oracledb` + +### Test Coverage + +```bash +./node_modules/.bin/nyc --include "packages/dd-trace/src/debugger/**/*.js" \ + ./node_modules/.bin/mocha -r "packages/dd-trace/test/setup/mocha.js" \ + "packages/dd-trace/test/debugger/**/*.spec.js" +``` + +**Philosophy:** +- Integration tests (running in sandboxes) don't count towards nyc coverage metrics +- Don't add redundant unit tests solely to improve coverage numbers +- Focus on covering important production code paths with whichever test type makes sense + +### Test Assertions + +Use `node:assert/strict` for standard assertions. For partial deep object checks, use `assertObjectContains` from `integration-tests/helpers/index.js`: + +```js +const assert = require('node:assert/strict') + +const { assertObjectContains } = require('../helpers') + +assert.equal(actual, expected) +assertObjectContains(response, { status: 200, body: { user: { name: 'Alice' } } }) +``` + +### Time-Based Testing + +**Never rely on actual time passing in unit tests.** Use sinon's fake timers to mock time and make tests deterministic and fast. + +## Code Style & Linting + +### Linting & Naming +- Lint: `yarn lint` / `yarn lint:fix` +- Files: kebab-case + +### JSDoc +- Use TypeScript-compatible syntax (`@param {string}`, `@returns {Promise}`, `@typedef`) +- Never use `any` (be specific or use `unknown` if type is truly unknown) + +### Import Ordering + +Separate groups with empty line, sort alphabetically within each: +1. Node.js core modules (with `node:` prefix) +2. Third-party modules +3. Internal imports (by path proximity, then alpha) + +Use destructuring for utility modules when appropriate. + +```js +const fs = require('node:fs') +const path = require('node:path') + +const express = require('express') + +const { myConf } = require('./config') +const log = require('../log') +``` + +### ECMAScript and Node.js API Standards + +**Target Node.js 18.0.0 compatibility:** +- Use modern JS features supported by Node.js (e.g., optional chaining `?.`, nullish coalescing `??`) +- Guard newer APIs with version checks using [`version.js`](./version.js): + ```js + const { NODE_MAJOR } = require('./version') + if (NODE_MAJOR >= 20) { /* Use Node.js 20+ API */ } + ``` + +### Performance and Memory + +**CRITICAL: Tracer runs in application hot paths - every operation counts.** + +**Async/Await:** +- Do NOT use `async/await` or promises in production code (npm package) +- Allowed ONLY in: test files, worker threads (e.g., `packages/dd-trace/src/debugger/devtools_client/`) +- Use callbacks or synchronous patterns instead + +**Memory:** +- Minimize allocations in frequently-called paths +- Avoid unnecessary objects, closures, arrays +- Reuse objects and buffers +- Minimize GC pressure + +#### Array Iteration + +**Prefer `for-of`, `for`, `while` loops over functional methods (`map()`, `forEach()`, `filter()`):** +- Avoid `items.forEach(item => process(item))` → use `for (const item of items) { process(item) }` +- Avoid chaining `items.filter(...).map(...)` → use single loop with conditional push +- Functional methods create closures and intermediate arrays + +**Functional methods acceptable in:** +- Test files +- Non-hot-path code where readability benefits +- One-time initialization code + +**Loop selection:** +- `for-of` - Simple iteration +- `for` with index - Need index or better performance in hot paths +- `while` - Custom iteration logic + +### Debugging and Logging + +Use `log` module (`packages/dd-trace/src/log/index.js`) with printf-style formatting (not template strings): + +```js +const log = require('../log') +log.debug('Value: %s', someValue) // printf-style +log.debug(() => `Expensive: ${expensive()}`) // callback for expensive ops +log.error('Error: %s', msg, err) // error as last arg +``` + +Enable: `DD_TRACE_DEBUG=true DD_TRACE_LOG_LEVEL=info node app.js` +Levels: `trace`, `debug`, `info`, `warn`, `error` + +### Error Handling + +**Never crash user apps:** Catch/log errors (`log.error()`/`log.warn()`), resume or disable plugin/subsystem +Avoid try/catch in hot paths - validate inputs early + +## Development Workflow + +### Core Principles +- **Search first**: Check for existing utilities/patterns before creating new code +- **Small PRs**: Break large efforts into incremental, reviewable changes +- **Descriptive code**: Self-documenting with verbs in function names; comment when needed +- **Readable formatting**: Empty lines for grouping, split complex objects, extract variables +- **Avoid large refactors**: Iterative changes, gradual pattern introduction +- **Test changes**: Test logic (not mocks), failure cases, edge cases - always update tests + +### Implementation and Testing Workflow + +**When making any code or type change, the following MUST be followed:** + +1. **Understand** - Read relevant code and tests to understand the current implementation +2. **Implement** - Make the necessary code changes +3. **Update Tests** - Modify or add tests to cover the changes +4. **Run Tests** - Execute the relevant test files to verify everything works +5. **Verify** - Confirm all tests pass before marking the task as complete + +### Always Consider Backportability + +**We always backport `master` to older versions.** +- Keep breaking changes to a minimum +- Don't use language/runtime features that are too new +- **Guard breaking changes with version checks** using [`version.js`](./version.js): + ```js + const { DD_MAJOR } = require('./version') + if (DD_MAJOR >= 6) { + // New behavior for v6+ + } else { + // Old behavior for v5 and earlier + } + ``` + +## Adding New Configuration Options + +1. **Add default value** in `packages/dd-trace/src/config_defaults.js` +2. **Map environment variable** in `packages/dd-trace/src/config.js` (`#applyEnvironment()` method) +3. **Add TypeScript definitions** in `index.d.ts` +4. **Add to telemetry name mapping** (if applicable) in `packages/dd-trace/src/telemetry/telemetry.js` +5. **Update** `packages/dd-trace/src/supported-configurations.json` +6. **Document** in `docs/API.md` (non-internal/experimental options only) +7. **Add tests** in `packages/dd-trace/test/config.spec.js` + +**Naming Convention:** Size/time-based config options should have unit suffixes (e.g., `timeoutMs`, `maxBytes`, `intervalSeconds`). + +## Adding New Instrumentation + +**New instrumentations go in `packages/datadog-instrumentations/`.** The instrumentation system uses diagnostic channels for communication. + +Many integrations have corresponding plugins in `packages/datadog-plugin-*/` that work with the instrumentation layer. + +### What Are Plugins? + +Plugins are modular code components in `packages/datadog-plugin-*/` directories that: +- Subscribe to diagnostic channels to receive instrumentation events +- Handle APM tracing logic (spans, metadata, error tracking) +- Manage feature-specific logic (e.g., code origin tracking, LLM observability) + +**Plugin Base Classes:** +- **`Plugin`** - Base class with diagnostic channel subscription, storage binding, enable/disable lifecycle. Use for non-tracing functionality. +- **`TracingPlugin`** - Extends `Plugin` with APM tracing helpers (`startSpan()`, automatic trace events, `activeSpan` getter). Use for plugins creating trace spans. +- **`CompositePlugin`** - Extends `Plugin` to compose multiple sub-plugins. Use when one integration needs multiple feature plugins (e.g., `express` combines tracing and code origin plugins). + +**Plugin Loading:** +- Plugins load lazily when application `require()`s the corresponding library +- Disable with `DD_TRACE_DISABLED_PLUGINS` or `DD_TRACE__ENABLED=false` +- Test framework plugins only load when Test Optimization mode (`isCiVisibility`) is enabled + +**When to Create a New Plugin:** +1. Adding support for a new third-party library/framework +2. Adding a new product feature that integrates with existing libraries (use `CompositePlugin`) + +### Creating a New Plugin + +```bash +mkdir -p packages/datadog-plugin-/{src,test} +cp packages/datadog-plugin-kafkajs/src/index.js packages/datadog-plugin-/src/ +``` + +Edit `src/index.js`, create `test/index.spec.js`, then register in: +`packages/dd-trace/src/plugins/index.js`, `index.d.ts`, `docs/test.ts`, `docs/API.md`, `.github/workflows/apm-integrations.yml` + +## Pull Requests and CI + +### Commit Messages + +Conventional format: `type(scope): description` +Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci` +Example: `feat(appsec): add new WAF rule` + +### PR Requirements + +- Use template from `.github/pull_request_template.md` +- Label: `semver-patch` (fixes only), `semver-minor` (new features), `semver-major` (breaking) +- **All tests must pass - all-green policy, no exceptions** + +## Vendoring Dependencies + +Using rspack: Run `yarn` in `vendor/` to install/bundle dependencies → `packages/node_modules/` +(Some deps excluded, e.g., `@opentelemetry/api`) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000000..47dc3e3d863 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index c8129b66780..17cc9e79a61 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,12 @@ /packages/dd-trace/src/llmobs/ @DataDog/ml-observability /packages/dd-trace/test/llmobs/ @DataDog/ml-observability +# Data Streams Monitoring +/packages/dd-trace/src/datastreams/ @DataDog/data-streams-monitoring +/packages/dd-trace/test/datastreams/ @DataDog/data-streams-monitoring +/packages/**/dsm.spec.js @DataDog/data-streams-monitoring +/packages/**/*.dsm.spec.js @DataDog/data-streams-monitoring + # API SDK /packages/dd-trace/src/telemetry/ @DataDog/apm-sdk-capabilities-js /packages/dd-trace/test/telemetry/ @DataDog/apm-sdk-capabilities-js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f93768c7c8..b1e0937d5db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,15 +28,26 @@ Sometimes new patterns or new ideas emerge which would be a substantial improvem ## Test everything -It's very difficult to know if a change is valid unless there are tests to prove it. As an extension of that, it's also difficult to know the _use_ of that code is valid if the way it is integrated is not propertly tested. For this reason we generally favour integration tests over unit tests. If an API is expected to be used in different places or in different ways then it should generally include unit tests too for each unique scenario, however great care should be taken to ensure unit tests are actually testing the _logic_ and not just testing the _mocks_. It's a very common mistake to write a unit test that abstracts away the actual use of the interface so much that it doesn't actually test how that interface works in real-world scenarios. Remember to test how it handles failures, how it operates under heavy load, and how it impacts usability of what its purpose is. +It's very difficult to know if a change is valid unless there are tests to prove it. As an extension of that, it's also difficult to know the _use_ of that code is valid if the way it is integrated is not properly tested. For this reason we generally favour integration tests over unit tests. If an API is expected to be used in different places or in different ways then it should generally include unit tests too for each unique scenario, however great care should be taken to ensure unit tests are actually testing the _logic_ and not just testing the _mocks_. It's a very common mistake to write a unit test that abstracts away the actual use of the interface so much that it doesn't actually test how that interface works in real-world scenarios. Remember to test how it handles failures, how it operates under heavy load, and how it impacts usability of what its purpose is. ## Don't forget benchmarks -Observability products tend to have quite a bit of their behaviour running in app code hot paths. It's important we extensively benchmark anything we expect to have heavy use to ensure it performs well and we don't cause any significant regressions through future changes. Measuring once at the time of writing is insufficient--a graph with just one data point is not going to tell you much of anything. +Observability products tend to have quite a bit of their behavior running in app code hot paths. It's important we extensively benchmark anything we expect to have heavy use to ensure it performs well and we don't cause any significant regressions through future changes. Measuring once at the time of writing is insufficient--a graph with just one data point is not going to tell you much of anything. ## Always consider backportability -To reduce delta between release lines and make it easier for us to support older versions we try as much as possible to backport every change we can. We should be diligent about keeping breaking changes to a minimum and ensuring we don't use language or runtime features which are too new. This way we can generally be confident that a change can be backported. +We always backport changes from `master` to older versions to avoid release lines drifting apart and to prevent merge conflicts. We should be diligent about keeping breaking changes to a minimum and ensuring we don't use language or runtime features which are too new. This way we can generally be confident that a change can be backported. + +**Breaking changes must be guarded by version checks** so they can land in `master` and be safely backported to older versions. Check the major version of the dd-trace package using the `version.js` module in the root of the project: + +```js +const { DD_MAJOR } = require('./version') +if (DD_MAJOR >= 6) { + // New behavior for v6+ +} else { + // Old behavior for v5 and earlier +} +``` To reduce the surface area of a breaking change, the breaking aspects could be placed behind a flag which is disabled by default or isolated to a function. In the next major the change would then be just to change the default of the flag or to start or stop calling the isolated function. By isolating the breaking logic it also becomes easier to delete later when it's no longer relevant on any release line. @@ -48,11 +59,11 @@ This library follows the semantic versioning standard, but there are some subtle ### semver-patch -If the change is a bug or security fix, it should be labelled as semver-patch. These changes should generally not alter existing behaviour in any way other than to correct the specific issue. +If the change is a bug or security fix, it should be labelled as semver-patch. These changes should generally not alter existing behavior in any way other than to correct the specific issue. ### semver-minor -Any addition of new functionality should be labelled as semver-minor and should not change any existing behaviour either in how any existing API works or in changing the contents or value of any existing data being reported except in purely additive cases where all existing data retains its prior state. Such changes may include new configuration options which when used will change behaviour, or may include the addition of new data being captured such as a new instrumentation, but should not impact the current operating design of any existing features. +Any addition of new functionality should be labelled as semver-minor and should not change any existing behavior either in how any existing API works or in changing the contents or value of any existing data being reported except in purely additive cases where all existing data retains its prior state. Such changes may include new configuration options which when used will change behavior, or may include the addition of new data being captured such as a new instrumentation, but should not impact the current operating design of any existing features. ### semver-major @@ -70,6 +81,28 @@ We follow an all-green policy which means that for any PR to be merged _all_ tes Eventually we plan to look into putting these permission-required tests behind a label which team members can add to their PRs at creation to run the full CI and can add to outside contributor PRs to trigger the CI from their own user credentials. If the label is not present there will be another action which checks the label is present. Rather than showing a bunch of confusing failures to new contributors it would just show a single job failure which indicates an additional label is required, and we can name it in a way that makes it clear that it's not the responsibility of the outside contributor to add it. Something like `approve-full-ci` is one possible choice there. +## Search before creating + +Always search the codebase first before creating new code to avoid duplicates. Check for existing utilities, helpers, or patterns that solve similar problems. Reuse existing code when possible rather than reinventing solutions. + +## Sign your commits + +All commits in a pull request must be signed. We require commit signing to ensure the authenticity and integrity of contributions to the project. + +**Datadog employees:** We recommend using the [sign-pull-request tool](https://datadoghq.atlassian.net/wiki/spaces/SECENG/pages/5371593157/Easily+sign+commits+with+sign-pull-request+tool) for easy signing of commits. + +You can also sign your commits manually using one of the following methods: + +- [Signing commits with GPG](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) +- [Signing commits with SSH](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#ssh-commit-signature-verification) +- [Signing commits with 1Password](https://developer.1password.com/docs/ssh/git-commit-signing/) + +If you have already created commits without signing them, you can sign them retroactively by using an interactive rebase: + +```sh +$ git rebase --exec 'git commit --amend --no-edit -n -S' -i +``` + ## Development Requirements Since this project supports multiple Node.js versions, using a version manager @@ -77,7 +110,7 @@ such as [nvm](https://github.com/creationix/nvm) is recommended. If you're unsure which version of Node.js to use, just use the latest version, which should always work. -We use [yarn](https://yarnpkg.com/) 1.x for its workspace functionality, so make sure to install that as well. The easist way to install yarn 1.x with with npm: +We use [yarn](https://yarnpkg.com/) 1.x for its workspace functionality, so make sure to install that as well. The easiest way to install yarn 1.x with with npm: ```sh $ npm install -g yarn @@ -89,28 +122,162 @@ To install dependencies once you have Node and yarn installed, run this in the p $ yarn ``` +## Coding Standards + +### File Naming and Import Conventions + +Use **kebab-case** for file names (e.g., `my-module.js`, not `myModule.js`). + +Organize imports in the following order (each group separated by an empty line): + +1. Node.js core modules first (sorted alphabetically) - always prefix with `node:` +2. Third-party modules (sorted alphabetically) +3. Internal imports (sorted by path proximity first - closest first - then alphabetically) + +Example: + +```js +const fs = require('node:fs') +const path = require('node:path') + +const express = require('express') +const lodash = require('lodash') + +const { myConf } = require('./config') +const { foo } = require('./helper') +const log = require('../log') +``` + +### Node.js Version Compatibility + +Follow the ECMAScript standard and Node.js APIs supported by **Node.js 18.0.0**. Never use features or APIs only supported in newer versions unless explicitly required. If newer APIs are needed, guard them with version checks using the `version.js` module located in the root of the project: + +```js +const { NODE_MAJOR } = require('./version') +if (NODE_MAJOR >= 20) { + // Use Node.js 20+ API +} +``` + +### Performance Considerations + +This tracer runs in application hot paths, so performance is critical: + +- **Avoid `async/await` and promises** in production code (they add overhead). Use callbacks or synchronous patterns instead. Async/await is acceptable in test files and worker threads. +- **Minimize memory allocations** in frequently-called code paths +- **Prefer imperative loops over functional array methods** (`for-of`, `for`, `while` instead of `map()`, `forEach()`, `filter()`) to avoid closure overhead and intermediate arrays +- Avoid creating unnecessary objects, closures, or arrays +- Reuse objects and buffers where possible + +Example of preferred loop style: + +```js +// ❌ Avoid - creates closure overhead +items.forEach(item => { + process(item) +}) + +// ✅ Prefer - no closure overhead +for (const item of items) { + process(item) +} + +// ❌ Avoid - loops over data multiple times, creates intermediate array, adds closure overhead +const result = items + .filter(item => item.active) + .map(item => item.value) + +// ✅ Prefer - single loop, no intermediate array +const result = [] +for (const item of items) { + if (item.active) { + result.push(item.value) + } +} +``` + +### Error Handling + +The tracer should never crash user applications. Catch errors and log them with `log.error()` or `log.warn()` as appropriate. Resume normal operation if possible, or disable the plugin/subsystem if not. + +### Logging and Debugging + +Use the `log` module (located at `packages/dd-trace/src/log/index.js`) for all logging: + +```js +const log = require('../log') + +log.debug('Debug message with value: %s', someValue) +log.info('Info message') +log.warn('Warning with data: %o', objectValue) +log.error('Error reading file %s', filepath, err) +``` + +**Important:** Use printf-style formatting (`%s`, `%d`, `%o`) instead of template strings to avoid unnecessary string concatenation when logging is disabled. + +For expensive computations in the log message itself, use a callback function: + +```js +// Callback is only executed if debug logging is enabled +log.debug(() => `Processed data: ${expensive.computation()}`) +``` + +When logging errors, pass the error object as the last argument after the format string: + +```js +log.error('Error processing request', err) +// or with additional context: +log.error('Error reading file %s', filename, err) +``` + +To enable debug logging when running tests or applications: + +```sh +# Run application with debug logging +DD_TRACE_DEBUG=true node your-app.js + +# Run a specific test suite with debug logging +DD_TRACE_DEBUG=true yarn test:debugger +``` + +### JSDoc + +Document all APIs with TypeScript-compatible JSDoc to ensure proper types without using TypeScript. This enables type checking and IDE autocompletion while maintaining the JavaScript codebase. Use TypeScript type syntax in JSDoc annotations (e.g., `@param {string}`, `@returns {Promise}`). + +Never use the type `any` - be specific. + +## Adding New Configuration Options + +To add a new configuration option: + +1. **Add the default value** in `packages/dd-trace/src/config_defaults.js` +2. **Map the environment variable** in `packages/dd-trace/src/config.js` (add to destructuring in `#applyEnvironment()` method) +3. **Add TypeScript definitions** in `index.d.ts` +4. **Add to telemetry name mapping** (if applicable) in `packages/dd-trace/src/telemetry/telemetry.js` +5. **Update supported configurations** in `packages/dd-trace/src/supported-configurations.json` +6. **Document the option** in `docs/API.md` (for non-internal/experimental options) +7. **Add tests** in `packages/dd-trace/test/config.spec.js` + +**Naming Convention:** Size/time-based config options should have unit suffixes (e.g., `timeoutMs`, `maxBytes`, `intervalSeconds`). + ## Adding a Plugin +Plugins are modular code components in `packages/datadog-plugin-*/` directories that integrate with specific third-party libraries and frameworks. They subscribe to diagnostic channels to receive instrumentation events and handle APM tracing logic, feature-specific logic, and more. + To create a new plugin for a third-party package, follow these steps: -1. `mkdir -p packages/datadog-plugin-/src` -2. Copy an `index.js` file from another plugin to use as a starting point: `cp packages/datadog-plugin-kafkajs/src/index.js packages/datadog-plugin-/src` -3. Edit index.js as appropriate for your new plugin -4. `mkdir -p packages/datadog-plugin-/test` -5. Create an packages/datadog-plugin-/test/index.spec.js file and add the necessary tests. See other plugin tests for inspiration to file structure. +1. `mkdir -p packages/datadog-plugin-/src` +2. Copy an `index.js` file from another plugin to use as a starting point: `cp packages/datadog-plugin-kafkajs/src/index.js packages/datadog-plugin-/src` +3. Edit index.js as appropriate for your plugin +4. `mkdir -p packages/datadog-plugin-/test` +5. Create an packages/datadog-plugin-/test/index.spec.js file and add the necessary tests. See other plugin tests for inspiration to file structure. 6. Edit index.spec.js as appropriate for your new plugin 7. Add entries to the following files for your new plugin: - `packages/dd-trace/src/plugins/index.js` - `index.d.ts` - `docs/test.ts` - `docs/API.md` - - `.github/workflows/apm-integrations.yml` - -### Adding a Plugin Test to CI - -The plugin tests run on pull requests in Github Actions. Each plugin test suite has its own Github job, so adding a new suite to CI -requires adding a new job to the Github Actions config. The file containing these configs is `.github/workflows/apm-integrations.yml`. -You can copypaste and modify an existing plugin job configuration in this file to create a new job config. + - `.github/workflows/apm-integrations.yml` (see [Adding a Plugin Test to CI](#adding-a-plugin-test-to-ci)) ## Testing @@ -119,6 +286,93 @@ You can copypaste and modify an existing plugin job configuration in this file t The `pg-native` package requires `pg_config` to be in your `$PATH` to be able to install. Please refer to [the "Install" section](https://github.com/brianc/node-postgres/tree/master/packages/pg-native#install) of the `pg-native` documentation for how to ensure your environment is configured correctly. +### Running Individual Tests + +When developing, it's often faster to run individual test files rather than entire test suites. **Never run `yarn test` directly** as it requires too much setup and takes too long. + +To target specific tests, use the `--grep` flag with mocha to match test names: + +```sh +yarn test:debugger --grep "test name pattern" +yarn test:appsec --grep "specific test" +``` + +**This project uses mocha for testing.** + +### Test Assertions + +Use the Node.js core `assert` library for assertions in tests. Import from `node:assert/strict` to ensure all assertions use strict equality without type coercion: + +```js +const assert = require('node:assert/strict') + +assert.equal(actual, expected) +assert.deepEqual(actualObject, expectedObject) +``` + +For asserting that an object contains certain properties (deeply), use `assertObjectContains` from `integration-tests/helpers/index.js`: + +```js +const { assertObjectContains } = require('../helpers') + +// Assert an object contains specific properties (actual object can have more) +assertObjectContains(response, { + status: 200, + body: { user: { name: 'Alice' } } +}) +``` + +This helper performs partial deep equality checking and provides better error messages than individual assertions. + +### Time-Based Testing + +**Never rely on actual time passing in unit tests.** Tests that use `setTimeout()`, `setInterval()`, or `Date.now()` should use [sinon's fake timers](https://sinonjs.org/releases/latest/fake-timers/) to mock time. This makes tests: +- Run instantly instead of waiting for real time +- Deterministic and reliable (no timing-related flakiness) +- Easier to reason about + +Example: + +```js +const sinon = require('sinon') + +describe('my test', () => { + let clock + + beforeEach(() => { + clock = sinon.useFakeTimers() + }) + + afterEach(() => { + clock.restore() + }) + + it('should handle timeout', () => { + let called = false + setTimeout(() => { called = true }, 1000) + + clock.tick(1000) // Instantly advance time by 1 second + assert.equal(called, true) + }) +}) +``` + +Use `clock.tick(ms)` to advance time, `clock.restore()` to restore real timers, and `clock.reset()` to reset fake time to 0. + +### Test Coverage + +Coverage is measured with nyc. To check coverage for your changes, use the `:ci` variant of the test scripts: + +```sh +# Run tests with coverage for specific components +yarn test:debugger:ci +yarn test:appsec:ci +yarn test:llmobs:sdk:ci +yarn test:lambda:ci +``` + +**Coverage Philosophy:** Given the nature of this library (instrumenting third-party code, hooking into runtime internals), unit tests can become overly complex when everything needs to be mocked. Integration tests that run in sandboxes don't count towards nyc's coverage metrics, so coverage numbers may look low even when code is well-tested. **Don't add redundant unit tests solely to improve coverage numbers.** + ### Plugin Tests Before running _plugin_ tests, the supporting docker containers need to be running. You _can_ attempt to start all of them using docker-compose, but that's a drain on your system, and not all the images will even run at all on AMD64 devices. @@ -176,7 +430,7 @@ details. When running integration tests, some packages are installed from npm into temporary sandboxes. If running locally without an internet connection, it's possible to use the environment variable `OFFLINE=true` to make `yarn` use the `--prefer-offline` flag, -which will use the local yarn cache instead of fecthing packages from npm. +which will use the local yarn cache instead of fetching packages from npm. ### Adding a Plugin Test to CI @@ -198,7 +452,6 @@ $ yarn lint This also checks that the `LICENSE-3rdparty.csv` file is up-to-date, and checks dependencies for vulnerabilities. - ### Benchmarks Our microbenchmarks live in `benchmark/sirun`. Each directory in there @@ -213,5 +466,4 @@ most efficient algorithm. To run your benchmark, use: $ yarn bench ``` - [1]: https://docs.datadoghq.com/help diff --git a/benchmark/core.js b/benchmark/core.js index cf8a235f8c9..c94197a747c 100644 --- a/benchmark/core.js +++ b/benchmark/core.js @@ -1,6 +1,5 @@ 'use strict' -const benchmark = require('./benchmark') const proxyquire = require('proxyquire') const getConfig = require('../packages/dd-trace/src/config') @@ -29,6 +28,7 @@ const histogram = new Histogram() const runtimeMetrics = require('../packages/dd-trace/src/runtime_metrics') const log = require('../packages/dd-trace/src/log') const { calculateHttpEndpoint } = require('../packages/dd-trace/src/plugins/util/url') +const benchmark = require('./benchmark') const encoder04 = new Agent04Encoder({ flush: () => encoder04.makePayload() }) const encoder05 = new Agent05Encoder({ flush: () => encoder05.makePayload() }) diff --git a/benchmark/openfeature.js b/benchmark/openfeature.js index e96d65f876b..f5b1d2f573b 100644 --- a/benchmark/openfeature.js +++ b/benchmark/openfeature.js @@ -1,10 +1,10 @@ 'use strict' -const benchmark = require('./benchmark') const proxyquire = require('proxyquire') +const getConfig = require('../packages/dd-trace/src/config') +const benchmark = require('./benchmark') const { createSingleExposureEvent, createExposureEventArray } = require('./stubs/exposure-events') -const getConfig = require('../packages/dd-trace/src/config') const ExposuresWriter = proxyquire('../packages/dd-trace/src/openfeature/writers/exposures', { '../../exporters/common/request': () => {} }) diff --git a/benchmark/scope.js b/benchmark/scope.js index 5b4859520ee..194bf53bf54 100644 --- a/benchmark/scope.js +++ b/benchmark/scope.js @@ -2,13 +2,11 @@ const { AsyncResource } = require('async_hooks') +const Scope = require('../packages/dd-trace/src/scope') const benchmark = require('./benchmark') - -const suite = benchmark('scope') - const spanStub = require('./stubs/span') -const Scope = require('../packages/dd-trace/src/scope') +const suite = benchmark('scope') const scope = new Scope({ experimental: {} diff --git a/benchmark/sirun/appsec-iast/client.js b/benchmark/sirun/appsec-iast/client.js index 91686dfa2c4..84e1bcf8d66 100644 --- a/benchmark/sirun/appsec-iast/client.js +++ b/benchmark/sirun/appsec-iast/client.js @@ -1,7 +1,7 @@ 'use strict' -const { port, reqs } = require('./common') const http = require('http') +const { port, reqs } = require('./common') let connectionsMade = 0 function request (opts) { diff --git a/benchmark/sirun/appsec-iast/server-with-vulnerability.js b/benchmark/sirun/appsec-iast/server-with-vulnerability.js index cdacb7d5d0c..d0bc2f5135d 100644 --- a/benchmark/sirun/appsec-iast/server-with-vulnerability.js +++ b/benchmark/sirun/appsec-iast/server-with-vulnerability.js @@ -1,9 +1,9 @@ 'use strict' -const { port, reqs } = require('./common') +const childProcess = require('child_process') const express = require('../../../versions/express').get() const cookieParser = require('../../../versions/cookie-parser').get() -const childProcess = require('child_process') +const { port, reqs } = require('./common') const app = express() app.use(cookieParser()) diff --git a/benchmark/sirun/appsec-iast/server-without-vulnerability.js b/benchmark/sirun/appsec-iast/server-without-vulnerability.js index 18bd943d2ef..3e668e470f8 100644 --- a/benchmark/sirun/appsec-iast/server-without-vulnerability.js +++ b/benchmark/sirun/appsec-iast/server-without-vulnerability.js @@ -1,8 +1,8 @@ 'use strict' -const { port, reqs } = require('./common') const express = require('../../../versions/express').get() const cookieParser = require('../../../versions/cookie-parser').get() +const { port, reqs } = require('./common') const app = express() app.use(cookieParser()) diff --git a/benchmark/sirun/appsec/client.js b/benchmark/sirun/appsec/client.js index 6e6b3eb63ee..13e96793cfd 100644 --- a/benchmark/sirun/appsec/client.js +++ b/benchmark/sirun/appsec/client.js @@ -1,8 +1,8 @@ 'use strict' +const http = require('http') const { port, reqs } = require('./common') -const http = require('http') let connectionsMade = 0 function request (opts) { diff --git a/benchmark/sirun/appsec/server.js b/benchmark/sirun/appsec/server.js index 65512571f22..eba0e2b2057 100644 --- a/benchmark/sirun/appsec/server.js +++ b/benchmark/sirun/appsec/server.js @@ -3,9 +3,8 @@ // AppSec is enabled from env config require('../../..').init() -const { port, reqs } = require('./common') - const http = require('http') +const { port, reqs } = require('./common') let connectionsMade = 0 diff --git a/benchmark/sirun/diffserver.js b/benchmark/sirun/diffserver.js index 4f2ca824cb8..cc953d50f55 100644 --- a/benchmark/sirun/diffserver.js +++ b/benchmark/sirun/diffserver.js @@ -1,8 +1,8 @@ 'use strict' -const express = require('express') const path = require('path') const fs = require('fs') +const express = require('express') const diffRecent = require('./diff-recent') const { getBuildNumsFromGithub, diff --git a/benchmark/sirun/exporting-pipeline/index.js b/benchmark/sirun/exporting-pipeline/index.js index 5dc4626c5dc..6c8f6a993d5 100644 --- a/benchmark/sirun/exporting-pipeline/index.js +++ b/benchmark/sirun/exporting-pipeline/index.js @@ -2,12 +2,12 @@ // TODO: Update setup script to not leave agent process running in background. +const hostname = require('os').hostname() const SpanProcessor = require('../../../packages/dd-trace/src/span_processor') const Exporter = require('../../../packages/dd-trace/src/exporters/agent/index') const PrioritySampler = require('../../../packages/dd-trace/src/priority_sampler') const id = require('../../../packages/dd-trace/src/id') const defaults = require('../../../packages/dd-trace/src/config_defaults') -const hostname = require('os').hostname() const config = { url: `http://${defaults.hostname}:${defaults.port}`, diff --git a/benchmark/sirun/plugin-http/client.js b/benchmark/sirun/plugin-http/client.js index 94cef8f099c..0971ffbe642 100644 --- a/benchmark/sirun/plugin-http/client.js +++ b/benchmark/sirun/plugin-http/client.js @@ -4,9 +4,9 @@ if (Number(process.env.CLIENT_USE_TRACER)) { require('../../..').init() } +const http = require('http') const { port, reqs } = require('./common') -const http = require('http') let connectionsMade = 0 function request (url) { diff --git a/benchmark/sirun/plugin-http/server.js b/benchmark/sirun/plugin-http/server.js index 718adda3810..4c043ed8dec 100644 --- a/benchmark/sirun/plugin-http/server.js +++ b/benchmark/sirun/plugin-http/server.js @@ -4,9 +4,8 @@ if (Number(process.env.SERVER_USE_TRACER)) { require('../../..').init() } -const { port, reqs } = require('./common') - const http = require('http') +const { port, reqs } = require('./common') let connectionsMade = 0 diff --git a/benchmark/sirun/startup/startup-test.js b/benchmark/sirun/startup/startup-test.js index 960b6b470f6..ef477df9504 100644 --- a/benchmark/sirun/startup/startup-test.js +++ b/benchmark/sirun/startup/startup-test.js @@ -27,7 +27,6 @@ if (Number(process.env.EVERYTHING)) { 'axios', 'benchmark', 'body-parser', - 'chai', 'crypto-randomuuid', 'dc-polyfill', 'eslint-plugin-cypress', @@ -67,10 +66,8 @@ if (Number(process.env.EVERYTHING)) { 'semifies', 'semver', 'shell-quote', - 'sinon-chai', 'sinon', 'source-map', - 'tap', 'tiktoken', 'tlhunter-sorted-set', 'ttl-set', diff --git a/docker-compose.yml b/docker-compose.yml index 1b553ab6236..1582014046b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,7 +68,7 @@ services: ports: - "127.0.0.1:3306:3306" redis: - image: redis:4.0-alpine + image: redis:7.0-alpine ports: - "127.0.0.1:6379:6379" mongo: diff --git a/docs/API.md b/docs/API.md index 724c75cc8c2..5e8aecbe369 100644 --- a/docs/API.md +++ b/docs/API.md @@ -29,6 +29,7 @@ tracer.use('pg', {
+
@@ -111,6 +112,7 @@ tracer.use('pg', { * [aws-sdk](./interfaces/export_.plugins.aws_sdk.html) * [azure-functions](./interfaces/export_.plugins.azure_functions.html) * [bluebird](./interfaces/export_.plugins.bluebird.html) +* [bullmq](./interfaces/export_.plugins.bullmq.html) * [couchbase](./interfaces/export_.plugins.couchbase.html) * [cucumber](./interfaces/export_.plugins.cucumber.html) * [bunyan](./interfaces/export_.plugins.bunyan.html) diff --git a/docs/add-redirects.sh b/docs/add-redirects.sh index 700097beef8..27d5e5a9346 100755 --- a/docs/add-redirects.sh +++ b/docs/add-redirects.sh @@ -23,6 +23,7 @@ declare -a plugins=( "axios" "azure_functions" "azure_service_bus" + "bullmq" "bunyan" "cassandra_driver" "child_process" diff --git a/docs/package.json b/docs/package.json index 0f0afa155d2..4e95b68247c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -10,7 +10,7 @@ "license": "BSD-3-Clause", "private": true, "devDependencies": { - "typedoc": "^0.28.12", - "typescript": "^5.9.2" + "typedoc": "^0.28.15", + "typescript": "^5.9.3" } } diff --git a/docs/test.ts b/docs/test.ts index 218ecdd3390..f67b278d6f1 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -310,6 +310,7 @@ tracer.use('aws-sdk'); tracer.use('aws-sdk', awsSdkOptions); tracer.use('azure-event-hubs') tracer.use('azure-functions'); +tracer.use('bullmq'); tracer.use('bunyan'); tracer.use('couchbase'); tracer.use('cassandra-driver'); diff --git a/docs/yarn.lock b/docs/yarn.lock index 727453c6033..7fcb7b6f08a 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2,43 +2,43 @@ # yarn lockfile v1 -"@gerrit0/mini-shiki@^3.12.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.12.1.tgz#d6b40216d05f86fee07d27a30957dc7ff994b39a" - integrity sha512-qA9/VGm7No0kxb7k0oKFT0DSJ6BtuMMtC7JQdZn9ElMALE9hjbyoaS13Y8OWr0qHwzh07KHt3Wbw34az/FLsrg== +"@gerrit0/mini-shiki@^3.17.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.20.0.tgz#a9154b8faeb7c2268e6c54af4cec86a9fb722297" + integrity sha512-Wa57i+bMpK6PGJZ1f2myxo3iO+K/kZikcyvH8NIqNNZhQUbDav7V9LQmWOXhf946mz5c1NZ19WMsGYiDKTryzQ== dependencies: - "@shikijs/engine-oniguruma" "^3.12.1" - "@shikijs/langs" "^3.12.1" - "@shikijs/themes" "^3.12.1" - "@shikijs/types" "^3.12.1" + "@shikijs/engine-oniguruma" "^3.20.0" + "@shikijs/langs" "^3.20.0" + "@shikijs/themes" "^3.20.0" + "@shikijs/types" "^3.20.0" "@shikijs/vscode-textmate" "^10.0.2" -"@shikijs/engine-oniguruma@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.1.tgz#b93c07abfc83158350888a407fd6044bc48b19b9" - integrity sha512-hbYq+XOc55CU7Irkhsgwh8WgQbx2W5IVzHV4l+wZ874olMLSNg5o3F73vo9m4SAhimFyqq/86xnx9h+T30HhhQ== +"@shikijs/engine-oniguruma@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.20.0.tgz#4b476a8dff29561dfd9af1ba2edb4c378d3bee06" + integrity sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ== dependencies: - "@shikijs/types" "3.12.1" + "@shikijs/types" "3.20.0" "@shikijs/vscode-textmate" "^10.0.2" -"@shikijs/langs@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.12.1.tgz#8083318b1c7dad41b990f5644d7a27b93514e11f" - integrity sha512-Y1MbMfVO5baRz7Boo7EoD36TmzfUx/I5n8e+wZumx6SlUA81Zj1ZwNJL871iIuSHrdsheV4AxJtHQ9mlooklmg== +"@shikijs/langs@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.20.0.tgz#5dcfdeb9eb2d5f811144ca606553a4d8a6a667d5" + integrity sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA== dependencies: - "@shikijs/types" "3.12.1" + "@shikijs/types" "3.20.0" -"@shikijs/themes@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.12.1.tgz#df329febeaa80a931d2dfc8528342ad4bd55140f" - integrity sha512-9JrAm9cA5hqM/YXymA3oAAZdnCgQf1zyrNDtsnM105nNEoEpux4dyzdoOjc2KawEKj1iUs/WH2ota6Atp7GYkQ== +"@shikijs/themes@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.20.0.tgz#9b030fe81fcd0a8b7941131ef14c274b4c6451a8" + integrity sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ== dependencies: - "@shikijs/types" "3.12.1" + "@shikijs/types" "3.20.0" -"@shikijs/types@3.12.1", "@shikijs/types@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.12.1.tgz#d9eef9ed3bbcf78d1a40a0e5527a1022e0067547" - integrity sha512-Is/p+1vTss22LIsGCJTmGrxu7ZC1iBL9doJFYLaZ4aI8d0VDXb7Mn0kBzhkc7pdsRpmUbQLQ5HXwNpa3H6F8og== +"@shikijs/types@3.20.0", "@shikijs/types@^3.20.0": + version "3.20.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.20.0.tgz#b1fbacba2e1e38d31e3f869309fff216a5d27126" + integrity sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw== dependencies: "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" @@ -123,21 +123,21 @@ punycode.js@^2.3.1: resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== -typedoc@^0.28.12: - version "0.28.12" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.12.tgz#1baa55ab242e237fc896bc01b57cf5f8bd995d32" - integrity sha512-H5ODu4f7N+myG4MfuSp2Vh6wV+WLoZaEYxKPt2y8hmmqNEMVrH69DAjjdmYivF4tP/C2jrIZCZhPalZlTU/ipA== +typedoc@^0.28.15: + version "0.28.15" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.15.tgz#667faf77eb934deb935fbfd5108a6de5f953948f" + integrity sha512-mw2/2vTL7MlT+BVo43lOsufkkd2CJO4zeOSuWQQsiXoV2VuEn7f6IZp2jsUDPmBMABpgR0R5jlcJ2OGEFYmkyg== dependencies: - "@gerrit0/mini-shiki" "^3.12.0" + "@gerrit0/mini-shiki" "^3.17.0" lunr "^2.3.9" markdown-it "^14.1.0" minimatch "^9.0.5" yaml "^2.8.1" -typescript@^5.9.2: - version "5.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" - integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== +typescript@^5.9.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" diff --git a/eslint.config.mjs b/eslint.config.mjs index 2a670689c33..fbdc71e3499 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,3 +1,4 @@ +import { readFileSync } from 'fs' import eslintPluginJs from '@eslint/js' import eslintPluginStylistic from '@stylistic/eslint-plugin' import eslintPluginCypress from 'eslint-plugin-cypress' @@ -7,7 +8,6 @@ import eslintPluginMocha from 'eslint-plugin-mocha' import eslintPluginN from 'eslint-plugin-n' import eslintPluginPromise from 'eslint-plugin-promise' import eslintPluginUnicorn from 'eslint-plugin-unicorn' -import { readFileSync } from 'fs' import globals from 'globals' import eslintProcessEnv from './eslint-rules/eslint-process-env.mjs' @@ -48,6 +48,7 @@ export default [ '**/versions', // This is effectively a node_modules tree. '**/acmeair-nodejs', // We don't own this. '**/vendor', // Generally, we didn't author this code. + '**/.analysis', // Ignore apm-instrumentation-toolkit analysis results 'integration-tests/code-origin/typescript.js', // Generated 'integration-tests/debugger/target-app/source-map-support/bundle.js', // Generated 'integration-tests/debugger/target-app/source-map-support/hello/world.js', // Generated @@ -219,16 +220,30 @@ export default [ 'import/export': 'error', 'import/first': 'error', 'import/no-absolute-path': ['error', { esmodule: true, commonjs: true, amd: false }], + 'import/no-cycle': 'error', 'import/no-duplicates': 'error', 'import/no-named-default': 'error', + 'import/no-self-import': 'error', + 'import/order': ['error', { + // `dd-trace` must be allowed first (and is often intentionally required before any other module). + // eslint-plugin-import defaults can exclude some import types (notably `builtin`) from `pathGroups`, + // which would make the `dd-trace` exception below a no-op. Make this explicit. + pathGroupsExcludedImportTypes: [], + pathGroups: [ + { + pattern: 'dd-trace', + group: 'builtin', + position: 'before' + } + ] + }], + 'import/no-useless-path-segments': 'error', 'import/no-webpack-loader-syntax': 'error', 'jsdoc/check-tag-names': ['error', { definedTags: ['datadog'] }], // TODO: Enable the rules that we want to use. - 'jsdoc/check-types': 'off', // Should be activated, but it needs a couple of fixes. // no-defaults: This should be activated, since the defaults will not be picked up in a description. 'jsdoc/no-defaults': 'off', 'jsdoc/no-undefined-types': 'off', - 'jsdoc/reject-any-type': 'off', // Should be activated, but it needs a couple of fixes. 'jsdoc/reject-function-type': 'off', 'jsdoc/require-jsdoc': 'off', 'jsdoc/require-param-description': 'off', // Having a description is not crucial for now. @@ -342,7 +357,8 @@ export default [ // major release line at the same time, we need to specify the lowest version here to ensure backporting will // not fail. version: '>=18.0.0' - } + }, + jsdoc: { mode: 'typescript' } }, rules: { '@stylistic/max-len': ['error', { code: 120, tabWidth: 2, ignoreUrls: true, ignoreRegExpLiterals: true }], @@ -406,11 +422,11 @@ export default [ 'n/no-restricted-require': ['error', [ { name: 'diagnostics_channel', - message: 'Please use dc-polyfill instead.' + message: 'Please use `dc-polyfill` instead.' }, { name: 'semver', - message: 'Please use semifies instead.' + message: 'Please use `semifies` instead.' }, { name: 'get-port', @@ -418,12 +434,20 @@ export default [ }, { name: 'rimraf', - message: 'Please use fs.rm(path, { recursive: true, force: true }) instead.' + message: 'Please use `fs.rm(path, { recursive: true, force: true })` instead.' }, { name: 'koalas', message: 'Please use nullish coalescing operator (??) instead.' - } + }, + { + name: 'chai', + message: 'Please use `node:assert/strict` instead.' + }, + { + name: 'tap', + message: 'Please use `mocha` instead.' + }, ]], 'no-await-in-loop': 'error', @@ -595,7 +619,6 @@ export default [ allowModules: [ ...Object.keys(dependencies), 'mocha', - 'chai' ] }], 'n/no-missing-require': 'off', diff --git a/index.d.ts b/index.d.ts index 4c14af1ce84..c6cf2ea5268 100644 --- a/index.d.ts +++ b/index.d.ts @@ -191,6 +191,7 @@ interface Plugins { "azure-event-hubs": tracer.plugins.azure_event_hubs; "azure-functions": tracer.plugins.azure_functions; "azure-service-bus": tracer.plugins.azure_service_bus; + "bullmq": tracer.plugins.bullmq; "bunyan": tracer.plugins.bunyan; "cassandra-driver": tracer.plugins.cassandra_driver; "child_process": tracer.plugins.child_process; @@ -1757,6 +1758,12 @@ declare namespace tracer { * [logInjection](interfaces/traceroptions.html#logInjection) option is enabled * on the tracer. */ + /** + * This plugin automatically instruments the + * [bullmq](https://github.com/npmjs/package/bullmq) message queue library. + */ + interface bullmq extends Instrumentation {} + interface bunyan extends Integration {} /** diff --git a/integration-tests/aiguard/index.spec.js b/integration-tests/aiguard/index.spec.js index 94a8e42dad0..79ee787d2a5 100644 --- a/integration-tests/aiguard/index.spec.js +++ b/integration-tests/aiguard/index.spec.js @@ -6,9 +6,9 @@ const path = require('path') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../helpers') +const { assertObjectContains } = require('../helpers') const startApiMock = require('./api-mock') const { executeRequest } = require('./util') -const { assertObjectContains } = require('../helpers') describe('AIGuard SDK integration tests', () => { let cwd, appFile, agent, proc, api, url diff --git a/integration-tests/aiguard/server.js b/integration-tests/aiguard/server.js index 2851a25cd9a..8a29b586d00 100644 --- a/integration-tests/aiguard/server.js +++ b/integration-tests/aiguard/server.js @@ -1,7 +1,7 @@ 'use strict' -const express = require('express') const tracer = require('dd-trace').init({ flushInterval: 0 }) +const express = require('express') const app = express() diff --git a/integration-tests/appsec/data-collection.spec.js b/integration-tests/appsec/data-collection.spec.js index 96f2ea8615e..0d8882606ef 100644 --- a/integration-tests/appsec/data-collection.spec.js +++ b/integration-tests/appsec/data-collection.spec.js @@ -27,7 +27,7 @@ describe('ASM Data collection', () => { const env = { DD_TRACE_AGENT_PORT: agent.port, - DD_APPSEC_ENABLED: true, + DD_APPSEC_ENABLED: 'true', DD_APPSEC_RULES: path.join(cwd, 'appsec', 'data-collection', 'data-collection-rules.json') } diff --git a/integration-tests/appsec/endpoints-collection.spec.js b/integration-tests/appsec/endpoints-collection.spec.js index 9df64053440..61743b1190f 100644 --- a/integration-tests/appsec/endpoints-collection.spec.js +++ b/integration-tests/appsec/endpoints-collection.spec.js @@ -169,14 +169,15 @@ describe('Endpoints collection', () => { assert.strictEqual(trueCount, 1) // Check that all expected endpoints were found + const expectedOperationName = `${framework}.request` expectedEndpoints.forEach(expected => { const found = endpointsFound.find(e => e.method === expected.method && e.path === expected.path ) - assert.ok(found != null) + assert.ok(found) assert.strictEqual(found.type, 'REST') - assert.strictEqual(found.operation_name, 'http.request') + assert.strictEqual(found.operation_name, expectedOperationName) assert.strictEqual(found.resource_name, `${expected.method} ${expected.path}`) }) diff --git a/integration-tests/appsec/esm-app/index.mjs b/integration-tests/appsec/esm-app/index.mjs index f891aa9c132..ca8fa28fe72 100644 --- a/integration-tests/appsec/esm-app/index.mjs +++ b/integration-tests/appsec/esm-app/index.mjs @@ -1,6 +1,6 @@ import childProcess from 'node:child_process' -import express from 'express' import Module from 'node:module' +import express from 'express' import './worker.mjs' const app = express() diff --git a/integration-tests/appsec/esm-app/more.mjs b/integration-tests/appsec/esm-app/more.mjs index 444e46b761d..a92e728ce52 100644 --- a/integration-tests/appsec/esm-app/more.mjs +++ b/integration-tests/appsec/esm-app/more.mjs @@ -1,5 +1,5 @@ -import express from 'express' import childProcess from 'node:child_process' +import express from 'express' const router = express.Router() router.get('/cmdi-vulnerable', (req, res) => { diff --git a/integration-tests/appsec/iast-esbuild-cjs/iast/index.js b/integration-tests/appsec/iast-esbuild-cjs/iast/index.js index 28e60d38b33..5feeae7356b 100644 --- a/integration-tests/appsec/iast-esbuild-cjs/iast/index.js +++ b/integration-tests/appsec/iast-esbuild-cjs/iast/index.js @@ -1,7 +1,7 @@ 'use strict' -const express = require('express') const { execSync } = require('child_process') +const express = require('express') const router = express.Router() diff --git a/integration-tests/appsec/iast-esbuild-esm/iast/index.mjs b/integration-tests/appsec/iast-esbuild-esm/iast/index.mjs index 8dec642788d..d1e92a4b8fe 100644 --- a/integration-tests/appsec/iast-esbuild-esm/iast/index.mjs +++ b/integration-tests/appsec/iast-esbuild-esm/iast/index.mjs @@ -1,5 +1,5 @@ -import express from 'express' import childProcess from 'node:child_process' +import express from 'express' const router = express.Router() diff --git a/integration-tests/appsec/iast-esbuild.spec.js b/integration-tests/appsec/iast-esbuild.spec.js index 58743870e75..6fcfcfc4b38 100644 --- a/integration-tests/appsec/iast-esbuild.spec.js +++ b/integration-tests/appsec/iast-esbuild.spec.js @@ -2,11 +2,11 @@ const assert = require('node:assert/strict') -const Axios = require('axios') const childProcess = require('child_process') const fs = require('fs') const path = require('path') const { promisify } = require('util') +const Axios = require('axios') const msgpack = require('@msgpack/msgpack') const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../helpers') diff --git a/integration-tests/appsec/iast-stack-traces-with-sourcemaps.spec.js b/integration-tests/appsec/iast-stack-traces-with-sourcemaps.spec.js index 66a7980839c..66ad5a300c7 100644 --- a/integration-tests/appsec/iast-stack-traces-with-sourcemaps.spec.js +++ b/integration-tests/appsec/iast-stack-traces-with-sourcemaps.spec.js @@ -2,10 +2,10 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers') const childProcess = require('child_process') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers') describe('IAST stack traces and vulnerabilities with sourcemaps', () => { let axios, cwd, appDir, appFile, agent, proc diff --git a/integration-tests/appsec/iast.esm-security-controls.spec.js b/integration-tests/appsec/iast.esm-security-controls.spec.js index 540414218e1..398aa570b4f 100644 --- a/integration-tests/appsec/iast.esm-security-controls.spec.js +++ b/integration-tests/appsec/iast.esm-security-controls.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers') describe('ESM Security controls', () => { let axios, cwd, appFile, agent, proc diff --git a/integration-tests/appsec/iast.esm.spec.js b/integration-tests/appsec/iast.esm.spec.js index 7dbb47d1de1..26ba5ee7499 100644 --- a/integration-tests/appsec/iast.esm.spec.js +++ b/integration-tests/appsec/iast.esm.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, spawnProc, FakeAgent } = require('../helpers') describe('ESM', () => { let axios, cwd, appFile, agent, proc @@ -17,7 +17,8 @@ describe('ESM', () => { const nodeOptionsList = [ '--import dd-trace/initialize.mjs', - '--require dd-trace/init.js --loader dd-trace/loader-hook.mjs' + '--require dd-trace/init.js --loader dd-trace/loader-hook.mjs', + '--import dd-trace/register.js --require dd-trace/init' ] nodeOptionsList.forEach(nodeOptions => { diff --git a/integration-tests/appsec/multer.spec.js b/integration-tests/appsec/multer.spec.js index 75f6580d1e8..623be6b25e2 100644 --- a/integration-tests/appsec/multer.spec.js +++ b/integration-tests/appsec/multer.spec.js @@ -2,11 +2,10 @@ const assert = require('node:assert/strict') +const path = require('node:path') const axios = require('axios') const { describe, it, beforeEach, afterEach, before } = require('mocha') -const path = require('node:path') - const { sandboxCwd, useSandbox, diff --git a/integration-tests/appsec/multer/index.js b/integration-tests/appsec/multer/index.js index a4d87a28518..f34298ed621 100644 --- a/integration-tests/appsec/multer/index.js +++ b/integration-tests/appsec/multer/index.js @@ -22,9 +22,9 @@ const tracer = require('dd-trace') tracer.init(options) const http = require('http') -const express = require('express') const childProcess = require('child_process') +const express = require('express') const multer = require('multer') const uploadToMemory = multer({ storage: multer.memoryStorage(), limits: { fileSize: 200000 } }) diff --git a/integration-tests/appsec/response-headers.spec.js b/integration-tests/appsec/response-headers.spec.js index ec76a946da1..bda413cc608 100644 --- a/integration-tests/appsec/response-headers.spec.js +++ b/integration-tests/appsec/response-headers.spec.js @@ -26,7 +26,7 @@ describe('Headers collection - Fastify', () => { const env = { DD_TRACE_AGENT_PORT: agent.port, - DD_APPSEC_ENABLED: true, + DD_APPSEC_ENABLED: 'true', DD_APPSEC_RULES: path.join(cwd, 'appsec/data-collection/data-collection-rules.json') } proc = await spawnProc(appFile, { cwd, env, execArgv: [] }) diff --git a/integration-tests/appsec/trace-tagging.spec.js b/integration-tests/appsec/trace-tagging.spec.js index 6cdd9dd069d..b7ae8cba45d 100644 --- a/integration-tests/appsec/trace-tagging.spec.js +++ b/integration-tests/appsec/trace-tagging.spec.js @@ -20,7 +20,7 @@ describe('ASM Trace Tagging rules', () => { const env = { DD_TRACE_AGENT_PORT: agent.port, - DD_APPSEC_ENABLED: true, + DD_APPSEC_ENABLED: 'true', DD_APPSEC_RULES: path.join(cwd, 'appsec', 'data-collection', 'data-collection-rules.json') } diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index 0f84d33181e..26f4f3a233f 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -1,12 +1,12 @@ 'use strict' +const http = require('http') +const zlib = require('zlib') const express = require('express') const bodyParser = require('body-parser') const msgpack = require('@msgpack/msgpack') -const http = require('http') const multer = require('multer') const upload = multer() -const zlib = require('zlib') const { FakeAgent } = require('./helpers') diff --git a/integration-tests/ci-visibility/automatic-log-submission.spec.js b/integration-tests/ci-visibility/automatic-log-submission.spec.js index 5f1f1810ea7..8b61cd346db 100644 --- a/integration-tests/ci-visibility/automatic-log-submission.spec.js +++ b/integration-tests/ci-visibility/automatic-log-submission.spec.js @@ -12,8 +12,8 @@ const { assertObjectContains } = require('../helpers') const { FakeCiVisIntake } = require('../ci-visibility-intake') -const webAppServer = require('./web-app-server') const { NODE_MAJOR } = require('../../version') +const webAppServer = require('./web-app-server') describe('test optimization automatic log submission', () => { let cwd, receiver, childProcess, webAppPort @@ -24,7 +24,6 @@ describe('test optimization automatic log submission', () => { '@cucumber/cucumber', 'jest', 'winston', - 'chai@4', '@playwright/test' ], true) @@ -73,7 +72,7 @@ describe('test optimization automatic log submission', () => { getExtraEnvVars: () => ({ PW_BASE_URL: `http://localhost:${webAppPort}`, TEST_DIR: 'ci-visibility/automatic-log-submission-playwright', - DD_TRACE_DEBUG: 1 + DD_TRACE_DEBUG: '1' }) } ] diff --git a/integration-tests/ci-visibility/cucumber-plugin-tests/features/support/steps.js b/integration-tests/ci-visibility/cucumber-plugin-tests/features/support/steps.js index 963bb8e9c75..f4c7c330169 100644 --- a/integration-tests/ci-visibility/cucumber-plugin-tests/features/support/steps.js +++ b/integration-tests/ci-visibility/cucumber-plugin-tests/features/support/steps.js @@ -1,7 +1,7 @@ 'use strict' -const { Before, Given, When, Then, setWorldConstructor } = require('@cucumber/cucumber') const assert = require('assert') +const { Before, Given, When, Then, setWorldConstructor } = require('@cucumber/cucumber') const ENDPOINT_URL = process.env.DD_CIVISIBILITY_AGENTLESS_URL || `http://127.0.0.1:${process.env.DD_TRACE_AGENT_PORT}` diff --git a/integration-tests/ci-visibility/features/support/steps.js b/integration-tests/ci-visibility/features/support/steps.js index 889b88632b5..eac065f3dc6 100644 --- a/integration-tests/ci-visibility/features/support/steps.js +++ b/integration-tests/ci-visibility/features/support/steps.js @@ -1,8 +1,8 @@ 'use strict' +const tracer = require('dd-trace') const assert = require('assert') const { When, Then, Before, After } = require('@cucumber/cucumber') -const tracer = require('dd-trace') class Greeter { sayFarewell () { diff --git a/integration-tests/ci-visibility/jest-plugin-tests/jest-test.js b/integration-tests/ci-visibility/jest-plugin-tests/jest-test.js index 5aed8a33376..5829f90ca14 100644 --- a/integration-tests/ci-visibility/jest-plugin-tests/jest-test.js +++ b/integration-tests/ci-visibility/jest-plugin-tests/jest-test.js @@ -1,10 +1,9 @@ 'use strict' +const tracer = require('dd-trace') const http = require('http') const assert = require('assert') -const tracer = require('dd-trace') - const ENDPOINT_URL = process.env.DD_CIVISIBILITY_AGENTLESS_URL || `http://127.0.0.1:${process.env.DD_TRACE_AGENT_PORT}` diff --git a/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-custom-span-test.js b/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-custom-span-test.js index 9b81c8a8a08..82e9eb5ee73 100644 --- a/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-custom-span-test.js +++ b/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-custom-span-test.js @@ -1,7 +1,7 @@ 'use strict' -const { test, expect } = require('@playwright/test') const tracer = require('dd-trace') +const { test, expect } = require('@playwright/test') test.beforeEach(async ({ page }) => { await page.goto(process.env.PW_BASE_URL) diff --git a/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-tags-test.js b/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-tags-test.js index e7424277e39..857ec571569 100644 --- a/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-tags-test.js +++ b/integration-tests/ci-visibility/playwright-tests-active-test-span/active-test-span-tags-test.js @@ -1,7 +1,7 @@ 'use strict' -const { test, expect } = require('@playwright/test') const tracer = require('dd-trace') +const { test, expect } = require('@playwright/test') test.beforeEach(async ({ page }) => { await page.goto(process.env.PW_BASE_URL) diff --git a/integration-tests/ci-visibility/playwright-tests/landing-page-test.js b/integration-tests/ci-visibility/playwright-tests/landing-page-test.js index b4b342513ce..e15d1dfbdc3 100644 --- a/integration-tests/ci-visibility/playwright-tests/landing-page-test.js +++ b/integration-tests/ci-visibility/playwright-tests/landing-page-test.js @@ -1,7 +1,7 @@ 'use strict' -const { test, expect } = require('@playwright/test') const tracer = require('dd-trace') +const { test, expect } = require('@playwright/test') test.beforeEach(async ({ page }) => { await page.goto(process.env.PW_BASE_URL) diff --git a/integration-tests/ci-visibility/run-mocha.mjs b/integration-tests/ci-visibility/run-mocha.mjs index f4473e262f3..4d6785044de 100644 --- a/integration-tests/ci-visibility/run-mocha.mjs +++ b/integration-tests/ci-visibility/run-mocha.mjs @@ -1,5 +1,5 @@ -import Mocha from 'mocha' import { fileURLToPath } from 'url' +import Mocha from 'mocha' const mocha = new Mocha({ parallel: !!process.env.RUN_IN_PARALLEL diff --git a/integration-tests/ci-visibility/test-custom-tags/custom-tags.js b/integration-tests/ci-visibility/test-custom-tags/custom-tags.js index eb994e5e98a..e0162fa2052 100644 --- a/integration-tests/ci-visibility/test-custom-tags/custom-tags.js +++ b/integration-tests/ci-visibility/test-custom-tags/custom-tags.js @@ -1,8 +1,7 @@ 'use strict' -const assert = require('assert') - const tracer = require('dd-trace') +const assert = require('assert') const sum = require('../test/sum') describe('test optimization custom tags', () => { diff --git a/integration-tests/ci-visibility/test-optimization-wrong-init-cucumber/support/steps.js b/integration-tests/ci-visibility/test-optimization-wrong-init-cucumber/support/steps.js index 543c92aa780..530f8260868 100644 --- a/integration-tests/ci-visibility/test-optimization-wrong-init-cucumber/support/steps.js +++ b/integration-tests/ci-visibility/test-optimization-wrong-init-cucumber/support/steps.js @@ -3,6 +3,7 @@ const assert = require('assert') const { Given, When, Then } = require('@cucumber/cucumber') +// eslint-disable-next-line import/order const tracer = require('dd-trace') let num1, num2, result diff --git a/integration-tests/ci-visibility/test-optimization-wrong-init/sum-wrong-init-test.js b/integration-tests/ci-visibility/test-optimization-wrong-init/sum-wrong-init-test.js index b5d7db25eb6..2c3ae0f559c 100644 --- a/integration-tests/ci-visibility/test-optimization-wrong-init/sum-wrong-init-test.js +++ b/integration-tests/ci-visibility/test-optimization-wrong-init/sum-wrong-init-test.js @@ -1,7 +1,7 @@ 'use strict' -const assert = require('node:assert') const tracer = require('dd-trace') +const assert = require('node:assert') tracer.trace('sum.test', { resource: 'sum.test.js' }, () => { describe('sum', () => { diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index 2faca9f7f1c..ff6dd5e183d 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -201,7 +201,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testSpan.meta[ORIGIN_KEY], CI_APP_ORIGIN) assert.strictEqual(testSpan.meta[COMPONENT], 'cucumber') assert.strictEqual(testSpan.metrics[SAMPLING_PRIORITY], AUTO_KEEP) - assert.ok(testSpan.meta[TEST_FRAMEWORK_VERSION] != null) + assert.ok(testSpan.meta[TEST_FRAMEWORK_VERSION]) assert.strictEqual(testSpan.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) assert.strictEqual(testSpan.meta[TEST_SUITE], 'ci-visibility/cucumber-plugin-tests/features/simple.feature') assert.strictEqual( @@ -209,7 +209,7 @@ describe(`cucumber@${version} commonJS`, () => { 'ci-visibility/cucumber-plugin-tests/features/simple.feature', 'Test source file should be the simple feature' ) - assert.ok(testSpan.metrics[TEST_SOURCE_START] != null) + assert.ok(testSpan.metrics[TEST_SOURCE_START]) assert.strictEqual(testSpan.type, 'test') assert.strictEqual(testSpan.name, 'cucumber.test') assert.strictEqual(testSpan.parent_id.toString(), '0') @@ -236,7 +236,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.match(errorMessage, /AssertionError/) assert.match(errorMessage, /datadog/) assert.match(errorMessage, /godatad/) - assert.ok(testSpan.meta[ERROR_STACK] != null) + assert.ok(testSpan.meta[ERROR_STACK]) } if (testName === 'hooks fail') { @@ -245,7 +245,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.match(errorMessage, /TypeError: Cannot set/) assert.match(errorMessage, /of undefined/) assert.match(errorMessage, /boom/) - assert.ok(testSpan.meta[ERROR_STACK] != null) + assert.ok(testSpan.meta[ERROR_STACK]) } const testSteps = spans.filter( @@ -254,7 +254,7 @@ describe(`cucumber@${version} commonJS`, () => { const { steps } = testInfoByTestName[testName] steps.forEach(({ name, stepStatus }) => { const stepSpan = testSteps.find(span => span.meta['cucumber.step'] === name) - assert.ok(stepSpan != null) + assert.ok(stepSpan) assert.strictEqual(stepSpan.meta['step.status'], stepStatus, `Test ${testName} should have step ${name} with status ${stepStatus}`) assert.strictEqual(stepSpan.meta[COMPONENT], 'cucumber') @@ -328,16 +328,16 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testSessionEventContent.meta[CUCUMBER_IS_PARALLEL], 'true') } - assert.ok(testSessionEventContent.test_session_id != null) - assert.ok(testSessionEventContent.meta[TEST_COMMAND] != null) - assert.ok(testSessionEventContent.meta[TEST_TOOLCHAIN] != null) + assert.ok(testSessionEventContent.test_session_id) + assert.ok(testSessionEventContent.meta[TEST_COMMAND]) + assert.ok(testSessionEventContent.meta[TEST_TOOLCHAIN]) assert.strictEqual(testSessionEventContent.resource.startsWith('test_session.'), true) assert.strictEqual(testSessionEventContent.meta[TEST_STATUS], 'fail') - assert.ok(testModuleEventContent.test_session_id != null) - assert.ok(testModuleEventContent.test_module_id != null) - assert.ok(testModuleEventContent.meta[TEST_COMMAND] != null) - assert.ok(testModuleEventContent.meta[TEST_MODULE] != null) + assert.ok(testModuleEventContent.test_session_id) + assert.ok(testModuleEventContent.test_module_id) + assert.ok(testModuleEventContent.meta[TEST_COMMAND]) + assert.ok(testModuleEventContent.meta[TEST_MODULE]) assert.strictEqual(testModuleEventContent.resource.startsWith('test_module.'), true) assert.strictEqual(testModuleEventContent.meta[TEST_STATUS], 'fail') assert.strictEqual( @@ -363,14 +363,14 @@ describe(`cucumber@${version} commonJS`, () => { test_session_id: testSessionId } }) => { - assert.ok(meta[TEST_COMMAND] != null) - assert.ok(meta[TEST_MODULE] != null) - assert.ok(testSuiteId != null) + assert.ok(meta[TEST_COMMAND]) + assert.ok(meta[TEST_MODULE]) + assert.ok(testSuiteId) assert.strictEqual(testModuleId.toString(10), testModuleEventContent.test_module_id.toString(10)) assert.strictEqual(testSessionId.toString(10), testSessionEventContent.test_session_id.toString(10)) assert.strictEqual(meta[TEST_SOURCE_FILE].startsWith(featuresPath), true) assert.strictEqual(metrics[TEST_SOURCE_START], 1) - assert.ok(metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(metrics[DD_HOST_CPU_COUNT]) }) assert.deepStrictEqual(testEvents.map(test => test.content.resource).sort(), [ @@ -399,9 +399,9 @@ describe(`cucumber@${version} commonJS`, () => { test_session_id: testSessionId } }) => { - assert.ok(meta[TEST_COMMAND] != null) - assert.ok(meta[TEST_MODULE] != null) - assert.ok(testSuiteId != null) + assert.ok(meta[TEST_COMMAND]) + assert.ok(meta[TEST_MODULE]) + assert.ok(testSuiteId) assert.strictEqual(testModuleId.toString(10), testModuleEventContent.test_module_id.toString(10)) assert.strictEqual(testSessionId.toString(10), testSessionEventContent.test_session_id.toString(10)) assert.strictEqual(meta[TEST_SOURCE_FILE].startsWith('ci-visibility/features'), true) @@ -412,7 +412,7 @@ describe(`cucumber@${version} commonJS`, () => { if (runMode === 'parallel') { assert.strictEqual(meta[CUCUMBER_IS_PARALLEL], 'true') } - assert.ok(metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(metrics[DD_HOST_CPU_COUNT]) if (!meta[TEST_NAME].includes('Say skip')) { assert.strictEqual(meta['custom_tag.before'], 'hello before') assert.strictEqual(meta['custom_tag.after'], 'hello after') @@ -535,15 +535,15 @@ describe(`cucumber@${version} commonJS`, () => { 2, 'Steps should be covered twice' ) - assert.ok(coveragePayload.content.coverages[0].test_session_id != null) - assert.ok(coveragePayload.content.coverages[0].test_suite_id != null) + assert.ok(coveragePayload.content.coverages[0].test_session_id) + assert.ok(coveragePayload.content.coverages[0].test_suite_id) const testSession = eventsRequest .payload .events .find(event => event.type === 'test_session_end') .content - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) const eventTypes = eventsRequest.payload.events.map(event => event.type) assertObjectContains(eventTypes, ['test', 'test_session_end', 'test_module_end', 'test_suite_end']) @@ -593,7 +593,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(testSession.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testSession.meta[TEST_CODE_COVERAGE_ENABLED], 'false') assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_ENABLED], 'false') - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) const testModule = payload.events.find(event => event.type === 'test_module_end').content assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'false') @@ -1838,7 +1838,7 @@ describe(`cucumber@${version} commonJS`, () => { cwd, env: { ...envVars, - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: 1 + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'pipe' } @@ -1980,7 +1980,7 @@ describe(`cucumber@${version} commonJS`, () => { assert.strictEqual(retriedTest.metrics[`${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_LINE_SUFFIX}`], 6) const snapshotIdKey = `${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}` - assert.ok(retriedTest.meta[snapshotIdKey] != null) + assert.ok(retriedTest.meta[snapshotIdKey]) snapshotIdByTest = retriedTest.meta[snapshotIdKey] spanIdByTest = retriedTest.span_id.toString() diff --git a/integration-tests/cypress/cypress.spec.js b/integration-tests/cypress/cypress.spec.js index 3f668066549..be0d1f17aff 100644 --- a/integration-tests/cypress/cypress.spec.js +++ b/integration-tests/cypress/cypress.spec.js @@ -222,7 +222,7 @@ moduleTypes.forEach(({ span.resource === 'cypress/e2e/basic-fail.js.basic fail suite can fail' ) - assert.ok(passedTestSpan != null) + assert.ok(passedTestSpan) assert.strictEqual(passedTestSpan.name, 'cypress.test') assert.strictEqual(passedTestSpan.resource, 'cypress/e2e/basic-pass.js.basic pass suite can pass') assert.strictEqual(passedTestSpan.type, 'test') @@ -231,17 +231,17 @@ moduleTypes.forEach(({ assert.strictEqual(passedTestSpan.meta[TEST_SUITE], 'cypress/e2e/basic-pass.js') assert.strictEqual(passedTestSpan.meta[TEST_FRAMEWORK], 'cypress') assert.strictEqual(passedTestSpan.meta[TEST_TYPE], 'browser') - assert.ok(passedTestSpan.meta[TEST_SOURCE_FILE] != null) + assert.ok(passedTestSpan.meta[TEST_SOURCE_FILE]) assert.match(passedTestSpan.meta[TEST_SOURCE_FILE], /cypress\/e2e\/basic-pass\.js/) - assert.ok(passedTestSpan.meta[TEST_FRAMEWORK_VERSION] != null) - assert.ok(passedTestSpan.meta[COMPONENT] != null) - assert.ok(passedTestSpan.metrics[TEST_SOURCE_START] != null) + assert.ok(passedTestSpan.meta[TEST_FRAMEWORK_VERSION]) + assert.ok(passedTestSpan.meta[COMPONENT]) + assert.ok(passedTestSpan.metrics[TEST_SOURCE_START]) assert.strictEqual(passedTestSpan.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) assert.strictEqual(passedTestSpan.meta.customTag, 'customValue') assert.strictEqual(passedTestSpan.meta.addTagsBeforeEach, 'customBeforeEach') assert.strictEqual(passedTestSpan.meta.addTagsAfterEach, 'customAfterEach') - assert.ok(failedTestSpan != null) + assert.ok(failedTestSpan) assert.strictEqual(failedTestSpan.name, 'cypress.test') assert.strictEqual(failedTestSpan.resource, 'cypress/e2e/basic-fail.js.basic fail suite can fail') assert.strictEqual(failedTestSpan.type, 'test') @@ -250,14 +250,14 @@ moduleTypes.forEach(({ assert.strictEqual(failedTestSpan.meta[TEST_SUITE], 'cypress/e2e/basic-fail.js') assert.strictEqual(failedTestSpan.meta[TEST_FRAMEWORK], 'cypress') assert.strictEqual(failedTestSpan.meta[TEST_TYPE], 'browser') - assert.ok(failedTestSpan.meta[TEST_SOURCE_FILE] != null) + assert.ok(failedTestSpan.meta[TEST_SOURCE_FILE]) assert.match(failedTestSpan.meta[TEST_SOURCE_FILE], /cypress\/e2e\/basic-fail\.js/) - assert.ok(failedTestSpan.meta[TEST_FRAMEWORK_VERSION] != null) - assert.ok(failedTestSpan.meta[COMPONENT] != null) - assert.ok(failedTestSpan.meta[ERROR_MESSAGE] != null) + assert.ok(failedTestSpan.meta[TEST_FRAMEWORK_VERSION]) + assert.ok(failedTestSpan.meta[COMPONENT]) + assert.ok(failedTestSpan.meta[ERROR_MESSAGE]) assert.match(failedTestSpan.meta[ERROR_MESSAGE], /expected/) - assert.ok(failedTestSpan.meta[ERROR_TYPE] != null) - assert.ok(failedTestSpan.metrics[TEST_SOURCE_START] != null) + assert.ok(failedTestSpan.meta[ERROR_TYPE]) + assert.ok(failedTestSpan.metrics[TEST_SOURCE_START]) assert.strictEqual(passedTestSpan.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) assert.strictEqual(failedTestSpan.meta.customTag, 'customValue') assert.strictEqual(failedTestSpan.meta.addTagsBeforeEach, 'customBeforeEach') @@ -482,23 +482,23 @@ moduleTypes.forEach(({ const { content: testSessionEventContent } = testSessionEvent const { content: testModuleEventContent } = testModuleEvent - assert.ok(testSessionEventContent.test_session_id != null) - assert.ok(testSessionEventContent.meta[TEST_COMMAND] != null) - assert.ok(testSessionEventContent.meta[TEST_TOOLCHAIN] != null) + assert.ok(testSessionEventContent.test_session_id) + assert.ok(testSessionEventContent.meta[TEST_COMMAND]) + assert.ok(testSessionEventContent.meta[TEST_TOOLCHAIN]) assert.strictEqual(testSessionEventContent.resource.startsWith('test_session.'), true) assert.strictEqual(testSessionEventContent.meta[TEST_STATUS], 'fail') - assert.ok(testModuleEventContent.test_session_id != null) - assert.ok(testModuleEventContent.test_module_id != null) - assert.ok(testModuleEventContent.meta[TEST_COMMAND] != null) - assert.ok(testModuleEventContent.meta[TEST_MODULE] != null) + assert.ok(testModuleEventContent.test_session_id) + assert.ok(testModuleEventContent.test_module_id) + assert.ok(testModuleEventContent.meta[TEST_COMMAND]) + assert.ok(testModuleEventContent.meta[TEST_MODULE]) assert.strictEqual(testModuleEventContent.resource.startsWith('test_module.'), true) assert.strictEqual(testModuleEventContent.meta[TEST_STATUS], 'fail') assert.strictEqual( testModuleEventContent.test_session_id.toString(10), testSessionEventContent.test_session_id.toString(10) ) - assert.ok(testModuleEventContent.meta[TEST_FRAMEWORK_VERSION] != null) + assert.ok(testModuleEventContent.meta[TEST_FRAMEWORK_VERSION]) assertObjectContains(testSuiteEvents.map(suite => suite.content.resource), [ 'test_suite.cypress/e2e/other.cy.js', @@ -519,14 +519,14 @@ moduleTypes.forEach(({ test_session_id: testSessionId } }) => { - assert.ok(meta[TEST_COMMAND] != null) - assert.ok(meta[TEST_MODULE] != null) - assert.ok(testSuiteId != null) + assert.ok(meta[TEST_COMMAND]) + assert.ok(meta[TEST_MODULE]) + assert.ok(testSuiteId) assert.strictEqual(testModuleId.toString(10), testModuleEventContent.test_module_id.toString(10)) assert.strictEqual(testSessionId.toString(10), testSessionEventContent.test_session_id.toString(10)) assert.strictEqual(meta[TEST_SOURCE_FILE].startsWith('cypress/e2e/'), true) assert.strictEqual(metrics[TEST_SOURCE_START], 1) - assert.ok(metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(metrics[DD_HOST_CPU_COUNT]) }) assertObjectContains(testEvents.map(test => test.content.resource), [ @@ -550,9 +550,9 @@ moduleTypes.forEach(({ test_session_id: testSessionId } }) => { - assert.ok(meta[TEST_COMMAND] != null) - assert.ok(meta[TEST_MODULE] != null) - assert.ok(testSuiteId != null) + assert.ok(meta[TEST_COMMAND]) + assert.ok(meta[TEST_MODULE]) + assert.ok(testSuiteId) assert.strictEqual(testModuleId.toString(10), testModuleEventContent.test_module_id.toString(10)) assert.strictEqual(testSessionId.toString(10), testSessionEventContent.test_session_id.toString(10)) assert.strictEqual(meta[TEST_SOURCE_FILE].startsWith('cypress/e2e/'), true) @@ -560,7 +560,7 @@ moduleTypes.forEach(({ assert.strictEqual(meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'false') assert.strictEqual(meta['test.customtag'], 'customvalue') assert.strictEqual(meta['test.customtag2'], 'customvalue2') - assert.ok(metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(metrics[DD_HOST_CPU_COUNT]) }) }, 25000) @@ -820,7 +820,7 @@ moduleTypes.forEach(({ const notSkippedTest = events.find(event => event.content.resource === 'cypress/e2e/other.cy.js.context passes' ) - assert.ok(notSkippedTest != null) + assert.ok(notSkippedTest) assert.strictEqual(notSkippedTest.content.meta[TEST_STATUS], 'pass') }, 25000) @@ -1173,9 +1173,9 @@ moduleTypes.forEach(({ const testModuleEvent = events.find(event => event.type === 'test_module_end') testEvents.forEach(testEvent => { - assert.ok(testEvent.content.test_suite_id != null) - assert.ok(testEvent.content.test_module_id != null) - assert.ok(testEvent.content.test_session_id != null) + assert.ok(testEvent.content.test_suite_id) + assert.ok(testEvent.content.test_module_id) + assert.ok(testEvent.content.test_session_id) assert.notStrictEqual(testEvent.content.test_suite_id, testModuleEvent.content.test_module_id) }) }, 25000) @@ -1205,9 +1205,9 @@ moduleTypes.forEach(({ .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { const events = payloads.flatMap(({ payload }) => payload.events) const testSessionEvent = events.find(event => event.type === 'test_session_end') - assert.ok(testSessionEvent != null) + assert.ok(testSessionEvent) const testModuleEvent = events.find(event => event.type === 'test_module_end') - assert.ok(testModuleEvent != null) + assert.ok(testModuleEvent) const testSuiteEvents = events.filter(event => event.type === 'test_suite_end') assert.strictEqual(testSuiteEvents.length, 4) const testEvents = events.filter(event => event.type === 'test') @@ -1243,9 +1243,9 @@ moduleTypes.forEach(({ .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { const events = payloads.flatMap(({ payload }) => payload.events) const testSessionEvent = events.find(event => event.type === 'test_session_end') - assert.ok(testSessionEvent != null) + assert.ok(testSessionEvent) const testModuleEvent = events.find(event => event.type === 'test_module_end') - assert.ok(testModuleEvent != null) + assert.ok(testModuleEvent) const testSuiteEvents = events.filter(event => event.type === 'test_suite_end') assert.strictEqual(testSuiteEvents.length, 4) const testEvents = events.filter(event => event.type === 'test') @@ -2012,7 +2012,7 @@ moduleTypes.forEach(({ env: { ...restEnvVars, CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: 1, + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', SPEC_PATTERN: specToRun }, stdio: 'pipe' @@ -2302,7 +2302,7 @@ moduleTypes.forEach(({ CYPRESS_BASE_URL: `http://localhost:${webAppPort}`, CYPRESS_BASE_URL_SECOND: `http://localhost:${secondWebAppPort}`, SPEC_PATTERN: specToRun, - DD_TRACE_DEBUG: true + DD_TRACE_DEBUG: 'true' }, stdio: 'pipe' } diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 6578801f0b0..3436cf7de7e 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -5,10 +5,10 @@ const os = require('os') const { join } = require('path') const assert = require('assert') -const { pollInterval, setup } = require('./utils') const { assertObjectContains, assertUUID } = require('../helpers') const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('../../packages/dd-trace/src/remote_config/apply_states') const { version } = require('../../package.json') +const { pollInterval, setup } = require('./utils') describe('Dynamic Instrumentation', function () { describe('Default env', function () { @@ -767,7 +767,7 @@ describe('Dynamic Instrumentation', function () { describe('DD_TRACING_ENABLED=true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=true', function () { const t = setup({ - env: { DD_TRACING_ENABLED: true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: true }, + env: { DD_TRACING_ENABLED: 'true', DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: true }, dependencies: ['fastify'] }) @@ -778,7 +778,7 @@ describe('Dynamic Instrumentation', function () { describe('DD_TRACING_ENABLED=true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=false', function () { const t = setup({ - env: { DD_TRACING_ENABLED: true, DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: false }, + env: { DD_TRACING_ENABLED: 'true', DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: false }, dependencies: ['fastify'] }) @@ -789,7 +789,7 @@ describe('Dynamic Instrumentation', function () { describe('DD_TRACING_ENABLED=false', function () { const t = setup({ - env: { DD_TRACING_ENABLED: false }, + env: { DD_TRACING_ENABLED: 'false' }, dependencies: ['fastify'] }) @@ -800,6 +800,54 @@ describe('Dynamic Instrumentation', function () { ) }) }) + + describe('DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=true', function () { + const t = setup({ + env: { DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED: 'true' }, + dependencies: ['fastify'] + }) + + describe('input messages', function () { + it('should include process_tags in snapshot when enabled', function (done) { + t.agent.on('debugger-input', ({ payload }) => { + const snapshot = payload[0].debugger.snapshot + + // Check for expected process tags keys + assert.ok(snapshot.process_tags['entrypoint.name']) + assert.ok(snapshot.process_tags['entrypoint.type']) + assert.strictEqual(snapshot.process_tags['entrypoint.type'], 'script') + + done() + }) + + t.triggerBreakpoint() + t.agent.addRemoteConfig(t.rcConfig) + }) + }) + }) + + describe('DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED=false', function () { + const t = setup({ + env: { DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED: 'false' }, + dependencies: ['fastify'] + }) + + describe('input messages', function () { + it('should not include process_tags in snapshot when disabled', function (done) { + t.agent.on('debugger-input', ({ payload }) => { + const snapshot = payload[0].debugger.snapshot + + // Assert that process_tags are not present + assert.strictEqual(snapshot.process_tags, undefined) + + done() + }) + + t.triggerBreakpoint() + t.agent.addRemoteConfig(t.rcConfig) + }) + }) + }) }) function testBasicInput (t, done) { diff --git a/integration-tests/debugger/ddtags.spec.js b/integration-tests/debugger/ddtags.spec.js index 9901b267d65..805a9ed8b99 100644 --- a/integration-tests/debugger/ddtags.spec.js +++ b/integration-tests/debugger/ddtags.spec.js @@ -3,8 +3,8 @@ const os = require('os') const assert = require('assert') -const { setup } = require('./utils') const { version } = require('../../package.json') +const { setup } = require('./utils') describe('Dynamic Instrumentation', function () { describe('ddtags', function () { diff --git a/integration-tests/debugger/redact.spec.js b/integration-tests/debugger/redact.spec.js index 3cfc7237272..a8e30faaeed 100644 --- a/integration-tests/debugger/redact.spec.js +++ b/integration-tests/debugger/redact.spec.js @@ -1,8 +1,8 @@ 'use strict' -const { setup } = require('./utils') const { once } = require('node:events') const { assertObjectContains } = require('../helpers') +const { setup } = require('./utils') // Default settings is tested in unit tests, so we only need to test the env vars here describe('Dynamic Instrumentation snapshot PII redaction', function () { diff --git a/integration-tests/debugger/snapshot-pruning.spec.js b/integration-tests/debugger/snapshot-pruning.spec.js index d3ba92c4e3a..d30f1b248c7 100644 --- a/integration-tests/debugger/snapshot-pruning.spec.js +++ b/integration-tests/debugger/snapshot-pruning.spec.js @@ -17,7 +17,7 @@ describe('Dynamic Instrumentation', function () { assert.ok(payloadSize < 1024 * 1024) // 1MB const capturesJson = JSON.stringify(payload.debugger.snapshot.captures) - assert.ok(capturesJson.includes('"pruned":true')) + assert.match(capturesJson, /"pruned":true/) done() }) diff --git a/integration-tests/debugger/snapshot-time-budget.spec.js b/integration-tests/debugger/snapshot-time-budget.spec.js index 02140be6afb..12f028d4aee 100644 --- a/integration-tests/debugger/snapshot-time-budget.spec.js +++ b/integration-tests/debugger/snapshot-time-budget.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert') -const { setup } = require('./utils') const { DEFAULT_MAX_COLLECTION_SIZE, LARGE_OBJECT_SKIP_THRESHOLD } = require('../../packages/dd-trace/src/debugger/devtools_client/snapshot/constants') +const { setup } = require('./utils') describe('Dynamic Instrumentation', function () { describe('input messages', function () { @@ -20,8 +20,11 @@ describe('Dynamic Instrumentation', function () { it( 'should include partial snapshot marked with notCapturedReason: timeout', - // A tolerance of 15ms is used to avoid flakiness - test({ t, maxPausedTime: budget + 15, breakpointIndex: 0, maxReferenceDepth: 5 }, (locals) => { + // Timing is tested in unit tests with mocked time (collector-deadline.spec.js). + // This integration test verifies the end-to-end behavior: that timeout markers + // appear in snapshots when the budget is exceeded. We don't assert on exact timing + // to avoid flakiness in CI environments where execution time is unpredictable. + test({ t, breakpointIndex: 0, maxReferenceDepth: 5 }, (locals) => { assert.strictEqual( containsTimeBudget(locals), true, @@ -187,7 +190,15 @@ describe('Dynamic Instrumentation', function () { }) }) -function test ({ t, maxPausedTime = 0, breakpointIndex, maxReferenceDepth }, assertFn) { +/** + * @param {object} config + * @param {object} config.t - Test environment + * @param {number} [config.maxPausedTime] - Optional maximum pause time in ms (skips timing assertion if not provided) + * @param {number} config.breakpointIndex - Index of the breakpoint to test + * @param {number} config.maxReferenceDepth - Maximum reference depth for snapshot + * @param {Function} [assertFn] - Optional assertion function for the snapshot locals + */ +function test ({ t, maxPausedTime, breakpointIndex, maxReferenceDepth }, assertFn) { const breakpoint = t.breakpoints[breakpointIndex] return async function () { @@ -200,10 +211,12 @@ function test ({ t, maxPausedTime = 0, breakpointIndex, maxReferenceDepth }, ass const { data } = await breakpoint.triggerBreakpoint() - assert.ok( - data.paused <= maxPausedTime, - `expected thread to be paused <=${maxPausedTime}ms, but was paused for ~${data.paused}ms` - ) + if (maxPausedTime !== undefined) { + assert.ok( + data.paused <= maxPausedTime, + `expected thread to be paused <=${maxPausedTime}ms, but was paused for ~${data.paused}ms` + ) + } const snapshot = await snapshotPromise assertFn?.(snapshot.captures.lines[breakpoint.line].locals) diff --git a/integration-tests/debugger/template.spec.js b/integration-tests/debugger/template.spec.js index 34c5d2fe679..2467c3fff70 100644 --- a/integration-tests/debugger/template.spec.js +++ b/integration-tests/debugger/template.spec.js @@ -2,8 +2,8 @@ const assert = require('node:assert/strict') const semver = require('semver') -const { setup } = require('./utils') const { NODE_MAJOR } = require('../../version') +const { setup } = require('./utils') const NODE_24_11_1_OR_LATER = semver.gte(process.version, '24.11.1') diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index 35d4d6e5394..8bef9757c48 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -29,7 +29,7 @@ const pollInterval = 0.1 */ /** - * @typedef {Object} BreakpointInfo + * @typedef {object} BreakpointInfo * @property {string} sourceFile * @property {string} deployedFile * @property {number} line @@ -51,7 +51,7 @@ const pollInterval = 0.1 * The live‑debugger integration test harness returned by {@link setup}. Provides the spawned app process, fake agent, * axios client, and helpers to generate remote config and trigger breakpoints. * - * @typedef {Object} DebuggerTestEnvironment + * @typedef {object} DebuggerTestEnvironment * @property {BreakpointInfo} breakpoint - Primary breakpoint metadata. * @property {EnrichedBreakpoint[]} breakpoints - All discovered breakpoints with helpers. * @property {import('axios').AxiosInstance} axios - HTTP client bound to the test app. Throws if accessed before @@ -222,7 +222,7 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle /** * Get breakpoint information from a test file by scanning for BREAKPOINT_TOKEN markers. * - * @param {Object} [options] - Options for finding breakpoints. + * @param {object} [options] - Options for finding breakpoints. * @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. diff --git a/integration-tests/esbuild/basic-test.js b/integration-tests/esbuild/basic-test.js index 4d8b34de672..453ac9ca81d 100755 --- a/integration-tests/esbuild/basic-test.js +++ b/integration-tests/esbuild/basic-test.js @@ -1,11 +1,12 @@ #!/usr/bin/env node 'use strict' +// eslint-disable-next-line import/order const tracer = require('../../').init() // dd-trace const assert = require('assert') -const express = require('express') const http = require('http') +const express = require('express') require('knex') // has dead code paths for multiple instrumented packages require('@apollo/server') diff --git a/integration-tests/esbuild/build-and-run.esm-patched-const-banner.mjs b/integration-tests/esbuild/build-and-run.esm-patched-const-banner.mjs index 0a43b687cfe..d1053b8c8ae 100755 --- a/integration-tests/esbuild/build-and-run.esm-patched-const-banner.mjs +++ b/integration-tests/esbuild/build-and-run.esm-patched-const-banner.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node /* eslint-disable no-console */ -import esbuild from 'esbuild' import { spawnSync } from 'child_process' +import esbuild from 'esbuild' import commonConfig from './build.esm.common-config.js' await esbuild.build({ diff --git a/integration-tests/esbuild/build-and-run.esm-patched-global-banner.mjs b/integration-tests/esbuild/build-and-run.esm-patched-global-banner.mjs index d00c6c33d7e..b86edcc1b27 100755 --- a/integration-tests/esbuild/build-and-run.esm-patched-global-banner.mjs +++ b/integration-tests/esbuild/build-and-run.esm-patched-global-banner.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node /* eslint-disable no-console */ -import esbuild from 'esbuild' import { spawnSync } from 'child_process' +import esbuild from 'esbuild' import commonConfig from './build.esm.common-config.js' await esbuild.build({ diff --git a/integration-tests/esbuild/build-and-run.esm-relying-in-extension.mjs b/integration-tests/esbuild/build-and-run.esm-relying-in-extension.mjs index 1ac77c51f87..62ed640a255 100755 --- a/integration-tests/esbuild/build-and-run.esm-relying-in-extension.mjs +++ b/integration-tests/esbuild/build-and-run.esm-relying-in-extension.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node /* eslint-disable no-console */ -import esbuild from 'esbuild' import { spawnSync } from 'child_process' +import esbuild from 'esbuild' import commonConfig from './build.esm.common-config.js' await esbuild.build({ diff --git a/integration-tests/esbuild/build-and-run.esm-relying-in-format.mjs b/integration-tests/esbuild/build-and-run.esm-relying-in-format.mjs index d278a1a0733..e8b39024904 100755 --- a/integration-tests/esbuild/build-and-run.esm-relying-in-format.mjs +++ b/integration-tests/esbuild/build-and-run.esm-relying-in-format.mjs @@ -1,9 +1,9 @@ #!/usr/bin/env node /* eslint-disable no-console */ -import esbuild from 'esbuild' import { spawnSync } from 'child_process' import { renameSync } from 'fs' +import esbuild from 'esbuild' import commonConfig from './build.esm.common-config.js' await esbuild.build({ diff --git a/integration-tests/esbuild/build-and-run.esm-relying-in-out-extension.mjs b/integration-tests/esbuild/build-and-run.esm-relying-in-out-extension.mjs index e6260b3f70b..439145a9836 100755 --- a/integration-tests/esbuild/build-and-run.esm-relying-in-out-extension.mjs +++ b/integration-tests/esbuild/build-and-run.esm-relying-in-out-extension.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node /* eslint-disable no-console */ -import esbuild from 'esbuild' import { spawnSync } from 'child_process' +import esbuild from 'esbuild' import commonConfig from './build.esm.common-config.js' // output => basic-test.mjs diff --git a/integration-tests/esbuild/build-and-run.esm-unrelated-js-banner.mjs b/integration-tests/esbuild/build-and-run.esm-unrelated-js-banner.mjs index bdaa37fb11e..3a2ead0a412 100755 --- a/integration-tests/esbuild/build-and-run.esm-unrelated-js-banner.mjs +++ b/integration-tests/esbuild/build-and-run.esm-unrelated-js-banner.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node /* eslint-disable no-console */ -import esbuild from 'esbuild' import { spawnSync } from 'child_process' +import esbuild from 'esbuild' import commonConfig from './build.esm.common-config.js' await esbuild.build({ diff --git a/integration-tests/esbuild/build-and-test-aws-sdk.js b/integration-tests/esbuild/build-and-test-aws-sdk.js index 925459169f9..b516382a174 100755 --- a/integration-tests/esbuild/build-and-test-aws-sdk.js +++ b/integration-tests/esbuild/build-and-test-aws-sdk.js @@ -5,8 +5,8 @@ const fs = require('fs') const { spawnSync } = require('child_process') -const ddPlugin = require('../../esbuild') // dd-trace/esbuild const esbuild = require('esbuild') +const ddPlugin = require('../../esbuild') // dd-trace/esbuild const SCRIPT = './aws-sdk-out.js' diff --git a/integration-tests/esbuild/build-and-test-git-tags.js b/integration-tests/esbuild/build-and-test-git-tags.js index 3d99e75dd28..8a83fed6e62 100755 --- a/integration-tests/esbuild/build-and-test-git-tags.js +++ b/integration-tests/esbuild/build-and-test-git-tags.js @@ -6,8 +6,8 @@ const fs = require('fs') const { spawnSync } = require('child_process') const assert = require('assert') -const ddPlugin = require('../../esbuild') const esbuild = require('esbuild') +const ddPlugin = require('../../esbuild') const SCRIPT = './git-tags-out.js' diff --git a/integration-tests/esbuild/build-and-test-openai.js b/integration-tests/esbuild/build-and-test-openai.js index 77a2fd3ca47..e3472d3511d 100644 --- a/integration-tests/esbuild/build-and-test-openai.js +++ b/integration-tests/esbuild/build-and-test-openai.js @@ -5,8 +5,8 @@ const fs = require('fs') const { spawnSync } = require('child_process') -const ddPlugin = require('../../esbuild') // dd-trace/esbuild const esbuild = require('esbuild') +const ddPlugin = require('../../esbuild') // dd-trace/esbuild const SCRIPT = './openai-out.js' diff --git a/integration-tests/esbuild/build-and-test-skip-external.js b/integration-tests/esbuild/build-and-test-skip-external.js index 0960e96d3f7..b165d6c0316 100755 --- a/integration-tests/esbuild/build-and-test-skip-external.js +++ b/integration-tests/esbuild/build-and-test-skip-external.js @@ -4,8 +4,8 @@ const fs = require('fs') const assert = require('assert') -const ddPlugin = require('../../esbuild') // dd-trace/esbuild const esbuild = require('esbuild') +const ddPlugin = require('../../esbuild') // dd-trace/esbuild esbuild.build({ entryPoints: ['skip-external.js'], diff --git a/integration-tests/esbuild/build.js b/integration-tests/esbuild/build.js index a558c1dbf6b..0b8fd15dae5 100755 --- a/integration-tests/esbuild/build.js +++ b/integration-tests/esbuild/build.js @@ -1,8 +1,8 @@ #!/usr/bin/env node 'use strict' -const ddPlugin = require('../../esbuild') // dd-trace/esbuild const esbuild = require('esbuild') +const ddPlugin = require('../../esbuild') // dd-trace/esbuild esbuild.build({ entryPoints: ['basic-test.js'], diff --git a/integration-tests/helpers/fake-agent.js b/integration-tests/helpers/fake-agent.js index 572b9304c1a..581f3c79d94 100644 --- a/integration-tests/helpers/fake-agent.js +++ b/integration-tests/helpers/fake-agent.js @@ -57,12 +57,12 @@ 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 {object} config - Object containing the Remote Config "file" and metadata * @param {number} [config.orgId=2] - The Datadog organization ID * @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` - * @param {Object} config.config - The Remote Config "file" object + * @param {object} config.config - The Remote Config "file" object */ addRemoteConfig (config) { config = { ...config } @@ -84,7 +84,7 @@ module.exports = class FakeAgent extends EventEmitter { /** * Update an existing config object * @param {string} id - The Remote Config config ID - * @param {Object} config - The Remote Config "file" object + * @param {object} config - The Remote Config "file" object */ updateRemoteConfig (id, config) { config = JSON.stringify(config) @@ -299,6 +299,9 @@ function buildExpressServer (agent) { cached_target_files: cachedTargetFiles } = req.body + // Emit the remote config request payload for testing + agent.emit('remote-config-request', req.body) + if (state.has_error) { // Print the error sent by the client in case it's useful in debugging tests console.error(state.error) // eslint-disable-line no-console diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index 7ecc37b8d13..d5b3eab2313 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -11,9 +11,9 @@ const os = require('os') const path = require('path') const { inspect } = require('util') -const FakeAgent = require('./fake-agent') const id = require('../../packages/dd-trace/src/id') const { getCappedRange } = require('../../packages/dd-trace/test/plugins/versions') +const FakeAgent = require('./fake-agent') const { BUN, withBun } = require('./bun') const sandboxRoot = path.join(os.tmpdir(), id().toString()) @@ -24,6 +24,11 @@ const { DEBUG } = process.env // This is set by the setShouldKill function let shouldKill +// Symbol constants for dynamic value matching in assertObjectContains +const ANY_STRING = Symbol('test.ANY_STRING') +const ANY_NUMBER = Symbol('test.ANY_NUMBER') +const ANY_VALUE = Symbol('test.ANY_VALUE') + /** * @param {string} filename * @param {string} cwd @@ -167,26 +172,50 @@ function assertTelemetryPoints (pid, msgs, expectedTelemetryPoints) { } /** - * @typedef {childProcess.ChildProcess & { url: string }} SpawnedProcess + * @typedef {childProcess.ChildProcess & { + * url: string, + * stdout: NodeJS.ReadableStream, + * stderr: NodeJS.ReadableStream + * }} SpawnedProcess */ /** * Spawns a Node.js script in a child process and returns a promise that resolves when the process is ready. * + * This function expects the spawned process to stay alive (e.g., a server). If the process exits + * (even with code 0), the promise will reject with an error. + * + * For processes that are expected to run and exit cleanly, use `spawnProcAndExpectExit` instead. + * * @param {string|URL} filename - The filename of the Node.js script to spawn in a child process. * @param {childProcess.ForkOptions} [options] - The options to pass to the child process. * @param {(data: Buffer) => void} [stdioHandler] - A function that's called with one data argument to handle the * standard output of the child process. If not provided, the output will be logged to the console. * @param {(data: Buffer) => void} [stderrHandler] - A function that's called with one data argument to handle the * standard error of the child process. If not provided, the error will be logged to the console. - * @returns {Promise} A promise that resolves when the process is either ready or terminated - * without an error. If the process is terminated without an error, the promise will resolve with `undefined`. The - * returned process will have a `url` property if the process didn't terminate. + * @returns {Promise} A promise that resolves with a SpawnedProcess when the process is ready. + * The returned `SpawnedProcess` will have a `url` property that can be accessed to get the server URL. + * Note: Accessing `url` before the spawned process sends its port message will throw an error. */ function spawnProc (filename, options = {}, stdioHandler, stderrHandler) { - const proc = fork(filename, { ...options, stdio: 'pipe' }) + const proc = spawnProcImpl(filename, options, stdioHandler, stderrHandler) - return /** @type {Promise} */ (new Promise((resolve, reject) => { + let urlValue + Object.defineProperty(proc, 'url', { + get () { + if (urlValue === undefined) { + throw new Error('Process URL is not available yet. The spawned process has not sent a port message.') + } + return urlValue + }, + set (value) { + urlValue = value + }, + enumerable: true, + configurable: true + }) + + return new Promise((resolve, reject) => { proc .on('message', ({ port }) => { if (typeof port !== 'number' && typeof port !== 'string') { @@ -195,6 +224,34 @@ function spawnProc (filename, options = {}, stdioHandler, stderrHandler) { proc.url = `http://localhost:${port}` resolve(proc) }) + .on('error', reject) + .on('exit', code => { + reject(new Error(`Process exited with status code ${code}.`)) + }) + }) +} + +/** + * Spawns a Node.js script in a child process that is expected to run and exit cleanly. + * + * This function expects the process to complete and exit with code 0, in which case the promise resolves + * with `undefined`. Use this for short-lived processes like validation scripts or tests that run to completion. + * + * For long-running processes (like servers) that should not exit, use `spawnProc` instead. + * + * @param {string|URL} filename - The filename of the Node.js script to spawn in a child process. + * @param {childProcess.ForkOptions} [options] - The options to pass to the child process. + * @param {(data: Buffer) => void} [stdioHandler] - A function that's called with one data argument to handle the + * standard output of the child process. If not provided, the output will be logged to the console. + * @param {(data: Buffer) => void} [stderrHandler] - A function that's called with one data argument to handle the + * standard error of the child process. If not provided, the error will be logged to the console. + * @returns {Promise} A promise that resolves when the process exits with code 0. + */ +function spawnProcAndExpectExit (filename, options = {}, stdioHandler, stderrHandler) { + const proc = spawnProcImpl(filename, options, stdioHandler, stderrHandler) + + return new Promise((resolve, reject) => { + proc .on('error', reject) .on('exit', code => { if (code !== 0) { @@ -202,23 +259,39 @@ function spawnProc (filename, options = {}, stdioHandler, stderrHandler) { } resolve() }) + }) +} - proc.stdout.on('data', data => { - if (stdioHandler) { - stdioHandler(data) - } - // eslint-disable-next-line no-console - if (!options.silent) console.log(data.toString()) - }) +/** + * Internal implementation for spawnProc and spawnProcAndAllowExit. + * + * @param {string|URL} filename + * @param {childProcess.ForkOptions} options + * @param {(data: Buffer) => void} [stdioHandler] + * @param {(data: Buffer) => void} [stderrHandler] + * @returns {SpawnedProcess} + */ +function spawnProcImpl (filename, options, stdioHandler, stderrHandler) { + // Cast to SpawnedProcess type - when stdio is 'pipe', stdout/stderr are guaranteed non-null + const proc = /** @type {SpawnedProcess} */ (fork(filename, { ...options, stdio: 'pipe' })) - proc.stderr.on('data', data => { - if (stderrHandler) { - stderrHandler(data) - } - // eslint-disable-next-line no-console - if (!options.silent) console.error(data.toString()) - }) - })) + proc.stdout.on('data', data => { + if (stdioHandler) { + stdioHandler(data) + } + // eslint-disable-next-line no-console + if (!options.silent) console.log(data.toString()) + }) + + proc.stderr.on('data', data => { + if (stderrHandler) { + stderrHandler(data) + } + // eslint-disable-next-line no-console + if (!options.silent) console.error(data.toString()) + }) + + return proc } function log (...args) { @@ -304,6 +377,10 @@ async function createSandbox ( addFlags.push('--prefer-offline') } + if (process.env.OMIT) { + addFlags.push(...process.env.OMIT.split(',').map(omit => `--omit=${omit}`)) + } + if (DEBUG !== 'true') { addFlags.push('--silent') } @@ -553,36 +630,95 @@ function checkSpansForServiceName (spans, name) { } /** - * @overload + * @typedef {Record} AdditionalEnvArgs + */ + +/** + * Prepares spawn options for plugin integration tests. + * + * @param {string} cwd + * @param {string} serverFile + * @param {string|number} agentPort + * @param {AdditionalEnvArgs} [additionalEnvArgs] + * @param {string[]} [execArgv] + * @param {(data: Buffer) => void} [stdioHandler] + * @returns {{ filename: string, options: childProcess.ForkOptions, + * stdioHandler: ((data: Buffer) => void) | undefined }} + */ +function preparePluginIntegrationTestSpawnOptions ( + cwd, serverFile, agentPort, additionalEnvArgs, execArgv, stdioHandler +) { + additionalEnvArgs = { ...additionalEnvArgs } + + let NODE_OPTIONS = `--loader=${hookFile}` + if (additionalEnvArgs.NODE_OPTIONS !== undefined) { + if (/--(loader|import)/.test(additionalEnvArgs.NODE_OPTIONS ?? '')) { + NODE_OPTIONS = additionalEnvArgs.NODE_OPTIONS + } else { + NODE_OPTIONS += ` ${additionalEnvArgs.NODE_OPTIONS}` + } + delete additionalEnvArgs.NODE_OPTIONS + } + + return { + filename: path.join(cwd, serverFile), + options: { + cwd, + env: { + ...process.env, + NODE_OPTIONS, + DD_TRACE_AGENT_PORT: String(agentPort), + DD_TRACE_FLUSH_INTERVAL: '0', + ...additionalEnvArgs + }, + execArgv + }, + stdioHandler + } +} + +/** + * Spawns a plugin integration test process that runs a long-lived server. + * + * The spawned process should call `process.send({ port })` to signal it's ready. + * Use this for tests that spawn HTTP servers or other long-running processes. + * + * For short-lived scripts that run and exit, use `spawnPluginIntegrationTestProcAndExpectExit` instead. + * * @param {string} cwd * @param {string} serverFile * @param {string|number} agentPort - * @param {Record} [additionalEnvArgs] + * @param {AdditionalEnvArgs} [additionalEnvArgs] + * @param {string[]} [execArgv] + * @param {(data: Buffer) => void} [stdioHandler] */ +function spawnPluginIntegrationTestProc (cwd, serverFile, agentPort, additionalEnvArgs, execArgv, stdioHandler) { + const { filename, options, stdioHandler: handler } = + preparePluginIntegrationTestSpawnOptions(cwd, serverFile, agentPort, additionalEnvArgs, execArgv, stdioHandler) + return spawnProc(filename, options, handler) +} + /** + * Spawns a plugin integration test process that is expected to run and exit cleanly. + * + * Use this for short-lived test scripts that run instrumented code and exit (e.g., making a + * fetch request, DNS lookup, etc.) rather than starting a long-running server. + * + * For tests that spawn a server which should stay alive, use `spawnPluginIntegrationTestProc` instead. + * * @param {string} cwd * @param {string} serverFile * @param {string|number} agentPort + * @param {AdditionalEnvArgs} [additionalEnvArgs] + * @param {string[]} [execArgv] * @param {(data: Buffer) => void} [stdioHandler] - * @param {Record} [additionalEnvArgs] */ -async function spawnPluginIntegrationTestProc (cwd, serverFile, agentPort, stdioHandler, additionalEnvArgs) { - if (typeof stdioHandler !== 'function' && !additionalEnvArgs) { - additionalEnvArgs = stdioHandler - stdioHandler = undefined - } - additionalEnvArgs = additionalEnvArgs || {} - let env = /** @type {Record} */ ({ - NODE_OPTIONS: `--loader=${hookFile}`, - DD_TRACE_AGENT_PORT: String(agentPort), - DD_TRACE_FLUSH_INTERVAL: '0' - }) - env = { ...process.env, ...env, ...additionalEnvArgs } - return spawnProc(path.join(cwd, serverFile), { - cwd, - env, - execArgv: additionalEnvArgs.execArgv - }, stdioHandler) +function spawnPluginIntegrationTestProcAndExpectExit ( + cwd, serverFile, agentPort, additionalEnvArgs, execArgv, stdioHandler +) { + const { filename, options, stdioHandler: handler } = + preparePluginIntegrationTestSpawnOptions(cwd, serverFile, agentPort, additionalEnvArgs, execArgv, stdioHandler) + return spawnProcAndExpectExit(filename, options, handler) } /** @@ -635,9 +771,8 @@ function setShouldKill (value) { }) } -// @ts-expect-error assert.partialDeepStrictEqual does not exist on older Node.js versions -// eslint-disable-next-line n/no-unsupported-features/node-builtins -const assertObjectContains = assert.partialDeepStrictEqual || function assertObjectContains (actual, expected, msg) { +// Implementation with optional matcher support (ANY_STRING, ANY_NUMBER, ANY_VALUE) +function assertObjectContainsImpl (actual, expected, msg, useMatchers) { if (expected === null || typeof expected !== 'object') { assert.strictEqual(actual, expected, msg) return @@ -652,7 +787,7 @@ const assertObjectContains = assert.partialDeepStrictEqual || function assertObj const actualItem = actual[i] try { if (expectedItem !== null && typeof expectedItem === 'object') { - assertObjectContains(actualItem, expectedItem, msg) + assertObjectContainsImpl(actualItem, expectedItem, msg, useMatchers) } else { assert.strictEqual(actualItem, expectedItem, msg) } @@ -670,8 +805,14 @@ const assertObjectContains = assert.partialDeepStrictEqual || function assertObj for (const [key, val] of Object.entries(expected)) { assert.ok(Object.hasOwn(actual, key), msg) - if (val !== null && typeof val === 'object') { - assertObjectContains(actual[key], val, msg) + if (useMatchers && val === ANY_STRING) { + assert.strictEqual(typeof actual[key], 'string', `Expected ${key} to be a string but got ${typeof actual[key]}`) + } else if (useMatchers && val === ANY_NUMBER) { + assert.strictEqual(typeof actual[key], 'number', `Expected ${key} to be a number but got ${typeof actual[key]}`) + } else if (useMatchers && val === ANY_VALUE) { + assert.ok(actual[key] !== undefined, `Expected ${key} to be present but it was undefined`) + } else if (val !== null && typeof val === 'object') { + assertObjectContainsImpl(actual[key], val, msg, useMatchers) } else { assert.ok(actual, msg) assert.strictEqual(actual[key], expected[key], msg) @@ -679,6 +820,25 @@ const assertObjectContains = assert.partialDeepStrictEqual || function assertObj } } +// Main assertObjectContains: tries partialDeepStrictEqual or strict first, falls back to matchers +const assertObjectContains = function assertObjectContains (actual, expected, msg) { + // @ts-expect-error assert.partialDeepStrictEqual does not exist on older Node.js versions + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const assertionFn = assert.partialDeepStrictEqual || + ((actual, expected, msg) => assertObjectContainsImpl(actual, expected, msg, false)) + + try { + assertionFn(actual, expected, msg) + } catch (originalError) { + // First attempt failed, retry with matcher support + try { + assertObjectContainsImpl(actual, expected, msg, true) + } catch { + throw originalError + } + } +} + /** * @param {string} actual * @param {string} [msg] @@ -688,11 +848,15 @@ function assertUUID (actual, msg = 'not a valid UUID') { } module.exports = { + ANY_NUMBER, + ANY_STRING, + ANY_VALUE, FakeAgent, hookFile, assertObjectContains, assertUUID, spawnProc, + spawnProcAndExpectExit, telemetryForwarder, assertTelemetryPoints, runAndCheckWithTelemetry, @@ -702,6 +866,7 @@ module.exports = { getCiVisEvpProxyConfig, checkSpansForServiceName, spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, useEnv, setShouldKill, sandboxCwd, diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index dd091b63be3..cb299badeb1 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -1,26 +1,27 @@ 'use strict' -const semver = require('semver') -const { - runAndCheckWithTelemetry: testFile, - useEnv, - useSandbox, - sandboxCwd, - setShouldKill -} = require('./helpers') +const assert = require('assert') const path = require('path') const fs = require('fs') +const semver = require('semver') const DD_INJECTION_ENABLED = 'tracing' const DD_INJECT_FORCE = 'true' const DD_TRACE_DEBUG = 'true' -const { NODE_VERSION } = require('../version') +const { NODE_MAJOR, NODE_VERSION } = require('../version') const telemetryAbort = ['abort', 'reason:incompatible_runtime', 'abort.runtime', ''] const telemetryForced = ['complete', 'injection_forced:true'] const telemetryGood = ['complete', 'injection_forced:false'] const { engines } = require('../package.json') +const { + runAndCheckWithTelemetry: testFile, + useEnv, + useSandbox, + sandboxCwd, + setShouldKill +} = require('./helpers') const supportedRange = engines.node const currentVersionIsSupported = semver.satisfies(NODE_VERSION, supportedRange) // These are on by default in release tests, so we'll turn them off for @@ -99,46 +100,118 @@ function testRuntimeVersionChecks (arg, filename) { } } - if (!currentVersionIsSupported) { - context('when node version is less than engines field', () => { - useEnv({ NODE_OPTIONS }) + let pkgPath + let pkgStr - it('should not initialize the tracer', () => doTest('false\n', [])) + before(() => { + pkgPath = `${sandboxCwd()}/node_modules/dd-trace/package.json` + pkgStr = fs.readFileSync(pkgPath, 'utf8') + }) - context('with DD_INJECTION_ENABLED', () => { - useEnv({ DD_INJECTION_ENABLED }) + after(() => { + fs.writeFileSync(pkgPath, pkgStr) + }) - context('without debug', () => { - it('should not initialize the tracer', () => doTest('false\n', telemetryAbort)) + it('should be able to use the engines field', () => { + const engines = require(`${sandboxCwd()}/node_modules/dd-trace/package.json`).engines.node - it('should initialize the tracer, if DD_INJECT_FORCE', () => doTestForced('true\n', telemetryForced)) - }) + assert.match(engines, /^>=\d+ <\d+$/) + }) - context('with debug', () => { - useEnv({ DD_TRACE_DEBUG }) + context('when node version is too recent', () => { + useEnv({ NODE_OPTIONS }) + + before(() => { + const pkg = JSON.parse(pkgStr) + pkg.engines.node = `>=${NODE_MAJOR - 1} <${NODE_MAJOR}` + fs.writeFileSync(pkgPath, JSON.stringify(pkg)) + }) + + it('should not initialize the tracer', () => doTest('false\n', [])) + + context('with DD_INJECTION_ENABLED', () => { + useEnv({ DD_INJECTION_ENABLED }) + + context('without debug', () => { + it('should not initialize the tracer', () => doTest('false\n', telemetryAbort)) - it('should not initialize the tracer', () => - doTest(`Aborting application instrumentation due to incompatible_runtime. + it('should initialize the tracer, if DD_INJECT_FORCE', () => doTestForced('true\n', telemetryForced)) + }) + + context('with debug', () => { + useEnv({ DD_TRACE_DEBUG }) + + it('should not initialize the tracer', () => + doTest(`Aborting application instrumentation due to incompatible_runtime. Found incompatible runtime Node.js ${process.versions.node}, Supported runtimes: Node.js \ ->=18. +>=${NODE_MAJOR - 1} <${NODE_MAJOR}. false `, telemetryAbort)) - it('should initialize the tracer, if DD_INJECT_FORCE', () => - doTestForced(`Aborting application instrumentation due to incompatible_runtime. + it('should initialize the tracer, if DD_INJECT_FORCE', () => + doTestForced(`Aborting application instrumentation due to incompatible_runtime. Found incompatible runtime Node.js ${process.versions.node}, Supported runtimes: Node.js \ ->=18. +>=${NODE_MAJOR - 1} <${NODE_MAJOR}. DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing. Application instrumentation bootstrapping complete true `, telemetryForced)) - }) }) }) - } else { - context('when node version is more than engines field', () => { + }) + + context('when node version is too old', () => { + useEnv({ NODE_OPTIONS }) + + before(() => { + const pkg = JSON.parse(pkgStr) + pkg.engines.node = `>=${NODE_MAJOR + 1} <${NODE_MAJOR + 2}` + fs.writeFileSync(pkgPath, JSON.stringify(pkg)) + }) + + it('should not initialize the tracer', () => doTest('false\n', [])) + + context('with DD_INJECTION_ENABLED', () => { + useEnv({ DD_INJECTION_ENABLED }) + + context('without debug', () => { + it('should not initialize the tracer', () => doTest('false\n', telemetryAbort)) + + it('should initialize the tracer, if DD_INJECT_FORCE', () => doTestForced('true\n', telemetryForced)) + }) + + context('with debug', () => { + useEnv({ DD_TRACE_DEBUG }) + + it('should not initialize the tracer', () => + doTest(`Aborting application instrumentation due to incompatible_runtime. +Found incompatible runtime Node.js ${process.versions.node}, Supported runtimes: Node.js \ +>=${NODE_MAJOR + 1} <${NODE_MAJOR + 2}. +false +`, telemetryAbort)) + + it('should initialize the tracer, if DD_INJECT_FORCE', () => + doTestForced(`Aborting application instrumentation due to incompatible_runtime. +Found incompatible runtime Node.js ${process.versions.node}, Supported runtimes: Node.js \ +>=${NODE_MAJOR + 1} <${NODE_MAJOR + 2}. +DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing. +Application instrumentation bootstrapping complete +true +`, telemetryForced)) + }) + }) + }) + + if (currentVersionIsSupported) { + context('when node version is in range of the engines field', () => { useEnv({ NODE_OPTIONS }) + before(() => { + const pkg = JSON.parse(pkgStr) + pkg.engines.node = '>=0 <1000' + fs.writeFileSync(pkgPath, JSON.stringify(pkg)) + }) + it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => doTest('true\n', [], 'manual')) context('with DD_INJECTION_ENABLED', () => { diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index 32e04324a4b..2181b84d13a 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -101,7 +101,6 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { useSandbox([ `jest@${JEST_VERSION}`, - 'chai@v4', `jest-jasmine2@${JEST_VERSION}`, // jest-environment-jsdom is included in older versions of jest JEST_VERSION === 'latest' && `jest-environment-jsdom@${JEST_VERSION}`, @@ -191,7 +190,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { testSpans.forEach(testSpan => { assert.strictEqual(testSpan.meta[TEST_SOURCE_FILE].startsWith('ci-visibility/test/ci-visibility-test'), true) - assert.ok(testSpan.metrics[TEST_SOURCE_START] != null) + assert.ok(testSpan.metrics[TEST_SOURCE_START]) }) }) @@ -241,7 +240,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { (!parameters || test.meta[TEST_PARAMETERS] === JSON.stringify(parameters)) ) - assert.ok(test != null) + assert.ok(test) assert.strictEqual(test.meta.language, 'javascript') assert.strictEqual(test.meta.service, 'plugin-tests') @@ -262,8 +261,8 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(test.service, 'plugin-tests') assert.strictEqual(test.resource, `ci-visibility/jest-plugin-tests/jest-test.js.${name}`) - assert.ok(test.metrics[TEST_SOURCE_START] != null) - assert.ok(test.meta[TEST_FRAMEWORK_VERSION] != null) + assert.ok(test.metrics[TEST_SOURCE_START]) + assert.ok(test.meta[TEST_FRAMEWORK_VERSION]) if (extraTags) { Object.entries(extraTags).forEach(([key, value]) => { @@ -319,7 +318,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { span.resource === `ci-visibility/jest-plugin-tests/jest-hook-failure.js.${name}` ) - assert.ok(testSpan != null) + assert.ok(testSpan) assert.strictEqual(testSpan.meta.language, 'javascript') assert.strictEqual(testSpan.meta[ORIGIN_KEY], CI_APP_ORIGIN) assert.strictEqual(testSpan.meta[TEST_FRAMEWORK], 'jest') @@ -334,7 +333,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testSpan.type, 'test') assert.strictEqual(testSpan.name, 'jest.test') assert.strictEqual(testSpan.resource, `ci-visibility/jest-plugin-tests/jest-hook-failure.js.${name}`) - assert.ok(testSpan.meta[TEST_FRAMEWORK_VERSION] != null) + assert.ok(testSpan.meta[TEST_FRAMEWORK_VERSION]) }) }, 25000) @@ -372,7 +371,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { span.resource === `ci-visibility/jest-plugin-tests/jest-focus.js.${name}` ) - assert.ok(testSpan != null) + assert.ok(testSpan) assert.strictEqual(testSpan.meta.language, 'javascript') assert.strictEqual(testSpan.meta[ORIGIN_KEY], CI_APP_ORIGIN) assert.strictEqual(testSpan.meta[TEST_FRAMEWORK], 'jest') @@ -384,7 +383,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testSpan.type, 'test') assert.strictEqual(testSpan.name, 'jest.test') assert.strictEqual(testSpan.resource, `ci-visibility/jest-plugin-tests/jest-focus.js.${name}`) - assert.ok(testSpan.meta[TEST_FRAMEWORK_VERSION] != null) + assert.ok(testSpan.meta[TEST_FRAMEWORK_VERSION]) }) }, 25000) @@ -413,7 +412,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { const testSpan = payloads .flatMap(({ payload }) => payload.flatMap(trace => trace)) .find(span => span.type === 'test') - assert.ok(testSpan != null) + assert.ok(testSpan) assert.strictEqual(testSpan.meta[TEST_NAME], 'jest-inject-globals will be run') assert.strictEqual(testSpan.meta[TEST_STATUS], 'pass') assert.strictEqual(testSpan.meta[TEST_SUITE], 'ci-visibility/jest-plugin-tests/jest-inject-globals.js') @@ -476,8 +475,8 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { ] ) assert.strictEqual(suites.length, 2) - assert.ok(sessionEventContent != null) - assert.ok(moduleEventContent != null) + assert.ok(sessionEventContent) + assert.ok(moduleEventContent) assert.match(testOutput, new RegExp(expectedStdout)) @@ -486,12 +485,12 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { testEvent.meta[TEST_SOURCE_FILE].startsWith('ci-visibility/test/ci-visibility-test'), true ) - assert.ok(testEvent.metrics[TEST_SOURCE_START] != null) + assert.ok(testEvent.metrics[TEST_SOURCE_START]) assert.strictEqual(testEvent.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'false') // Can read DD_TAGS assert.strictEqual(testEvent.meta['test.customtag'], 'customvalue') assert.strictEqual(testEvent.meta['test.customtag2'], 'customvalue2') - assert.ok(testEvent.metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(testEvent.metrics[DD_HOST_CPU_COUNT]) }) suites.forEach(testSuite => { @@ -500,7 +499,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { true ) assert.strictEqual(testSuite.metrics[TEST_SOURCE_START], 1) - assert.ok(testSuite.metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(testSuite.metrics[DD_HOST_CPU_COUNT]) }) done() } catch (error) { @@ -542,38 +541,38 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { const testSuiteEvent = events.find(event => event.type === 'test_suite_end').content const testEvent = events.find(event => event.type === 'test').content - assert.ok(testSessionEvent != null) + assert.ok(testSessionEvent) assert.strictEqual(testSessionEvent.meta[TEST_STATUS], 'pass') - assert.ok(testSessionEvent[TEST_SESSION_ID] != null) - assert.ok(testSessionEvent.meta[TEST_COMMAND] != null) + assert.ok(testSessionEvent[TEST_SESSION_ID]) + assert.ok(testSessionEvent.meta[TEST_COMMAND]) assert.ok(testSessionEvent[TEST_SUITE_ID] == null) assert.ok(testSessionEvent[TEST_MODULE_ID] == null) - assert.ok(testModuleEvent != null) + assert.ok(testModuleEvent) assert.strictEqual(testModuleEvent.meta[TEST_STATUS], 'pass') - assert.ok(testModuleEvent[TEST_SESSION_ID] != null) - assert.ok(testModuleEvent[TEST_MODULE_ID] != null) - assert.ok(testModuleEvent.meta[TEST_COMMAND] != null) + assert.ok(testModuleEvent[TEST_SESSION_ID]) + assert.ok(testModuleEvent[TEST_MODULE_ID]) + assert.ok(testModuleEvent.meta[TEST_COMMAND]) assert.ok(testModuleEvent[TEST_SUITE_ID] == null) - assert.ok(testSuiteEvent != null) + assert.ok(testSuiteEvent) assert.strictEqual(testSuiteEvent.meta[TEST_STATUS], 'pass') assert.strictEqual(testSuiteEvent.meta[TEST_SUITE], 'ci-visibility/jest-plugin-tests/jest-test-suite.js') - assert.ok(testSuiteEvent.meta[TEST_COMMAND] != null) - assert.ok(testSuiteEvent.meta[TEST_MODULE] != null) - assert.ok(testSuiteEvent[TEST_SUITE_ID] != null) - assert.ok(testSuiteEvent[TEST_SESSION_ID] != null) - assert.ok(testSuiteEvent[TEST_MODULE_ID] != null) + assert.ok(testSuiteEvent.meta[TEST_COMMAND]) + assert.ok(testSuiteEvent.meta[TEST_MODULE]) + assert.ok(testSuiteEvent[TEST_SUITE_ID]) + assert.ok(testSuiteEvent[TEST_SESSION_ID]) + assert.ok(testSuiteEvent[TEST_MODULE_ID]) - assert.ok(testEvent != null) + assert.ok(testEvent) assert.strictEqual(testEvent.meta[TEST_STATUS], 'pass') assert.strictEqual(testEvent.meta[TEST_NAME], 'jest-test-suite-visibility works') assert.strictEqual(testEvent.meta[TEST_SUITE], 'ci-visibility/jest-plugin-tests/jest-test-suite.js') - assert.ok(testEvent.meta[TEST_COMMAND] != null) - assert.ok(testEvent.meta[TEST_MODULE] != null) - assert.ok(testEvent[TEST_SUITE_ID] != null) - assert.ok(testEvent[TEST_SESSION_ID] != null) - assert.ok(testEvent[TEST_MODULE_ID] != null) + assert.ok(testEvent.meta[TEST_COMMAND]) + assert.ok(testEvent.meta[TEST_MODULE]) + assert.ok(testEvent[TEST_SUITE_ID]) + assert.ok(testEvent[TEST_SESSION_ID]) + assert.ok(testEvent[TEST_MODULE_ID]) }) childProcess = exec( @@ -823,7 +822,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { env: { DD_TRACE_AGENT_PORT: receiver.port, NODE_OPTIONS: '-r dd-trace/ci/init', - RUN_IN_PARALLEL: true + RUN_IN_PARALLEL: 'true', }, stdio: 'pipe' }) @@ -844,7 +843,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { cwd, env: { ...getCiVisAgentlessConfig(receiver.port), - RUN_IN_PARALLEL: true, + RUN_IN_PARALLEL: 'true', DD_TEST_SESSION_NAME: 'my-test-session' }, stdio: 'pipe' @@ -874,7 +873,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { cwd, env: { ...getCiVisEvpProxyConfig(receiver.port), - RUN_IN_PARALLEL: true + RUN_IN_PARALLEL: 'true', }, stdio: 'pipe' }) @@ -915,7 +914,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(retriedTest.metrics[`${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_LINE_SUFFIX}`], 6) const snapshotIdKey = `${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}` - assert.ok(retriedTest.meta[snapshotIdKey] != null) + assert.ok(retriedTest.meta[snapshotIdKey]) snapshotIdByTest = retriedTest.meta[snapshotIdKey] spanIdByTest = retriedTest.span_id.toString() @@ -946,7 +945,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'dynamic-instrumentation/test-', DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', - RUN_IN_PARALLEL: true + RUN_IN_PARALLEL: 'true', }, stdio: 'inherit' } @@ -969,7 +968,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { env: { ...getCiVisAgentlessConfig(receiver.port), NODE_OPTIONS: '-r dd-trace/ci/init', - RUN_IN_PARALLEL: true, + RUN_IN_PARALLEL: 'true', TESTS_TO_RUN: 'timeout-test/timeout-test.js' }, stdio: 'pipe' @@ -1051,8 +1050,8 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'jest-bad-import/jest-bad-import-test', - RUN_IN_PARALLEL: true, - WAIT_FOR_UNHANDLED_REJECTIONS: true + RUN_IN_PARALLEL: 'true', + WAIT_FOR_UNHANDLED_REJECTIONS: 'true' }, stdio: 'inherit' }) @@ -1103,7 +1102,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { env: { ...getCiVisAgentlessConfig(receiver.port), TESTS_TO_RUN: 'jest-bad-import-torn-down/jest-bad-import-test', - RUN_IN_PARALLEL: true, + RUN_IN_PARALLEL: 'true', }, stdio: 'inherit' }) @@ -1146,7 +1145,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { receiver.assertPayloadReceived(({ payload }) => { const testSession = payload.events.find(event => event.type === 'test_session_end').content - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) }, ({ url }) => url === '/api/v2/citestcycle').then(() => done()).catch(done) childProcess = exec( @@ -1170,8 +1169,8 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { const testSuites = events.filter(event => event.type === 'test_suite_end') const tests = events.filter(event => event.type === 'test') - assert.ok(testSession != null) - assert.ok(testModule != null) + assert.ok(testSession) + assert.ok(testModule) assert.strictEqual(testSuites.length, 2) assert.strictEqual(tests.length, 2) }) @@ -1301,7 +1300,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { childProcess = fork(startupTestFile, { cwd, env: { - DD_CIVISIBILITY_AGENTLESS_ENABLED: 1, + DD_CIVISIBILITY_AGENTLESS_ENABLED: '1', NODE_OPTIONS: '-r dd-trace/ci/init' }, stdio: 'pipe' @@ -1431,11 +1430,11 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assertObjectContains(allCoverageFiles.sort(), expectedCoverageFiles.sort()) const [coveragePayload] = codeCovRequest.payload - assert.ok(coveragePayload.content.coverages[0].test_session_id != null) - assert.ok(coveragePayload.content.coverages[0].test_suite_id != null) + assert.ok(coveragePayload.content.coverages[0].test_session_id) + assert.ok(coveragePayload.content.coverages[0].test_suite_id) const testSession = eventsRequest.payload.events.find(event => event.type === 'test_session_end').content - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) const eventTypes = eventsRequest.payload.events.map(event => event.type) assertObjectContains(eventTypes, ['test', 'test_suite_end', 'test_session_end', 'test_module_end']) @@ -1482,7 +1481,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(testSession.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testSession.meta[TEST_CODE_COVERAGE_ENABLED], 'false') assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_ENABLED], 'false') - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) const testModule = payload.events.find(event => event.type === 'test_module_end').content assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'false') @@ -2792,7 +2791,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { ...getCiVisAgentlessConfig(receiver.port), // use agentless for this test, just for variety TESTS_TO_RUN: 'test/ci-visibility-test', ENABLE_JSDOM: true, - DD_TRACE_DEBUG: 1, + DD_TRACE_DEBUG: '1', DD_TRACE_LOG_LEVEL: 'warn' }, stdio: 'inherit' @@ -3386,7 +3385,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { env: { ...getCiVisEvpProxyConfig(receiver.port), TESTS_TO_RUN: 'jest-flaky/flaky-', - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: 1 + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' } @@ -3519,7 +3518,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(retriedTest.metrics[`${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_LINE_SUFFIX}`], 6) const snapshotIdKey = `${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}` - assert.ok(retriedTest.meta[snapshotIdKey] != null) + assert.ok(retriedTest.meta[snapshotIdKey]) snapshotIdByTest = retriedTest.meta[snapshotIdKey] spanIdByTest = retriedTest.span_id.toString() @@ -3603,7 +3602,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { assert.strictEqual(retriedTest.metrics[`${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_LINE_SUFFIX}`], 6) const snapshotIdKey = `${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}` - assert.ok(retriedTest.meta[snapshotIdKey] != null) + assert.ok(retriedTest.meta[snapshotIdKey]) snapshotIdByTest = retriedTest.meta[snapshotIdKey] spanIdByTest = retriedTest.span_id.toString() @@ -4801,7 +4800,7 @@ describe(`jest@${JEST_VERSION} commonJS`, () => { fs.writeFileSync( path.join(cwd, 'ci-visibility/test-impacted-test/test-impacted-1.js'), - `const { expect } = require('chai') + `const assert = require('assert') describe('impacted tests', () => { it('can pass normally', () => { diff --git a/integration-tests/log_injection.spec.js b/integration-tests/log_injection.spec.js index 478d71ac1a9..b592137001b 100644 --- a/integration-tests/log_injection.spec.js +++ b/integration-tests/log_injection.spec.js @@ -1,5 +1,7 @@ 'use strict' +const path = require('path') +const { USER_KEEP } = require('../ext/priority') const { FakeAgent, sandboxCwd, @@ -8,8 +10,6 @@ const { curlAndAssertMessage, assertObjectContains } = require('./helpers') -const path = require('path') -const { USER_KEEP } = require('../ext/priority') describe('Log Injection', () => { let agent diff --git a/integration-tests/memory-leak/index.js b/integration-tests/memory-leak/index.js index 8d87a5ab99e..c65e264ca97 100644 --- a/integration-tests/memory-leak/index.js +++ b/integration-tests/memory-leak/index.js @@ -3,6 +3,7 @@ const tracer = require('../../') tracer.init() +// eslint-disable-next-line import/order const http = require('http') http.createServer((req, res) => { diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index af50f45e597..4fe4616e58f 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -97,7 +97,6 @@ describe(`mocha@${MOCHA_VERSION}`, function () { useSandbox( [ `mocha@${MOCHA_VERSION}`, - 'chai@v4', 'nyc', 'mocha-each', 'workerpool' @@ -149,7 +148,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { testSpans.forEach(testSpan => { assert.strictEqual(testSpan.meta[TEST_SOURCE_FILE].startsWith('ci-visibility/test/ci-visibility-test'), true) - assert.ok(testSpan.metrics[TEST_SOURCE_START] != null) + assert.ok(testSpan.metrics[TEST_SOURCE_START]) }) done() @@ -212,20 +211,20 @@ describe(`mocha@${MOCHA_VERSION}`, function () { ] ) assert.strictEqual(suites.length, 2) - assert.ok(sessionEventContent != null) - assert.ok(moduleEventContent != null) + assert.ok(sessionEventContent) + assert.ok(moduleEventContent) tests.forEach(testEvent => { assert.strictEqual( testEvent.meta[TEST_SOURCE_FILE].startsWith('ci-visibility/test/ci-visibility-test'), true ) - assert.ok(testEvent.metrics[TEST_SOURCE_START] != null) + assert.ok(testEvent.metrics[TEST_SOURCE_START]) assert.strictEqual(testEvent.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'false') // Can read DD_TAGS assert.strictEqual(testEvent.meta['test.customtag'], 'customvalue') assert.strictEqual(testEvent.meta['test.customtag2'], 'customvalue2') - assert.ok(testEvent.metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(testEvent.metrics[DD_HOST_CPU_COUNT]) }) suites.forEach(testSuite => { @@ -234,7 +233,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { true ) assert.strictEqual(testSuite.metrics[TEST_SOURCE_START], 1) - assert.ok(testSuite.metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(testSuite.metrics[DD_HOST_CPU_COUNT]) }) }) @@ -285,7 +284,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.parent_id.toString(), '0') assert.strictEqual(test.meta[TEST_STATUS], 'pass') assert.strictEqual(test.meta[ORIGIN_KEY], CI_APP_ORIGIN) - assert.ok(test.meta[TEST_FRAMEWORK_VERSION] != null) + assert.ok(test.meta[TEST_FRAMEWORK_VERSION]) assert.strictEqual(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) assert.strictEqual(test.meta[LIBRARY_VERSION], ddTraceVersion) assert.strictEqual(test.meta[COMPONENT], 'mocha') @@ -324,8 +323,8 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/failing.js') assert.strictEqual(test.meta[ERROR_TYPE], 'AssertionError') assert.strictEqual(test.meta[ERROR_MESSAGE], 'Expected values to be strictly equal:\n\ntrue !== false\n') - assert.ok(test.metrics[TEST_SOURCE_START] != null) - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.metrics[TEST_SOURCE_START]) + assert.ok(test.meta[ERROR_STACK]) assert.strictEqual(test.parent_id.toString(), '0') assert.strictEqual(test.type, 'test') assert.strictEqual(test.name, 'mocha.test') @@ -435,7 +434,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/done-fail.js') assert.strictEqual(test.meta[ERROR_TYPE], 'AssertionError') assert.strictEqual(test.meta[ERROR_MESSAGE], 'Expected values to be strictly equal:\n\ntrue !== false\n') - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) }) childProcess = exec( @@ -500,7 +499,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/promise-fail.js') assert.strictEqual(test.meta[ERROR_TYPE], 'AssertionError') assert.strictEqual(test.meta[ERROR_MESSAGE], 'Expected values to be strictly equal:\n\ntrue !== false\n') - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) }) childProcess = exec( @@ -565,7 +564,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/async-fail.js') assert.strictEqual(test.meta[ERROR_TYPE], 'AssertionError') assert.strictEqual(test.meta[ERROR_MESSAGE], 'Expected values to be strictly equal:\n\ntrue !== false\n') - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) }) childProcess = exec( @@ -599,7 +598,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/timeout-fail.js') assert.strictEqual(test.meta[ERROR_TYPE], 'Error') assert.match(test.meta[ERROR_MESSAGE], /Timeout/) - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) }) childProcess = exec( @@ -663,7 +662,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_SUITE], 'ci-visibility/mocha-plugin-tests/parameterized.js') assert.strictEqual(test.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/parameterized.js') assert.strictEqual(test.meta[TEST_PARAMETERS], JSON.stringify({ arguments: [1, 2, 3], metadata: {} })) - assert.ok(test.metrics[TEST_SOURCE_START] != null) + assert.ok(test.metrics[TEST_SOURCE_START]) assert.strictEqual(test.parent_id.toString(), '0') assert.strictEqual(test.type, 'test') assert.strictEqual(test.name, 'mocha.test') @@ -695,7 +694,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { const [testSpan] = tests const httpSpan = spans.find(span => span.name === 'http.request') - assert.ok(httpSpan != null) + assert.ok(httpSpan) // Test span assertions assert.strictEqual(testSpan.meta[COMPONENT], 'mocha') @@ -705,7 +704,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(testSpan.meta[TEST_SUITE], 'ci-visibility/mocha-plugin-tests/integration.js') assert.strictEqual(testSpan.meta[TEST_SOURCE_FILE], 'ci-visibility/mocha-plugin-tests/integration.js') assert.strictEqual(testSpan.meta[ORIGIN_KEY], CI_APP_ORIGIN) - assert.ok(testSpan.metrics[TEST_SOURCE_START] != null) + assert.ok(testSpan.metrics[TEST_SOURCE_START]) assert.strictEqual(testSpan.parent_id.toString(), '0') // HTTP span assertions @@ -747,7 +746,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { .includes('mocha-fail-hook-sync "before each" hook for "will not run but be reported as failed":') ) assert.match(test.meta[ERROR_MESSAGE], /Cannot set /) - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) }) childProcess = exec( @@ -834,13 +833,13 @@ describe(`mocha@${MOCHA_VERSION}`, function () { testNames.forEach(({ name, status, errorMsg }) => { const test = tests.find(t => t.meta[TEST_NAME] === name) - assert.ok(test != null) + assert.ok(test) assert.strictEqual(test.meta[TEST_STATUS], status) assert.strictEqual(test.meta[COMPONENT], 'mocha') if (errorMsg) { assert.strictEqual(test.meta[ERROR_MESSAGE].startsWith(errorMsg), true) assert.strictEqual(test.meta[ERROR_TYPE], 'Error') - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) } }) }) @@ -872,7 +871,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(test.meta[TEST_STATUS], 'fail') assert.strictEqual(test.meta[ERROR_TYPE], 'AssertionError') assert.strictEqual(test.meta[ERROR_MESSAGE], 'Expected values to be strictly equal:\n\ntrue !== false\n') - assert.ok(test.meta[ERROR_STACK] != null) + assert.ok(test.meta[ERROR_STACK]) }) childProcess = exec( @@ -953,7 +952,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { testNames.forEach(({ name, status }) => { const test = tests.find(t => t.meta[TEST_NAME] === name) - assert.ok(test != null) + assert.ok(test) assert.strictEqual(test.meta[TEST_STATUS], status) assert.strictEqual(test.meta[COMPONENT], 'mocha') }) @@ -1001,8 +1000,8 @@ describe(`mocha@${MOCHA_VERSION}`, function () { const testModuleEvent = events.find(event => event.type === 'test_module_end')?.content const testSuiteEvents = events.filter(event => event.type === 'test_suite_end').map(e => e.content) - assert.ok(testSessionEvent != null) - assert.ok(testModuleEvent != null) + assert.ok(testSessionEvent) + assert.ok(testModuleEvent) assert.strictEqual(testSuiteEvents.length, 4, 'Should have 4 test suite events') assert.strictEqual(testSessionEvent.meta[TEST_STATUS], 'fail') @@ -1206,7 +1205,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { cwd, env: { ...restEnvVars, - DD_TRACE_DEBUG: 1, + DD_TRACE_DEBUG: '1', DD_TRACE_LOG_LEVEL: 'error', DD_SITE: '= invalid = url' }, @@ -1254,9 +1253,9 @@ describe(`mocha@${MOCHA_VERSION}`, function () { test_module_id: testModuleId, test_session_id: testSessionId }) => { - assert.ok(meta[TEST_COMMAND] != null) - assert.ok(meta[TEST_MODULE] != null) - assert.ok(testSuiteId != null) + assert.ok(meta[TEST_COMMAND]) + assert.ok(meta[TEST_MODULE]) + assert.ok(testSuiteId) assert.strictEqual(testModuleId.toString(10), moduleEventContent.test_module_id.toString(10)) assert.strictEqual(testSessionId.toString(10), moduleEventContent.test_session_id.toString(10)) }) @@ -1268,13 +1267,13 @@ describe(`mocha@${MOCHA_VERSION}`, function () { test_module_id: testModuleId, test_session_id: testSessionId }) => { - assert.ok(meta[TEST_COMMAND] != null) - assert.ok(meta[TEST_MODULE] != null) - assert.ok(testSuiteId != null) + assert.ok(meta[TEST_COMMAND]) + assert.ok(meta[TEST_MODULE]) + assert.ok(testSuiteId) assert.strictEqual(testModuleId.toString(10), moduleEventContent.test_module_id.toString(10)) assert.strictEqual(testSessionId.toString(10), moduleEventContent.test_session_id.toString(10)) assert.strictEqual(meta[MOCHA_IS_PARALLEL], 'true') - assert.ok(metrics[TEST_SOURCE_START] != null) + assert.ok(metrics[TEST_SOURCE_START]) }) }) @@ -1283,7 +1282,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { env: { ...getCiVisAgentlessConfig(receiver.port), RUN_IN_PARALLEL: true, - DD_TRACE_DEBUG: 1, + DD_TRACE_DEBUG: '1', DD_TRACE_LOG_LEVEL: 'warn', DD_TEST_SESSION_NAME: 'my-test-session' }, @@ -1393,7 +1392,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { childProcess = fork(startupTestFile, { cwd, env: { - DD_CIVISIBILITY_AGENTLESS_ENABLED: 1, + DD_CIVISIBILITY_AGENTLESS_ENABLED: '1', NODE_OPTIONS: '-r dd-trace/ci/init' }, stdio: 'pipe' @@ -1531,11 +1530,11 @@ describe(`mocha@${MOCHA_VERSION}`, function () { ) const [coveragePayload] = codeCovRequest.payload - assert.ok(coveragePayload.content.coverages[0].test_session_id != null) - assert.ok(coveragePayload.content.coverages[0].test_suite_id != null) + assert.ok(coveragePayload.content.coverages[0].test_session_id) + assert.ok(coveragePayload.content.coverages[0].test_suite_id) const testSession = eventsRequest.payload.events.find(event => event.type === 'test_session_end').content - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) const eventTypes = eventsRequest.payload.events.map(event => event.type) assertObjectContains(eventTypes, ['test', 'test_session_end', 'test_module_end', 'test_suite_end']) @@ -1583,7 +1582,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { assert.strictEqual(testSession.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testSession.meta[TEST_CODE_COVERAGE_ENABLED], 'false') assert.strictEqual(testSession.meta[TEST_ITR_SKIPPING_ENABLED], 'false') - assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] != null) + assert.ok(testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT]) const testModule = payload.events.find(event => event.type === 'test_module_end').content assert.strictEqual(testModule.meta[TEST_ITR_TESTS_SKIPPED], 'false') assert.strictEqual(testModule.meta[TEST_CODE_COVERAGE_ENABLED], 'false') @@ -3070,7 +3069,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { TESTS_TO_RUN: JSON.stringify([ './test-flaky-test-retries/eventually-passing-test.js' ]), - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: 1 + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1' }, stdio: 'inherit' } @@ -3302,7 +3301,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { const snapshotIdKey = `${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}` - assert.ok(retriedTest.meta[snapshotIdKey] != null) + assert.ok(retriedTest.meta[snapshotIdKey]) snapshotIdByTest = retriedTest.meta[snapshotIdKey] spanIdByTest = retriedTest.span_id.toString() @@ -4231,7 +4230,7 @@ describe(`mocha@${MOCHA_VERSION}`, function () { execSync('git checkout -b feature-branch', { cwd, stdio: 'ignore' }) fs.writeFileSync( path.join(cwd, 'ci-visibility/test-impacted-test/test-impacted-1.js'), - `const { expect } = require('chai') + `const assert = require('assert') describe('impacted tests', () => { it('can pass normally', () => { assert.strictEqual(2 + 2, 3) diff --git a/integration-tests/openfeature/openfeature-exposure-events.spec.js b/integration-tests/openfeature/openfeature-exposure-events.spec.js index 8ab50dfb91d..0158c5931f7 100644 --- a/integration-tests/openfeature/openfeature-exposure-events.spec.js +++ b/integration-tests/openfeature/openfeature-exposure-events.spec.js @@ -2,8 +2,8 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../helpers') const path = require('path') +const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../helpers') const { UNACKNOWLEDGED, ACKNOWLEDGED } = require('../../packages/dd-trace/src/remote_config/apply_states') const ufcPayloads = require('./fixtures/ufc-payloads') const RC_PRODUCT = 'FFE_FLAGS' diff --git a/integration-tests/opentelemetry-logs.spec.js b/integration-tests/opentelemetry-logs.spec.js index 0fd1dce5d8e..dbfaa0520e6 100644 --- a/integration-tests/opentelemetry-logs.spec.js +++ b/integration-tests/opentelemetry-logs.spec.js @@ -1,8 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { useSandbox } = require('./helpers') const http = require('http') +const { useSandbox } = require('./helpers') describe('OpenTelemetry Logs Integration', () => { useSandbox() diff --git a/integration-tests/opentelemetry.spec.js b/integration-tests/opentelemetry.spec.js index 9684abdcfd7..6b089ed3cd4 100644 --- a/integration-tests/opentelemetry.spec.js +++ b/integration-tests/opentelemetry.spec.js @@ -2,10 +2,10 @@ const assert = require('node:assert/strict') -const { FakeAgent, sandboxCwd, useSandbox } = require('./helpers') const { fork } = require('child_process') const { join } = require('path') const axios = require('axios') +const { FakeAgent, sandboxCwd, useSandbox } = require('./helpers') async function check (agent, proc, timeout, onMessage = () => { }, isMetrics) { const messageReceiver = isMetrics @@ -83,9 +83,9 @@ describe('opentelemetry', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_TRACE_OTEL_ENABLED: 1, - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - TIMEOUT: 1500, + DD_TRACE_OTEL_ENABLED: '1', + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + TIMEOUT: '1500', DD_SERVICE: 'service', DD_TRACE_LOG_LEVEL: 'error', DD_TRACE_SAMPLE_RATE: '0.5', @@ -115,9 +115,9 @@ describe('opentelemetry', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_TRACE_OTEL_ENABLED: 1, - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - TIMEOUT: 1500, + DD_TRACE_OTEL_ENABLED: '1', + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + TIMEOUT: '1500', DD_SERVICE: 'service', OTEL_SERVICE_NAME: 'otel_service', DD_TRACE_LOG_LEVEL: 'error', @@ -195,9 +195,9 @@ describe('opentelemetry', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_TRACE_OTEL_ENABLED: 1, - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - TIMEOUT: 1500, + DD_TRACE_OTEL_ENABLED: '1', + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + TIMEOUT: '1500', OTEL_SERVICE_NAME: 'otel_service', OTEL_LOG_LEVEL: 'foo', OTEL_TRACES_SAMPLER: 'foo', @@ -293,9 +293,9 @@ describe('opentelemetry', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_TRACE_OTEL_ENABLED: 1, - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - TIMEOUT: 1500 + DD_TRACE_OTEL_ENABLED: '1', + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + TIMEOUT: '1500' } }) @@ -335,10 +335,10 @@ describe('opentelemetry', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_TRACE_OTEL_ENABLED: 1, + DD_TRACE_OTEL_ENABLED: '1', SERVER_PORT, DD_TRACE_DISABLED_INSTRUMENTATIONS: 'http,dns,express,net', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1' } }) await new Promise(resolve => setTimeout(resolve, 1000)) // Adjust the delay as necessary @@ -408,7 +408,7 @@ describe('opentelemetry', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_TRACE_OTEL_ENABLED: 1, + DD_TRACE_OTEL_ENABLED: '1', SERVER_PORT, DD_TRACE_DISABLED_INSTRUMENTATIONS: 'http,dns,express,net' } diff --git a/integration-tests/opentelemetry/auto-instrumentation.js b/integration-tests/opentelemetry/auto-instrumentation.js index 8628fc43be7..d1eb2844c32 100644 --- a/integration-tests/opentelemetry/auto-instrumentation.js +++ b/integration-tests/opentelemetry/auto-instrumentation.js @@ -5,6 +5,7 @@ const { TracerProvider } = tracer const provider = new TracerProvider() provider.register() +const http = require('http') const { registerInstrumentations } = require('@opentelemetry/instrumentation') const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http') const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express') @@ -29,7 +30,6 @@ registerInstrumentations({ }) const express = require('express') -const http = require('http') const app = express() const PORT = process.env.SERVER_PORT diff --git a/integration-tests/opentelemetry/server.js b/integration-tests/opentelemetry/server.js index 657bc45bb5f..80f629008ee 100644 --- a/integration-tests/opentelemetry/server.js +++ b/integration-tests/opentelemetry/server.js @@ -3,18 +3,17 @@ const tracer = require('dd-trace').init() const { TracerProvider } = tracer - const provider = new TracerProvider() provider.register() +const http = require('http') + const ot = require('@opentelemetry/api') const otelTracer = ot.trace.getTracer( 'my-service-tracer' ) -const http = require('http') - const server = http.createServer() server.listen(() => { diff --git a/integration-tests/package-guardrails.spec.js b/integration-tests/package-guardrails.spec.js index da01718ad9c..5e83929d0dc 100644 --- a/integration-tests/package-guardrails.spec.js +++ b/integration-tests/package-guardrails.spec.js @@ -1,14 +1,14 @@ 'use strict' +const path = require('path') +const fs = require('fs') +const assert = require('assert') const { runAndCheckWithTelemetry: testFile, useEnv, useSandbox, sandboxCwd } = require('./helpers') -const path = require('path') -const fs = require('fs') -const assert = require('assert') const NODE_OPTIONS = '--require dd-trace/init.js' const DD_TRACE_DEBUG = 'true' diff --git a/integration-tests/pino.spec.js b/integration-tests/pino.spec.js index 8716377c9cf..9d8a74d2bf9 100644 --- a/integration-tests/pino.spec.js +++ b/integration-tests/pino.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { FakeAgent, spawnProc, sandboxCwd, useSandbox, curl, assertObjectContains } = require('./helpers') const path = require('path') const { once } = require('events') +const { FakeAgent, spawnProc, sandboxCwd, useSandbox, curl, assertObjectContains } = require('./helpers') describe('pino test', () => { let agent diff --git a/integration-tests/pino/index.js b/integration-tests/pino/index.js index ba303a62336..6cdb968e95b 100644 --- a/integration-tests/pino/index.js +++ b/integration-tests/pino/index.js @@ -11,8 +11,8 @@ if (process.env.lOG_INJECTION) { } const tracer = require('dd-trace').init(options) - const http = require('http') + const logger = require('pino')() const server = http diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index f49b6a34185..c2425940be5 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -1,12 +1,11 @@ 'use strict' +const assert = require('node:assert') const { once } = require('node:events') const { exec, execSync } = require('child_process') -const satisfies = require('semifies') const path = require('path') const fs = require('fs') - -const { assert } = require('chai') +const satisfies = require('semifies') const { sandboxCwd, @@ -145,7 +144,7 @@ versions.forEach((version) => { metadataDicts.forEach(metadata => { for (const testLevel of TEST_LEVEL_EVENT_TYPES) { - assert.equal(metadata[testLevel][TEST_SESSION_NAME], 'my-test-session') + assert.strictEqual(metadata[testLevel][TEST_SESSION_NAME], 'my-test-session') } }) @@ -159,14 +158,14 @@ versions.forEach((version) => { const stepEvents = events.filter(event => event.type === 'span') assert.ok(testSessionEvent.content.resource.includes('test_session.playwright test')) - assert.equal(testSessionEvent.content.meta[TEST_STATUS], 'fail') + assert.strictEqual(testSessionEvent.content.meta[TEST_STATUS], 'fail') assert.ok(testModuleEvent.content.resource.includes('test_module.playwright test')) - assert.equal(testModuleEvent.content.meta[TEST_STATUS], 'fail') - assert.equal(testSessionEvent.content.meta[TEST_TYPE], 'browser') - assert.equal(testModuleEvent.content.meta[TEST_TYPE], 'browser') + assert.strictEqual(testModuleEvent.content.meta[TEST_STATUS], 'fail') + assert.strictEqual(testSessionEvent.content.meta[TEST_TYPE], 'browser') + assert.strictEqual(testModuleEvent.content.meta[TEST_TYPE], 'browser') - assert.exists(testSessionEvent.content.meta[ERROR_MESSAGE]) - assert.exists(testModuleEvent.content.meta[ERROR_MESSAGE]) + assert.strictEqual(typeof testSessionEvent.content.meta[ERROR_MESSAGE], 'string') + assert.strictEqual(typeof testModuleEvent.content.meta[ERROR_MESSAGE], 'string') assert.deepStrictEqual(testSuiteEvents.map(suite => suite.content.resource).sort(), [ 'test_suite.landing-page-test.js', @@ -182,11 +181,11 @@ versions.forEach((version) => { testSuiteEvents.forEach(testSuiteEvent => { if (testSuiteEvent.content.meta[TEST_STATUS] === 'fail') { - assert.exists(testSuiteEvent.content.meta[ERROR_MESSAGE]) + assert.ok(testSuiteEvent.content.meta[ERROR_MESSAGE]) } assert.ok(testSuiteEvent.content.meta[TEST_SOURCE_FILE].endsWith('-test.js')) - assert.equal(testSuiteEvent.content.metrics[TEST_SOURCE_START], 1) - assert.exists(testSuiteEvent.content.metrics[DD_HOST_CPU_COUNT]) + assert.strictEqual(testSuiteEvent.content.metrics[TEST_SOURCE_START], 1) + assert.ok(testSuiteEvent.content.metrics[DD_HOST_CPU_COUNT]) }) assert.deepStrictEqual(testEvents.map(test => test.content.resource).sort(), [ @@ -210,11 +209,12 @@ versions.forEach((version) => { ]) testEvents.forEach(testEvent => { - assert.exists(testEvent.content.metrics[TEST_SOURCE_START]) - assert.equal( - testEvent.content.meta[TEST_SOURCE_FILE].startsWith('ci-visibility/playwright-tests/'), true + assert.ok(testEvent.content.metrics[TEST_SOURCE_START]) + assert.strictEqual( + testEvent.content.meta[TEST_SOURCE_FILE].startsWith('ci-visibility/playwright-tests/'), + true ) - assert.equal(testEvent.content.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'false') + assert.strictEqual(testEvent.content.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'false') // Can read DD_TAGS assertObjectContains(testEvent.content.meta, { 'test.customtag': 'customvalue', @@ -223,7 +223,7 @@ versions.forEach((version) => { [TEST_BROWSER_NAME]: 'chromium', [TEST_PARAMETERS]: JSON.stringify({ arguments: { browser: 'chromium' }, metadata: {} }) }) - assert.exists(testEvent.content.metrics[DD_HOST_CPU_COUNT]) + assert.ok(testEvent.content.metrics[DD_HOST_CPU_COUNT]) if (version === 'latest' || satisfies(version, '>=1.38.0')) { if (testEvent.content.meta[TEST_STATUS] !== 'skip' && testEvent.content.meta[TEST_SUITE].includes('landing-page-test.js')) { @@ -241,7 +241,7 @@ versions.forEach((version) => { }) stepEvents.forEach(stepEvent => { - assert.equal(stepEvent.content.name, 'playwright.step') + assert.strictEqual(stepEvent.content.name, 'playwright.step') assert.ok(Object.hasOwn(stepEvent.content.meta, 'playwright.step')) }) const annotatedTest = testEvents.find(test => @@ -291,7 +291,7 @@ versions.forEach((version) => { ]) assert.match(testOutput, /1 passed/) assert.match(testOutput, /1 skipped/) - assert.notInclude(testOutput, 'TypeError') + assert.doesNotMatch(testOutput, /TypeError/) }, 25000).then(() => done()).catch(done) childProcess = exec( @@ -328,7 +328,7 @@ versions.forEach((version) => { assertObjectContains(testSessionEvent.meta, { [TEST_STATUS]: 'fail' }) - assert.exists(testSuiteEvent.meta[ERROR_MESSAGE]) + assert.ok(testSuiteEvent.meta[ERROR_MESSAGE]) assert.match(testSessionEvent.meta[ERROR_MESSAGE], /Test suites failed: 1/) }).then(() => done()).catch(done) @@ -340,7 +340,7 @@ versions.forEach((version) => { ...getCiVisAgentlessConfig(receiver.port), PW_BASE_URL: `http://localhost:${webAppPort}`, TEST_DIR: './ci-visibility/playwright-tests-error', - TEST_TIMEOUT: 3000 + TEST_TIMEOUT: '3000' }, stdio: 'pipe' } @@ -399,7 +399,7 @@ versions.forEach((version) => { [TEST_IS_NEW]: 'true' }) }) - assert.equal( + assert.strictEqual( newPassingTests.length, NUM_RETRIES_EFD + 1, 'passing test has not been retried the correct number of times' @@ -412,7 +412,7 @@ versions.forEach((version) => { [TEST_IS_NEW]: 'true' }) }) - assert.equal( + assert.strictEqual( newAnnotatedTests.length, NUM_RETRIES_EFD + 1, 'annotated test has not been retried the correct number of times' @@ -420,7 +420,7 @@ versions.forEach((version) => { // The only new tests are the passing and annotated tests const totalNewTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') - assert.equal( + assert.strictEqual( totalNewTests.length, newPassingTests.length + newAnnotatedTests.length, 'total new tests is not the sum of the passing and annotated tests' @@ -428,12 +428,12 @@ versions.forEach((version) => { // The only retried tests are the passing and annotated tests const totalRetriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal( + assert.strictEqual( totalRetriedTests.length, newPassingTests.length - 1 + newAnnotatedTests.length - 1, 'total retried tests is not the sum of the passing and annotated tests' ) - assert.equal( + assert.strictEqual( totalRetriedTests.length, NUM_RETRIES_EFD * 2, 'total retried tests is not the correct number of times' @@ -446,7 +446,7 @@ versions.forEach((version) => { }) // all but one has been retried - assert.equal(totalRetriedTests.length, totalNewTests.length - 2) + assert.strictEqual(totalRetriedTests.length, totalNewTests.length - 2) }) childProcess = exec( @@ -515,7 +515,7 @@ versions.forEach((version) => { }) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -579,7 +579,7 @@ versions.forEach((version) => { test.resource.endsWith('should work with fixme') ) // no retries - assert.equal(newTests.length, 2) + assert.strictEqual(newTests.length, 2) newTests.forEach(test => { assertObjectContains(test.meta, { [TEST_IS_NEW]: 'true' @@ -588,7 +588,7 @@ versions.forEach((version) => { const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -629,15 +629,15 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, 7) + assert.strictEqual(tests.length, 7) const testSession = events.find(event => event.type === 'test_session_end').content assert.ok(!(TEST_EARLY_FLAKE_ENABLED in testSession.meta)) const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') - assert.equal(newTests.length, 0) + assert.strictEqual(newTests.length, 0) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -707,7 +707,7 @@ versions.forEach((version) => { }) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -763,7 +763,7 @@ versions.forEach((version) => { }) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -824,9 +824,9 @@ versions.forEach((version) => { }) const newTests = tests.filter(test => test.meta[TEST_IS_NEW] === 'true') - assert.equal(newTests.length, 0) + assert.strictEqual(newTests.length, 0) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) ]) }) @@ -868,39 +868,39 @@ versions.forEach((version) => { .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.propertyVal(testSession.meta, TEST_EARLY_FLAKE_ENABLED, 'true') + assert.strictEqual(testSession.meta[TEST_EARLY_FLAKE_ENABLED], 'true') const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter( test => test.meta[TEST_NAME] === 'playwright should not retry new tests' ) - assert.equal(newTests.length, NUM_RETRIES_EFD + 1) + assert.strictEqual(newTests.length, NUM_RETRIES_EFD + 1) newTests.forEach(test => { // tests always fail because ATR and --retries are disabled for EFD, // so testInfo.retry is always 0 - assert.propertyVal(test.meta, TEST_STATUS, 'fail') - assert.propertyVal(test.meta, TEST_IS_NEW, 'true') + assert.strictEqual(test.meta[TEST_STATUS], 'fail') + assert.strictEqual(test.meta[TEST_IS_NEW], 'true') }) const retriedNewTests = newTests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedNewTests.length, NUM_RETRIES_EFD) + assert.strictEqual(retriedNewTests.length, NUM_RETRIES_EFD) retriedNewTests.forEach(test => { - assert.propertyVal(test.meta, TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd) - assert.propertyVal(test.meta, TEST_STATUS, 'fail') + assert.strictEqual(test.meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.efd) + assert.strictEqual(test.meta[TEST_STATUS], 'fail') }) // --retries works normally for old flaky tests const oldFlakyTests = tests.filter( test => test.meta[TEST_NAME] === 'playwright should retry old flaky tests' ) - assert.equal(oldFlakyTests.length, 2) + assert.strictEqual(oldFlakyTests.length, 2) const passedFlakyTests = oldFlakyTests.filter(test => test.meta[TEST_STATUS] === 'pass') - assert.equal(passedFlakyTests.length, 1) - assert.propertyVal(passedFlakyTests[0].meta, TEST_IS_RETRY, 'true') - assert.propertyVal(passedFlakyTests[0].meta, TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext) + assert.strictEqual(passedFlakyTests.length, 1) + assert.strictEqual(passedFlakyTests[0].meta[TEST_IS_RETRY], 'true') + assert.strictEqual(passedFlakyTests[0].meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.ext) const failedFlakyTests = oldFlakyTests.filter(test => test.meta[TEST_STATUS] === 'fail') - assert.equal(failedFlakyTests.length, 1) + assert.strictEqual(failedFlakyTests.length, 1) }) ]) }) @@ -943,37 +943,37 @@ versions.forEach((version) => { .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.propertyVal(testSession.meta, TEST_EARLY_FLAKE_ENABLED, 'true') + assert.strictEqual(testSession.meta[TEST_EARLY_FLAKE_ENABLED], 'true') const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter( test => test.meta[TEST_NAME] === 'playwright should not retry new tests' ) - assert.equal(newTests.length, NUM_RETRIES_EFD + 1) + assert.strictEqual(newTests.length, NUM_RETRIES_EFD + 1) newTests.forEach(test => { - assert.propertyVal(test.meta, TEST_STATUS, 'fail') - assert.propertyVal(test.meta, TEST_IS_NEW, 'true') + assert.strictEqual(test.meta[TEST_STATUS], 'fail') + assert.strictEqual(test.meta[TEST_IS_NEW], 'true') }) const retriedNewTests = newTests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedNewTests.length, NUM_RETRIES_EFD) + assert.strictEqual(retriedNewTests.length, NUM_RETRIES_EFD) retriedNewTests.forEach(test => { - assert.propertyVal(test.meta, TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd) - assert.propertyVal(test.meta, TEST_STATUS, 'fail') + assert.strictEqual(test.meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.efd) + assert.strictEqual(test.meta[TEST_STATUS], 'fail') }) // ATR works normally for old flaky tests const oldFlakyTests = tests.filter( test => test.meta[TEST_NAME] === 'playwright should retry old flaky tests' ) - assert.equal(oldFlakyTests.length, 2) + assert.strictEqual(oldFlakyTests.length, 2) const passedFlakyTests = oldFlakyTests.filter(test => test.meta[TEST_STATUS] === 'pass') - assert.equal(passedFlakyTests.length, 1) - assert.propertyVal(passedFlakyTests[0].meta, TEST_IS_RETRY, 'true') - assert.propertyVal(passedFlakyTests[0].meta, TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr) + assert.strictEqual(passedFlakyTests.length, 1) + assert.strictEqual(passedFlakyTests[0].meta[TEST_IS_RETRY], 'true') + assert.strictEqual(passedFlakyTests[0].meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.atr) const failedFlakyTests = oldFlakyTests.filter(test => test.meta[TEST_STATUS] === 'fail') - assert.equal(failedFlakyTests.length, 1) + assert.strictEqual(failedFlakyTests.length, 1) }) ]) }) @@ -1023,20 +1023,20 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, 3) + assert.strictEqual(tests.length, 3) const failedTests = tests.filter(test => test.meta[TEST_STATUS] === 'fail') - assert.equal(failedTests.length, 2) + assert.strictEqual(failedTests.length, 2) const failedRetryTests = failedTests.filter( test => test.meta[TEST_RETRY_REASON] === TEST_RETRY_REASON_TYPES.atr ) - assert.equal(failedRetryTests.length, 1) // the first one is not a retry + assert.strictEqual(failedRetryTests.length, 1) // the first one is not a retry const passedTests = tests.filter(test => test.meta[TEST_STATUS] === 'pass') - assert.equal(passedTests.length, 1) - assert.equal(passedTests[0].meta[TEST_IS_RETRY], 'true') - assert.equal(passedTests[0].meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.atr) + assert.strictEqual(passedTests.length, 1) + assert.strictEqual(passedTests[0].meta[TEST_IS_RETRY], 'true') + assert.strictEqual(passedTests[0].meta[TEST_RETRY_REASON], TEST_RETRY_REASON_TYPES.atr) }, 30000) childProcess = exec( @@ -1075,8 +1075,8 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, 1) - assert.equal(tests.filter( + assert.strictEqual(tests.length, 1) + assert.strictEqual(tests.filter( (test) => test.meta[TEST_RETRY_REASON] === TEST_RETRY_REASON_TYPES.atr ).length, 0) }, 30000) @@ -1118,15 +1118,15 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, 2) + assert.strictEqual(tests.length, 2) const failedTests = tests.filter(test => test.meta[TEST_STATUS] === 'fail') - assert.equal(failedTests.length, 2) + assert.strictEqual(failedTests.length, 2) const failedRetryTests = failedTests.filter( test => test.meta[TEST_RETRY_REASON] === TEST_RETRY_REASON_TYPES.atr ) - assert.equal(failedRetryTests.length, 1) + assert.strictEqual(failedRetryTests.length, 1) }, 30000) childProcess = exec( @@ -1159,9 +1159,9 @@ versions.forEach((version) => { const test = events.find(event => event.type === 'test').content const testSuite = events.find(event => event.type === 'test_suite_end').content // The test is in a subproject - assert.notEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) - assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) - assert.equal(testSuite.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + assert.notStrictEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) + assert.strictEqual(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + assert.strictEqual(testSuite.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) }) childProcess = exec( @@ -1231,7 +1231,7 @@ versions.forEach((version) => { }) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -1259,7 +1259,7 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) tests.forEach(test => { - assert.equal(test.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'true') + assert.strictEqual(test.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'true') }) }) @@ -1334,7 +1334,7 @@ versions.forEach((version) => { ) if (isDisabled) { - assert.equal(attemptedToFixTests.length, 2) + assert.strictEqual(attemptedToFixTests.length, 2) assert.ok(attemptedToFixTests.every(test => test.meta[TEST_MANAGEMENT_IS_DISABLED] === 'true' )) @@ -1343,9 +1343,9 @@ versions.forEach((version) => { } if (isAttemptingToFix) { - assert.equal(attemptedToFixTests.length, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1)) + assert.strictEqual(attemptedToFixTests.length, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1)) } else { - assert.equal(attemptedToFixTests.length, 2) + assert.strictEqual(attemptedToFixTests.length, 2) } if (isQuarantined) { @@ -1353,7 +1353,7 @@ versions.forEach((version) => { test.meta[TEST_MANAGEMENT_IS_QUARANTINED] === 'true' ).length // quarantined tests still run and are retried - assert.equal(numQuarantinedTests, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1)) + assert.strictEqual(numQuarantinedTests, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1)) } // Retried tests are in randomly order, so we just count number of tests @@ -1381,39 +1381,39 @@ versions.forEach((version) => { // One of the tests is passing always if (isAttemptingToFix) { - assert.equal(countAttemptToFixTests, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1)) - assert.equal(countRetriedAttemptToFixTests, 2 * ATTEMPT_TO_FIX_NUM_RETRIES) + assert.strictEqual(countAttemptToFixTests, 2 * (ATTEMPT_TO_FIX_NUM_RETRIES + 1)) + assert.strictEqual(countRetriedAttemptToFixTests, 2 * ATTEMPT_TO_FIX_NUM_RETRIES) if (shouldAlwaysPass) { - assert.equal(testsMarkedAsFailedAllRetries, 0) - assert.equal(testsMarkedAsFailed, 0) - assert.equal(testsMarkedAsPassedAllRetries, 2) + assert.strictEqual(testsMarkedAsFailedAllRetries, 0) + assert.strictEqual(testsMarkedAsFailed, 0) + assert.strictEqual(testsMarkedAsPassedAllRetries, 2) } else if (shouldFailSometimes) { // one test failed sometimes, the other always passed - assert.equal(testsMarkedAsFailedAllRetries, 0) - assert.equal(testsMarkedAsFailed, 1) - assert.equal(testsMarkedAsPassedAllRetries, 1) + assert.strictEqual(testsMarkedAsFailedAllRetries, 0) + assert.strictEqual(testsMarkedAsFailed, 1) + assert.strictEqual(testsMarkedAsPassedAllRetries, 1) } else { // one test failed always, the other always passed - assert.equal(testsMarkedAsFailedAllRetries, 1) - assert.equal(testsMarkedAsFailed, 1) - assert.equal(testsMarkedAsPassedAllRetries, 1) + assert.strictEqual(testsMarkedAsFailedAllRetries, 1) + assert.strictEqual(testsMarkedAsFailed, 1) + assert.strictEqual(testsMarkedAsPassedAllRetries, 1) } } else { - assert.equal(countAttemptToFixTests, 0) - assert.equal(countRetriedAttemptToFixTests, 0) - assert.equal(testsMarkedAsFailedAllRetries, 0) - assert.equal(testsMarkedAsPassedAllRetries, 0) + assert.strictEqual(countAttemptToFixTests, 0) + assert.strictEqual(countRetriedAttemptToFixTests, 0) + assert.strictEqual(testsMarkedAsFailedAllRetries, 0) + assert.strictEqual(testsMarkedAsPassedAllRetries, 0) } if (shouldIncludeFlakyTest) { const flakyTests = tests.filter( test => test.meta[TEST_NAME] === 'flaky test is retried without attempt to fix' ) // it passes at the second attempt - assert.equal(flakyTests.length, 2) + assert.strictEqual(flakyTests.length, 2) const passedFlakyTest = flakyTests.filter(test => test.meta[TEST_STATUS] === 'pass') const failedFlakyTest = flakyTests.filter(test => test.meta[TEST_STATUS] === 'fail') - assert.equal(passedFlakyTest.length, 1) - assert.equal(failedFlakyTest.length, 1) + assert.strictEqual(passedFlakyTest.length, 1) + assert.strictEqual(failedFlakyTest.length, 1) } }, 30000) @@ -1460,9 +1460,9 @@ versions.forEach((version) => { if (isQuarantined || isDisabled || shouldAlwaysPass) { // even though a test fails, the exit code is 0 because the test is quarantined - assert.equal(exitCode, 0) + assert.strictEqual(exitCode, 0) } else { - assert.equal(exitCode, 1) + assert.strictEqual(exitCode, 1) } } @@ -1652,19 +1652,19 @@ versions.forEach((version) => { } const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, 8) + assert.strictEqual(tests.length, 8) const disabledTests = tests.filter(test => test.meta[TEST_NAME] === 'disable should disable test') - assert.equal(disabledTests.length, 2) + assert.strictEqual(disabledTests.length, 2) disabledTests.forEach(test => { if (isDisabling) { - assert.equal(test.meta[TEST_STATUS], 'skip') + assert.strictEqual(test.meta[TEST_STATUS], 'skip') assertObjectContains(test.meta, { [TEST_MANAGEMENT_IS_DISABLED]: 'true' }) } else { - assert.equal(test.meta[TEST_STATUS], 'fail') + assert.strictEqual(test.meta[TEST_STATUS], 'fail') assert.ok(!(TEST_MANAGEMENT_IS_DISABLED in test.meta)) } }) @@ -1703,11 +1703,11 @@ versions.forEach((version) => { // the testOutput checks whether the test is actually skipped if (isDisabling) { - assert.notMatch(testOutput, /SHOULD NOT BE EXECUTED/) - assert.equal(exitCode, 0) + assert.doesNotMatch(testOutput, /SHOULD NOT BE EXECUTED/) + assert.strictEqual(exitCode, 0) } else { assert.match(testOutput, /SHOULD NOT BE EXECUTED/) - assert.equal(exitCode, 1) + assert.strictEqual(exitCode, 1) } } @@ -1770,27 +1770,27 @@ versions.forEach((version) => { ) quarantinedTests.forEach(test => { - assert.equal(test.meta[TEST_STATUS], 'fail') + assert.strictEqual(test.meta[TEST_STATUS], 'fail') }) if (hasFlakyTests) { - assert.equal(flakyTests.length, 2) // first attempt fails, second attempt passes - assert.equal(quarantinedTests.length, 2) // both fail + assert.strictEqual(flakyTests.length, 2) // first attempt fails, second attempt passes + assert.strictEqual(quarantinedTests.length, 2) // both fail assert.ok(!(TEST_MANAGEMENT_IS_QUARANTINED in flakyTests[0].meta)) assert.ok(!(TEST_MANAGEMENT_IS_QUARANTINED in flakyTests[1].meta)) const failedFlakyTest = flakyTests.filter(test => test.meta[TEST_STATUS] === 'fail') const passedFlakyTest = flakyTests.filter(test => test.meta[TEST_STATUS] === 'pass') - assert.equal(failedFlakyTest.length, 1) - assert.equal(passedFlakyTest.length, 1) + assert.strictEqual(failedFlakyTest.length, 1) + assert.strictEqual(passedFlakyTest.length, 1) } if (isQuarantining) { if (hasFlakyTests) { - assert.equal(quarantinedTests[1].meta[TEST_MANAGEMENT_IS_QUARANTINED], 'true') + assert.strictEqual(quarantinedTests[1].meta[TEST_MANAGEMENT_IS_QUARANTINED], 'true') } else { - assert.equal(quarantinedTests.length, 1) + assert.strictEqual(quarantinedTests.length, 1) } - assert.equal(quarantinedTests[0].meta[TEST_MANAGEMENT_IS_QUARANTINED], 'true') + assert.strictEqual(quarantinedTests[0].meta[TEST_MANAGEMENT_IS_QUARANTINED], 'true') assertObjectContains(testSession.meta, { [TEST_MANAGEMENT_ENABLED]: 'true' }) @@ -1798,7 +1798,7 @@ versions.forEach((version) => { if (hasFlakyTests) { assert.ok(!(TEST_MANAGEMENT_IS_QUARANTINED in quarantinedTests[1].meta)) } else { - assert.equal(quarantinedTests.length, 1) + assert.strictEqual(quarantinedTests.length, 1) } assert.ok(!(TEST_MANAGEMENT_IS_QUARANTINED in quarantinedTests[0].meta)) assert.ok(!(TEST_MANAGEMENT_ENABLED in testSession.meta)) @@ -1833,9 +1833,9 @@ versions.forEach((version) => { ]) if (isQuarantining) { - assert.equal(exitCode, 0) + assert.strictEqual(exitCode, 0) } else { - assert.equal(exitCode, 1) + assert.strictEqual(exitCode, 1) } } @@ -1897,9 +1897,9 @@ versions.forEach((version) => { assert.ok(!(TEST_MANAGEMENT_ENABLED in testSession.meta)) const tests = events.filter(event => event.type === 'test').map(event => event.content) // they are not retried - assert.equal(tests.length, 2) + assert.strictEqual(tests.length, 2) const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') - assert.equal(retriedTests.length, 0) + assert.strictEqual(retriedTests.length, 0) }) childProcess = exec( @@ -1941,25 +1941,25 @@ versions.forEach((version) => { assert.ok(metadataDicts.length > 0) metadataDicts.forEach(metadata => { - assert.equal(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], undefined) - assert.equal(metadata.test[DD_CAPABILITIES_AUTO_TEST_RETRIES], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_IMPACT_ANALYSIS], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_AUTO_TEST_RETRIES], '1') if (satisfies(version, '>=1.38.0') || version === 'latest') { - assert.equal(metadata.test[DD_CAPABILITIES_EARLY_FLAKE_DETECTION], '1') - assert.equal(metadata.test[DD_CAPABILITIES_IMPACTED_TESTS], '1') - assert.equal(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE], '1') - assert.equal(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE], '1') - assert.equal(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], '5') - assert.equal(metadata.test[DD_CAPABILITIES_FAILED_TEST_REPLAY], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_EARLY_FLAKE_DETECTION], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_IMPACTED_TESTS], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE], '1') + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], '5') + assert.strictEqual(metadata.test[DD_CAPABILITIES_FAILED_TEST_REPLAY], '1') } else { - assert.equal(metadata.test[DD_CAPABILITIES_EARLY_FLAKE_DETECTION], undefined) - assert.equal(metadata.test[DD_CAPABILITIES_IMPACTED_TESTS], undefined) - assert.equal(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE], undefined) - assert.equal(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE], undefined) - assert.equal(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], undefined) - assert.equal(metadata.test[DD_CAPABILITIES_FAILED_TEST_REPLAY], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_EARLY_FLAKE_DETECTION], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_IMPACTED_TESTS], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_QUARANTINE], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_DISABLE], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_TEST_MANAGEMENT_ATTEMPT_TO_FIX], undefined) + assert.strictEqual(metadata.test[DD_CAPABILITIES_FAILED_TEST_REPLAY], undefined) } // capabilities logic does not overwrite test session name - assert.equal(metadata.test[TEST_SESSION_NAME], 'my-test-session-name') + assert.strictEqual(metadata.test[TEST_SESSION_NAME], 'my-test-session-name') }) }) @@ -1993,7 +1993,7 @@ versions.forEach((version) => { const test = events.find(event => event.type === 'test').content - assert.equal(test.meta['test.custom_tag'], 'this is custom') + assert.strictEqual(test.meta['test.custom_tag'], 'this is custom') }) childProcess = exec( @@ -2024,12 +2024,12 @@ versions.forEach((version) => { const customSpan = spans.find(span => span.name === 'my custom span') - assert.exists(customSpan) - assert.equal(customSpan.meta['test.really_custom_tag'], 'this is really custom') + assert.ok(customSpan) + assert.strictEqual(customSpan.meta['test.really_custom_tag'], 'this is really custom') // custom span is children of active test span - assert.equal(customSpan.trace_id.toString(), test.trace_id.toString()) - assert.equal(customSpan.parent_id.toString(), test.span_id.toString()) + assert.strictEqual(customSpan.trace_id.toString(), test.trace_id.toString()) + assert.strictEqual(customSpan.parent_id.toString(), test.span_id.toString()) }) childProcess = exec( @@ -2113,7 +2113,7 @@ versions.forEach((version) => { .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.equal(testSession.meta[TEST_STATUS], 'fail') + assert.strictEqual(testSession.meta[TEST_STATUS], 'fail') }) receiver.setSettings({ test_management: { enabled: true } }) @@ -2132,7 +2132,7 @@ versions.forEach((version) => { ) childProcess.on('exit', (exitCode) => { - assert.equal(exitCode, 1) + assert.strictEqual(exitCode, 1) receiverPromise.then(() => done()).catch(done) }) }) @@ -2211,9 +2211,9 @@ versions.forEach((version) => { test.meta[TEST_SOURCE_FILE] === 'ci-visibility/playwright-tests-impacted-tests/impacted-test.js') if (isEfd) { - assert.equal(impactedTests.length, (NUM_RETRIES_EFD + 1) * 2) // Retries + original test + assert.strictEqual(impactedTests.length, (NUM_RETRIES_EFD + 1) * 2) // Retries + original test } else { - assert.equal(impactedTests.length, 2) + assert.strictEqual(impactedTests.length, 2) } for (const impactedTest of impactedTests) { @@ -2237,7 +2237,7 @@ versions.forEach((version) => { const retriedTests = tests.filter( test => test.meta[TEST_IS_RETRY] === 'true' ) - assert.equal(retriedTests.length, NUM_RETRIES_EFD * 2) + assert.strictEqual(retriedTests.length, NUM_RETRIES_EFD * 2) let retriedTestNew = 0 let retriedTestsWithReason = 0 retriedTests.forEach(test => { @@ -2248,8 +2248,8 @@ versions.forEach((version) => { retriedTestsWithReason++ } }) - assert.equal(retriedTestNew, isNew ? NUM_RETRIES_EFD * 2 : 0) - assert.equal(retriedTestsWithReason, NUM_RETRIES_EFD * 2) + assert.strictEqual(retriedTestNew, isNew ? NUM_RETRIES_EFD * 2 : 0) + assert.strictEqual(retriedTestsWithReason, NUM_RETRIES_EFD * 2) } }, 25000) @@ -2333,7 +2333,7 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, NUM_RETRIES_EFD + 1) + assert.strictEqual(tests.length, NUM_RETRIES_EFD + 1) for (const test of tests) { assert.ok(!(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED in test.meta)) assert.ok(!(TEST_HAS_FAILED_ALL_RETRIES in test.meta)) @@ -2390,9 +2390,9 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const testSuites = events.filter(event => event.type === 'test_suite_end').map(event => event.content) - assert.equal(testSuites.length, 2) + assert.strictEqual(testSuites.length, 2) const tests = events.filter(event => event.type === 'test').map(event => event.content) - assert.equal(tests.length, 3) + assert.strictEqual(tests.length, 3) const skippedTest = tests.find(test => test.meta[TEST_STATUS] === 'skip') assertObjectContains( @@ -2422,8 +2422,8 @@ versions.forEach((version) => { ...getCiVisAgentlessConfig(receiver.port), PW_BASE_URL: `http://localhost:${webAppPort}`, TEST_DIR: './ci-visibility/playwright-test-duration', - FULLY_PARALLEL: parallelism, - PLAYWRIGHT_WORKERS: 2 + FULLY_PARALLEL: String(parallelism), + PLAYWRIGHT_WORKERS: '2' }, stdio: 'pipe' } @@ -2443,7 +2443,7 @@ versions.forEach((version) => { .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) - assert.equal(tests.length, 2) + assert.strictEqual(tests.length, 2) const failedTest = tests.find(test => test.meta[TEST_STATUS] === 'fail') assertObjectContains(failedTest.meta, { [TEST_NAME]: 'failing test fails and causes early bail' diff --git a/integration-tests/profiler/fstest.js b/integration-tests/profiler/fstest.js index ef49f715bdb..fc28b95c205 100644 --- a/integration-tests/profiler/fstest.js +++ b/integration-tests/profiler/fstest.js @@ -1,10 +1,10 @@ 'use strict' +const tracer = require('dd-trace').init() const fs = require('fs') const os = require('os') const path = require('path') -const tracer = require('dd-trace').init() tracer.profilerStarted().then(() => { tracer.trace('x', (_, done) => { setImmediate(() => { diff --git a/integration-tests/profiler/oom.js b/integration-tests/profiler/oom.js index 40fb39ac56a..5851872d88f 100644 --- a/integration-tests/profiler/oom.js +++ b/integration-tests/profiler/oom.js @@ -3,7 +3,6 @@ /* eslint-disable no-console */ const tracer = require('dd-trace').init() - const { Worker, isMainThread, threadId } = require('worker_threads') const nworkers = Number(process.argv[2] || 0) diff --git a/integration-tests/profiler/profiler.spec.js b/integration-tests/profiler/profiler.spec.js index 99844d65c10..10e57ea1555 100644 --- a/integration-tests/profiler/profiler.spec.js +++ b/integration-tests/profiler/profiler.spec.js @@ -1,21 +1,22 @@ 'use strict' -const { - FakeAgent, - sandboxCwd, - useSandbox, - assertObjectContains, -} = require('../helpers') +const assert = require('node:assert/strict') const childProcess = require('child_process') const { fork } = childProcess const path = require('path') -const { assert } = require('chai') + const fs = require('fs/promises') const fsync = require('fs') const net = require('net') const zlib = require('zlib') -const { Profile } = require('../../vendor/dist/pprof-format') const satisfies = require('semifies') +const { Profile } = require('../../vendor/dist/pprof-format') +const { + FakeAgent, + sandboxCwd, + useSandbox, + assertObjectContains, +} = require('../helpers') const DEFAULT_PROFILE_TYPES = ['wall', 'space'] if (process.platform !== 'win32') { @@ -23,6 +24,7 @@ if (process.platform !== 'win32') { } const TIMEOUT = 30000 +const isAtLeast24 = satisfies(process.versions.node, '>=24.0.0') function checkProfiles (agent, proc, timeout, expectedProfileTypes = DEFAULT_PROFILE_TYPES, expectBadExit = false, expectSeq = true @@ -55,7 +57,7 @@ function expectProfileMessagePromise (agent, timeout, const attachments = event.attachments assert.ok(Array.isArray(attachments)) // Profiler encodes the files with Promise.all, so their ordering is not guaranteed - assert.sameMembers(attachments, fileNames) + assert.deepStrictEqual(attachments.slice().sort(), fileNames.sort()) for (const [index, fileName] of attachments.entries()) { assertObjectContains(files[index + 1], { originalname: fileName @@ -94,9 +96,9 @@ function processExitPromise (proc, timeout, expectBadExit = false) { } async function getLatestProfile (cwd, pattern) { - const pprofGzipped = await readLatestFile(cwd, pattern) - const pprofUnzipped = zlib.gunzipSync(pprofGzipped) - return { profile: Profile.decode(pprofUnzipped), encoded: pprofGzipped.toString('base64') } + const pprofCompressed = await readLatestFile(cwd, pattern) + const pprofUncompressed = zlib[isAtLeast24 ? 'zstdDecompressSync' : 'gunzipSync'](pprofCompressed) + return { profile: Profile.decode(pprofUncompressed), encoded: pprofCompressed.toString('base64') } } async function readLatestFile (cwd, pattern) { @@ -230,8 +232,8 @@ async function gatherTimelineEvents (cwd, scriptFilePath, agentPort, eventType, cwd, env: { DD_PROFILING_EXPORTERS: 'file', - DD_PROFILING_ENABLED: 1, - DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED: 0, // capture all events + DD_PROFILING_ENABLED: '1', + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED: '0', // capture all events DD_TRACE_AGENT_PORT: agentPort } }) @@ -351,16 +353,16 @@ describe('profiler', () => { const procStart = BigInt(Date.now() * 1000000) const env = { DD_PROFILING_EXPORTERS: 'file', - DD_PROFILING_ENABLED: 1, + DD_PROFILING_ENABLED: '1', BUSY_CYCLE_TIME: (busyCycleTimeNs | 0).toString(), DD_TRACE_AGENT_PORT: agent.port } - // With Node 23 or later, test the profiler with async context frame use. + // With Node 22.9.0 or later, test the profiler with async context frame use. const execArgv = [] - if (satisfies(process.versions.node, '>=23.0.0')) { + if (satisfies(process.versions.node, '>=22.9.0')) { env.DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED = 1 - if (!satisfies(process.versions.node, '>=24.0.0')) { - // For Node 23, use the experimental command line flag for Node to enable + if (!isAtLeast24) { + // For Node 22.9.0+, use the experimental command line flag for Node to enable // async context frame. Node 24 has it enabled by default. execArgv.push('--experimental-async-context-frame') } @@ -372,7 +374,7 @@ describe('profiler', () => { // Must've counted the number of times each endpoint was hit const event = JSON.parse((await readLatestFile(cwd, /^event_.+\.json$/)).toString()) - assert.deepEqual(event.endpoint_counts, { 'endpoint-0': 1, 'endpoint-1': 1, 'endpoint-2': 1 }) + assert.deepStrictEqual(event.endpoint_counts, { 'endpoint-0': 1, 'endpoint-1': 1, 'endpoint-2': 1 }) const { profile, encoded } = await getLatestProfile(cwd, /^wall_.+\.pprof$/) @@ -424,13 +426,13 @@ describe('profiler', () => { // Timestamp must be defined and be between process start and end time assert.notStrictEqual(ts, undefined, encoded) assert.strictEqual(typeof osThreadId, 'number', encoded) - assert.equal(threadId, strings.dedup('0'), encoded) + assert.strictEqual(threadId, strings.dedup('0'), encoded) assert.ok(ts <= procEnd, encoded) assert.ok(ts >= procStart, encoded) // Thread name must be defined and exactly equal "Main Event Loop" - assert.equal(threadName, threadNameValue, encoded) + assert.strictEqual(threadName, threadNameValue, encoded) } else { - assert.equal(threadId, strings.dedup('NA'), encoded) + assert.strictEqual(threadId, strings.dedup('NA'), encoded) } // Either all or none of span-related labels are defined if (endpoint === undefined) { @@ -454,7 +456,7 @@ describe('profiler', () => { const existingSpanData = spans.get(spanId) if (existingSpanData) { // Span's root span and endpoint must be consistent across samples - assert.deepEqual(spanData, existingSpanData, encoded) + assert.deepStrictEqual(spanData, existingSpanData, encoded) } else { // New span id, store span data spans.set(spanId, spanData) @@ -474,18 +476,18 @@ describe('profiler', () => { } // Need to have a total of 9 different spans, with 3 different root spans // and 3 different endpoints. - assert.equal(spans.size, 9, encoded) - assert.equal(rootSpans.size, 3, encoded) - assert.equal(endpoints.size, 3, encoded) + assert.strictEqual(spans.size, 9, encoded) + assert.strictEqual(rootSpans.size, 3, encoded) + assert.strictEqual(endpoints.size, 3, encoded) }) it('fs timeline events work', async () => { const fsEvents = await gatherFilesystemTimelineEvents(cwd, 'profiler/fstest.js', agent.port) - assert.equal(fsEvents.length, 6) + assert.strictEqual(fsEvents.length, 6) const path = fsEvents[0].path const fd = fsEvents[1].fd assert(path.endsWith('tempfile.txt')) - assert.sameDeepMembers(fsEvents, [ + assertObjectContains(fsEvents, [ { flag: 'w', mode: '', operation: 'open', path }, { fd, operation: 'write' }, { fd, operation: 'close' }, @@ -497,12 +499,15 @@ describe('profiler', () => { it('dns timeline events work', async () => { const dnsEvents = await gatherNetworkTimelineEvents(cwd, 'profiler/dnstest.js', agent.port, 'dns') - assert.sameDeepMembers(dnsEvents, [ - { operation: 'lookup', host: 'example.org' }, - { operation: 'lookup', host: 'example.com' }, + const compare = (a, b) => { + return a.operation.localeCompare(b.operation) || (a.host?.localeCompare(b.host) ?? 0) + } + assertObjectContains(dnsEvents.sort(compare), [ { operation: 'lookup', host: 'datadoghq.com' }, + { operation: 'lookup', host: 'example.com' }, + { operation: 'lookup', host: 'example.org' }, + { operation: 'lookupService', address: '13.224.103.60', port: 80 }, { operation: 'queryA', host: 'datadoghq.com' }, - { operation: 'lookupService', address: '13.224.103.60', port: 80 } ]) }) @@ -537,7 +542,7 @@ describe('profiler', () => { const events = await gatherNetworkTimelineEvents(cwd, 'profiler/nettest.js', agent.port, 'net', args) // The profiled program should have two TCP connection events to the two // servers. - assert.sameDeepMembers(events, [ + assertObjectContains(events, [ { operation: 'connect', host: '127.0.0.1', port: port1 }, { operation: 'connect', host: '127.0.0.1', port: port2 } ]) @@ -554,8 +559,8 @@ describe('profiler', () => { beforeEach(() => { oomEnv = { DD_TRACE_AGENT_PORT: agent.port, - DD_PROFILING_ENABLED: 1, - DD_TRACE_DEBUG: 1, + DD_PROFILING_ENABLED: '1', + DD_TRACE_DEBUG: '1', DD_TRACE_LOG_LEVEL: 'warn' } }) @@ -569,7 +574,7 @@ describe('profiler', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_PROFILING_ENABLED: 1 + DD_PROFILING_ENABLED: '1' } }) const checkTelemetry = agent.assertTelemetryReceived('generate-metrics', 1000) @@ -597,7 +602,7 @@ describe('profiler', () => { it('sends a heap profile on OOM in worker thread and exits successfully', () => { proc = fork(oomTestFile, [1, 50], { cwd, - env: { ...oomEnv, DD_PROFILING_WALLTIME_ENABLED: 0 } + env: { ...oomEnv, DD_PROFILING_WALLTIME_ENABLED: '0' } }) return checkProfiles(agent, proc, timeout, ['space'], false) }) @@ -611,8 +616,8 @@ describe('profiler', () => { execArgv: oomExecArgv, env: { ...oomEnv, - DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE: 15000000, - DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT: 3 + DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE: '15000000', + DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT: '3' } }) return checkProfiles(agent, proc, timeout, ['space'], false, false) @@ -624,8 +629,8 @@ describe('profiler', () => { execArgv: oomExecArgv, env: { ...oomEnv, - DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE: 10000000, - DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT: 1, + DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE: '10000000', + DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT: '1', DD_PROFILING_EXPERIMENTAL_OOM_EXPORT_STRATEGIES: 'async' } }) @@ -638,8 +643,8 @@ describe('profiler', () => { execArgv: oomExecArgv, env: { ...oomEnv, - DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE: 10000000, - DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT: 1, + DD_PROFILING_EXPERIMENTAL_OOM_HEAP_LIMIT_EXTENSION_SIZE: '10000000', + DD_PROFILING_EXPERIMENTAL_OOM_MAX_HEAP_EXTENSION_COUNT: '1', DD_PROFILING_EXPERIMENTAL_OOM_EXPORT_STRATEGIES: 'async,process' } }) @@ -687,8 +692,8 @@ describe('profiler', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_PROFILING_ENABLED: 1, - DD_PROFILING_UPLOAD_PERIOD: 1, + DD_PROFILING_ENABLED: '1', + DD_PROFILING_UPLOAD_PERIOD: '1', TEST_DURATION_MS: 2500 } }) @@ -698,10 +703,10 @@ describe('profiler', () => { const checkMetrics = agent.assertTelemetryReceived(({ _, payload }) => { const pp = payload.payload - assert.equal(pp.namespace, 'profilers') + assert.strictEqual(pp.namespace, 'profilers') const series = pp.series const requests = series.find(s => s.metric === 'profile_api.requests') - assert.equal(requests.type, 'count') + assert.strictEqual(requests.type, 'count') // There's a race between metrics and on-shutdown profile, so metric // value will be between 1 and 3 requestCount = requests.points[0][1] @@ -709,30 +714,30 @@ describe('profiler', () => { assert.ok(requestCount <= 3) const responses = series.find(s => s.metric === 'profile_api.responses') - assert.equal(responses.type, 'count') - assert.match(responses.tags, /status_code:200/) + assert.strictEqual(responses.type, 'count') + assert.deepStrictEqual(responses.tags, ['status_code:200']) // Same number of requests and responses - assert.equal(responses.points[0][1], requestCount) + assert.strictEqual(responses.points[0][1], requestCount) }, 'generate-metrics', timeout) const checkDistributions = agent.assertTelemetryReceived(({ _, payload }) => { const pp = payload.payload - assert.equal(pp.namespace, 'profilers') + assert.strictEqual(pp.namespace, 'profilers') const series = pp.series assert.strictEqual(series.length, 2) - assert.equal(series[0].metric, 'profile_api.bytes') - assert.equal(series[1].metric, 'profile_api.ms') + assert.strictEqual(series[0].metric, 'profile_api.bytes') + assert.strictEqual(series[1].metric, 'profile_api.ms') // Same number of points pointsCount = series[0].points.length - assert.equal(pointsCount, series[1].points.length) + assert.strictEqual(pointsCount, series[1].points.length) }, 'distributions', timeout) await Promise.all([checkProfiles(agent, proc, timeout), checkMetrics, checkDistributions]) // Same number of requests and points - assert.equal(requestCount, pointsCount) + assert.strictEqual(requestCount, pointsCount) }) it('sends wall profiler sample context telemetry', async function () { @@ -749,21 +754,21 @@ describe('profiler', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_PROFILING_ENABLED: 1, - DD_PROFILING_UPLOAD_PERIOD: 1, - DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED: 1, - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, // every second + DD_PROFILING_ENABLED: '1', + DD_PROFILING_UPLOAD_PERIOD: '1', + DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED: '1', + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', // every second TEST_DURATION_MS: 1500 } }) const checkMetrics = agent.assertTelemetryReceived(({ _, payload }) => { const pp = payload.payload - assert.equal(pp.namespace, 'profilers'); + assert.strictEqual(pp.namespace, 'profilers'); ['live', 'used'].forEach(metricName => { const sampleContexts = pp.series.find(s => s.metric === `wall.async_contexts_${metricName}`) assert.notStrictEqual(sampleContexts, undefined) - assert.equal(sampleContexts.type, 'gauge') + assert.strictEqual(sampleContexts.type, 'gauge') assert.ok(sampleContexts.points[0][1] >= 1) }) }, 'generate-metrics', timeout) diff --git a/integration-tests/remote_config.spec.js b/integration-tests/remote_config.spec.js index 50d478e8dd2..f604198fdf3 100644 --- a/integration-tests/remote_config.spec.js +++ b/integration-tests/remote_config.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('./helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('./helpers') describe('Remote config client id', () => { let axios, cwd, appFile @@ -42,9 +42,44 @@ describe('Remote config client id', () => { await axios.get('/') return agent.assertMessageReceived(({ payload }) => { - assert.ok(payload[0][0].meta['_dd.rc.client_id'] != null) + assert.ok(payload[0][0].meta['_dd.rc.client_id']) }) }) + + it('should include process tags in remote config requests', (done) => { + const handleRemoteConfigRequest = (payload) => { + try { + const { client } = payload + assert.ok(client, 'client should exist in remote config request') + assert.ok(client.client_tracer, 'client_tracer should exist') + assert.ok(client.client_tracer.process_tags, 'process_tags should exist') + + const processTags = client.client_tracer.process_tags + + // Verify process_tags is an array of strings + assert.ok(Array.isArray(processTags), 'process_tags should be an array') + + // Verify required process tags are present + assert.ok(processTags.some(tag => tag.startsWith('entrypoint.basedir:'))) + assert.ok(processTags.some(tag => tag.startsWith('entrypoint.name:'))) + assert.ok(processTags.some(tag => tag.startsWith('entrypoint.type:'))) + assert.ok(processTags.some(tag => tag.startsWith('entrypoint.workdir:'))) + + // Verify entrypoint.type has the expected value + assert.ok(processTags.some(tag => tag === 'entrypoint.type:script')) + done() + } catch (err) { + done(err) + } finally { + agent.removeListener('remote-config-request', handleRemoteConfigRequest) + } + } + + agent.on('remote-config-request', handleRemoteConfigRequest) + + // Trigger a request to ensure remote config is polled + axios.get('/').catch(() => {}) + }) }) describe('disabled', () => { @@ -56,7 +91,7 @@ describe('Remote config client id', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_REMOTE_CONFIGURATION_ENABLED: false + DD_REMOTE_CONFIGURATION_ENABLED: 'false' } }) axios = Axios.create({ baseURL: proc.url }) diff --git a/integration-tests/selenium/selenium.spec.js b/integration-tests/selenium/selenium.spec.js index f2b0eb7e6a6..c1b814730bd 100644 --- a/integration-tests/selenium/selenium.spec.js +++ b/integration-tests/selenium/selenium.spec.js @@ -35,7 +35,6 @@ versionRange.forEach(version => { 'mocha', 'jest', '@cucumber/cucumber', - 'chai@v4', `selenium-webdriver@${version}` ]) diff --git a/integration-tests/standalone-asm/index.js b/integration-tests/standalone-asm/index.js index f83ecc211e8..c7368cb7115 100644 --- a/integration-tests/standalone-asm/index.js +++ b/integration-tests/standalone-asm/index.js @@ -23,12 +23,13 @@ if (process.env.AGENT_URL) { const tracer = require('dd-trace') tracer.init(options) +const crypto = require('crypto') const http = require('http') + const express = require('express') const app = express() const valueToHash = 'iast-showcase-demo' -const crypto = require('crypto') async function makeRequest (url) { return new Promise((resolve, reject) => { diff --git a/integration-tests/startup.spec.js b/integration-tests/startup.spec.js index 6f472cca6b6..60843b6516a 100644 --- a/integration-tests/startup.spec.js +++ b/integration-tests/startup.spec.js @@ -2,17 +2,18 @@ const assert = require('node:assert/strict') +const path = require('path') +const { inspect } = require('util') +const fs = require('fs') +const semver = require('semver') const { FakeAgent, spawnProc, + spawnProcAndExpectExit, sandboxCwd, useSandbox, curlAndAssertMessage } = require('./helpers') -const path = require('path') -const semver = require('semver') -const { inspect } = require('util') -const fs = require('fs') const execArgvs = [ { @@ -25,19 +26,33 @@ const execArgvs = [ { execArgv: ['--loader', 'dd-trace/loader-hook.mjs'], skip: semver.satisfies(process.versions.node, '>=20.6') + }, + { + execArgv: [], + optional: false } ] -execArgvs.forEach(({ execArgv, skip }) => { +execArgvs.forEach(({ execArgv, skip, optional = true }) => { const describe = skip ? globalThis.describe.skip : globalThis.describe - describe(`startup ${execArgv.join(' ')}`, () => { + describe(`startup ${execArgv.join(' ').concat(`(optional: ${optional})`)}`, () => { let agent let proc let cwd let startupTestFile let unsupportedTestFile + before(() => { + if (optional === false) { + process.env.OMIT = 'optional' + } + }) + + after(() => { + delete process.env.OMIT + }) + useSandbox(['d3-format@3.1.0']) before(() => { @@ -74,38 +89,41 @@ execArgvs.forEach(({ execArgv, skip }) => { }) }) - it('saves tracer configuration on disk', async () => { - if (process.platform !== 'linux') { - return - } - - proc = await spawnProc(startupTestFile, { - cwd, - execArgv, - env: { - AGENT_PORT: agent.port + // This feature requires libdatadog which is an optional dependency. + if (optional) { + it('saves tracer configuration on disk', async () => { + if (process.platform !== 'linux') { + return } - }) - const containsDatadogMemfd = (fds) => { - for (const fd of fds) { - try { - const fdName = fs.readlinkSync(`/proc/${proc.pid}/fd/${fd}`) - if (fdName.includes('datadog-tracer-info-')) { - return true - } - } catch {} + proc = await spawnProc(startupTestFile, { + cwd, + execArgv, + env: { + AGENT_PORT: agent.port + } + }) + + const containsDatadogMemfd = (fds) => { + for (const fd of fds) { + try { + const fdName = fs.readlinkSync(`/proc/${proc.pid}/fd/${fd}`) + if (fdName.includes('datadog-tracer-info-')) { + return true + } + } catch {} + } + return false } - return false - } - const fds = fs.readdirSync(`/proc/${proc.pid}/fd`) + const fds = fs.readdirSync(`/proc/${proc.pid}/fd`) - assert( - containsDatadogMemfd(fds), - `FDs ${inspect(fds)} of PID ${proc.pid} did not contain the datadog tracer configuration in memfd` - ) - }) + assert( + containsDatadogMemfd(fds), + `FDs ${inspect(fds)} of PID ${proc.pid} did not contain the datadog tracer configuration in memfd` + ) + }) + } it('works for options.url', async () => { proc = await spawnProc(startupTestFile, { @@ -221,7 +239,7 @@ execArgvs.forEach(({ execArgv, skip }) => { context('with unsupported module', () => { it('skips the unsupported module', async () => { - await spawnProc(unsupportedTestFile, { + await spawnProcAndExpectExit(unsupportedTestFile, { cwd, execArgv }) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index 160eb098ea3..8b253c463ef 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -217,7 +217,7 @@ versions.forEach((version) => { assert.strictEqual(test.content.meta[TEST_IS_TEST_FRAMEWORK_WORKER], 'true') } assert.strictEqual(test.content.meta[TEST_COMMAND], 'vitest run') - assert.ok(test.content.metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(test.content.metrics[DD_HOST_CPU_COUNT]) assert.strictEqual(test.content.meta[DD_TEST_IS_USER_PROVIDED_SERVICE], 'false') }) @@ -232,7 +232,7 @@ versions.forEach((version) => { true ) assert.strictEqual(testSuite.content.metrics[TEST_SOURCE_START], 1) - assert.ok(testSuite.content.metrics[DD_HOST_CPU_COUNT] != null) + assert.ok(testSuite.content.metrics[DD_HOST_CPU_COUNT]) }) }) ]) @@ -390,7 +390,7 @@ versions.forEach((version) => { env: { ...getCiVisAgentlessConfig(receiver.port), TEST_DIR: 'ci-visibility/vitest-tests/flaky-test-retries*', - DD_CIVISIBILITY_FLAKY_RETRY_COUNT: 1, + DD_CIVISIBILITY_FLAKY_RETRY_COUNT: '1', NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' // ESM requires more flags }, stdio: 'pipe' @@ -1277,7 +1277,7 @@ versions.forEach((version) => { assert.strictEqual(retriedTest.metrics[`${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_LINE_SUFFIX}`], 4) const snapshotIdKey = `${DI_DEBUG_ERROR_PREFIX}.0.${DI_DEBUG_ERROR_SNAPSHOT_ID_SUFFIX}` - assert.ok(retriedTest.meta[snapshotIdKey] != null) + assert.ok(retriedTest.meta[snapshotIdKey]) snapshotIdByTest = retriedTest.meta[snapshotIdKey] spanIdByTest = retriedTest.span_id.toString() diff --git a/loader-hook.mjs b/loader-hook.mjs index 187027393e3..cae814381d3 100644 --- a/loader-hook.mjs +++ b/loader-hook.mjs @@ -1,8 +1,9 @@ -import regexpEscapeModule from './vendor/dist/escape-string-regexp/index.js' import * as iitm from 'import-in-the-middle/hook.mjs' +import regexpEscapeModule from './vendor/dist/escape-string-regexp/index.js' import hooks from './packages/datadog-instrumentations/src/helpers/hooks.js' import configHelper from './packages/dd-trace/src/config-helper.js' import * as rewriterLoader from './packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs' +import { isRelativeRequire } from './packages/datadog-instrumentations/src/helpers/shared-utils.js' const regexpEscape = regexpEscapeModule.default @@ -28,7 +29,11 @@ function addInstrumentations (data) { const instrumentations = Object.keys(hooks) for (const moduleName of instrumentations) { - data.include.push(new RegExp(`node_modules/${moduleName}/(?!node_modules).+`), moduleName) + // Skip instrumentation hooks with relative module names + // since there is no current business need of instrumenting imports outside of the node_modules folder + if (!isRelativeRequire(moduleName)) { + data.include.push(new RegExp(`node_modules/${moduleName}/(?!node_modules).+`), moduleName) + } } } diff --git a/package.json b/package.json index 51c25cbe45b..9507f38e924 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.81.0", + "version": "5.82.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", @@ -28,8 +28,8 @@ "test:debugger": "mocha -r 'packages/dd-trace/test/setup/mocha.js' 'packages/dd-trace/test/debugger/**/*.spec.js'", "test:debugger:ci": "nyc --no-clean --include 'packages/dd-trace/src/debugger/**/*.js' -- npm run test:debugger", "test:eslint-rules": "node eslint-rules/*.test.mjs", - "test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,datastreams,encode,exporters,opentelemetry,opentracing,plugins,remote_config,service-naming,standalone,telemetry,external-logger}/**/*.spec.js\"", - "test:trace:core:ci": "npm run test:trace:core -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/**/*.js\"", + "test:trace:core": "node scripts/mocha-parallel-files.js --expose-gc --timeout 30000 --require packages/dd-trace/test/setup/mocha.js -- \"packages/dd-trace/test/*.spec.js\" \"packages/dd-trace/test/{ci-visibility,config,datastreams,encode,exporters,opentelemetry,opentracing,plugins,remote_config,service-naming,standalone,telemetry,external-logger}/**/*.spec.js\"", + "test:trace:core:ci": "nyc --no-clean --include \"packages/dd-trace/src/**/*.js\" -- npm run test:trace:core", "test:trace:guardrails": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/guardrails/**/*.spec.js\"", "test:trace:guardrails:ci": "nyc --no-clean --include \"packages/dd-trace/src/guardrails/**/*.js\" -- npm run test:trace:guardrails", "test:esbuild": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/datadog-esbuild/test/**/*.spec.js\"", @@ -38,8 +38,8 @@ "test:instrumentations:ci": "yarn services && nyc --no-clean --include \"packages/datadog-instrumentations/src/@($(echo $PLUGINS)).js\" --include \"packages/datadog-instrumentations/src/@($(echo $PLUGINS))/**/*.js\" -- npm run test:instrumentations", "test:instrumentations:misc": "mocha -r 'packages/dd-trace/test/setup/mocha.js' 'packages/datadog-instrumentations/test/*/**/*.spec.js'", "test:instrumentations:misc:ci": "nyc --no-clean --include 'packages/datadog-instrumentations/src/**/*.js' -- npm run test:instrumentations:misc", - "test:core": "tap \"packages/datadog-core/test/**/*.spec.js\"", - "test:core:ci": "npm run test:core -- --coverage --nyc-arg=--include=\"packages/datadog-core/src/**/*.js\"", + "test:core": "node scripts/mocha-parallel-files.js --expose-gc --timeout 30000 -- \"packages/datadog-core/test/**/*.spec.js\"", + "test:core:ci": "nyc --no-clean --include \"packages/datadog-core/src/**/*.js\" -- npm run test:core", "test:lambda": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/lambda/**/*.spec.js\"", "test:lambda:ci": "nyc --no-clean --include \"packages/dd-trace/src/lambda/**/*.js\" -- npm run test:lambda", "test:llmobs:sdk": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" --exclude \"packages/dd-trace/test/llmobs/plugins/**/*.spec.js\" \"packages/dd-trace/test/llmobs/**/*.spec.js\" ", @@ -48,12 +48,12 @@ "test:llmobs:plugins:ci": "yarn services && nyc --no-clean --include \"packages/dd-trace/src/llmobs/**/*.js\" -- npm run test:llmobs:plugins", "test:openfeature": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/openfeature/*.spec.js\"", "test:openfeature:ci": "nyc --no-clean --include \"packages/dd-trace/src/openfeature/**/*.js\" -- npm run test:openfeature", - "test:plugins": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/datadog-plugin-@($(echo $PLUGINS))/test/**/@($(echo ${SPEC:-'*'})).spec.js\"", + "test:plugins": "mocha --expose-gc -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/datadog-plugin-@($(echo $PLUGINS))/test/**/@($(echo ${SPEC:-'*'}))*.spec.js\"", "test:plugins:ci": "yarn services && nyc --no-clean --include \"packages/datadog-plugin-@($(echo $PLUGINS))/src/**/*.js\" -- npm run test:plugins", "test:plugins:ci:flaky": "yarn services && nyc --no-clean --include \"packages/datadog-plugin-@($(echo $PLUGINS))/src/**/*.js\" -- npm run test:plugins -- --bail --retries 2", "test:plugins:upstream": "node ./packages/dd-trace/test/plugins/suite.js", - "test:profiler": "tap \"packages/dd-trace/test/profiling/**/*.spec.js\"", - "test:profiler:ci": "npm run test:profiler -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/profiling/**/*.js\"", + "test:profiler": "node scripts/mocha-parallel-files.js --expose-gc --timeout 30000 --require packages/dd-trace/test/setup/mocha.js -- \"packages/dd-trace/test/profiling/**/*.spec.js\"", + "test:profiler:ci": "nyc --no-clean --include \"packages/dd-trace/src/profiling/**/*.js\" -- npm run test:profiler", "test:integration": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/*.spec.js\"", "test:integration:aiguard": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/aiguard/*.spec.js\"", "test:integration:appsec": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/appsec/*.spec.js\"", @@ -95,7 +95,7 @@ }, "homepage": "https://github.com/DataDog/dd-trace-js#readme", "engines": { - "node": ">=18" + "node": ">=18 <26" }, "files": [ "/package.json", @@ -127,7 +127,7 @@ ], "dependencies": { "dc-polyfill": "^0.1.10", - "import-in-the-middle": "^1.14.2" + "import-in-the-middle": "2.0.0" }, "optionalDependencies": { "@datadog/libdatadog": "0.7.0", @@ -135,7 +135,7 @@ "@datadog/native-iast-taint-tracking": "4.1.0", "@datadog/native-metrics": "3.1.1", "@datadog/openfeature-node-server": "^0.2.0", - "@datadog/pprof": "5.12.0", + "@datadog/pprof": "5.13.1", "@datadog/wasm-js-rewriter": "5.0.1", "@opentelemetry/api": ">=1.0.0 <1.10.0", "@opentelemetry/api-logs": "<1.0.0" @@ -148,16 +148,13 @@ "@openfeature/core": "^1.8.1", "@openfeature/server-sdk": "~1.20.0", "@stylistic/eslint-plugin": "^5.5.0", - "@types/chai": "^4.3.16", "@types/mocha": "^10.0.10", "@types/node": "^18.19.106", "@types/sinon": "^21.0.0", - "@types/tap": "^15.0.12", "axios": "^1.12.2", "benchmark": "^2.1.4", "body-parser": "^2.2.0", "bun": "1.3.3", - "chai": "^4.5.0", "eslint": "^9.39.0", "eslint-plugin-cypress": "^5.2.0", "eslint-plugin-import": "^2.32.0", @@ -176,16 +173,14 @@ "mocha-multi-reporters": "^1.5.1", "multer": "^2.0.2", "nock": "^13.5.6", - "nyc": "^15.1.0", + "nyc": "^17.1.0", "octokit": "^5.0.3", "opentracing": ">=0.14.7", "proxyquire": "^2.1.3", "retry": "^0.13.1", "semifies": "^1.0.0", "semver": "^7.7.2", - "sinon": "^21.0.0", - "sinon-chai": "^3.7.0", - "tap": "^16.3.10", + "sinon": "^21.0.1", "tiktoken": "^1.0.21", "typescript": "^5.9.2", "workerpool": "^10.0.0", diff --git a/packages/datadog-code-origin/test/index.spec.js b/packages/datadog-code-origin/test/index.spec.js index 21b5e0870ca..d1feff6d206 100644 --- a/packages/datadog-code-origin/test/index.spec.js +++ b/packages/datadog-code-origin/test/index.spec.js @@ -1,11 +1,11 @@ 'use strict' -const { describe, it, afterEach } = require('tap').mocha const { resolve } = require('node:path') const assert = require('node:assert') -require('../../dd-trace/test/setup/core') +const { describe, it, afterEach } = require('mocha') +require('../../dd-trace/test/setup/core') const { entryTags, exitTags } = require('../index') const { getNextLineNumber } = require('../../dd-trace/test/plugins/helpers') diff --git a/packages/datadog-core/src/utils/src/parse-tags.js b/packages/datadog-core/src/utils/src/parse-tags.js index b3eb74429a3..4bc0ac6326d 100644 --- a/packages/datadog-core/src/utils/src/parse-tags.js +++ b/packages/datadog-core/src/utils/src/parse-tags.js @@ -8,7 +8,7 @@ const digitRegex = /^\d+$/ * Also supports array-keys. For example: * { 'a.0.b': 'value' } -> { a: [{ b: 'value' }] } * - * @param {Object} tags - Key/value pairs of tags + * @param {object} tags - Key/value pairs of tags * @returns Object - Parsed tags */ module.exports = function parseTags (tags) { diff --git a/packages/datadog-core/test/storage.spec.js b/packages/datadog-core/test/storage.spec.js index 259532fb099..670a6011f9c 100644 --- a/packages/datadog-core/test/storage.spec.js +++ b/packages/datadog-core/test/storage.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha const { executionAsyncResource } = require('async_hooks') -require('../../dd-trace/test/setup/core') +const { describe, it, beforeEach, afterEach } = require('mocha') +require('../../dd-trace/test/setup/core') const { storage } = require('../src/storage') describe('storage', () => { diff --git a/packages/datadog-core/test/utils/src/parse-tags.spec.js b/packages/datadog-core/test/utils/src/parse-tags.spec.js index 385bf699931..339ecb3976b 100644 --- a/packages/datadog-core/test/utils/src/parse-tags.spec.js +++ b/packages/datadog-core/test/utils/src/parse-tags.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../../../../dd-trace/test/setup/core') +const { describe, it } = require('mocha') +require('../../../../dd-trace/test/setup/core') const parseTags = require('../../../src/utils/src/parse-tags') describe('parseTags', () => { diff --git a/packages/datadog-core/test/utils/src/set.spec.js b/packages/datadog-core/test/utils/src/set.spec.js index db05792e327..05797eddf01 100644 --- a/packages/datadog-core/test/utils/src/set.spec.js +++ b/packages/datadog-core/test/utils/src/set.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../../../../dd-trace/test/setup/core') +const { describe, it } = require('mocha') +require('../../../../dd-trace/test/setup/core') const set = require('../../../src/utils/src/set') describe('set', () => { diff --git a/packages/datadog-esbuild/index.js b/packages/datadog-esbuild/index.js index ea1279e0270..1d9d657e07e 100644 --- a/packages/datadog-esbuild/index.js +++ b/packages/datadog-esbuild/index.js @@ -2,13 +2,18 @@ /* eslint-disable no-console */ +const { execSync } = require('child_process') +const fs = require('fs') +const RAW_BUILTINS = require('module').builtinModules +const path = require('path') +const { pathToFileURL, fileURLToPath } = require('url') + const instrumentations = require('../datadog-instrumentations/src/helpers/instrumentations.js') -const hooks = require('../datadog-instrumentations/src/helpers/hooks.js') + const extractPackageAndModulePath = require( '../datadog-instrumentations/src/helpers/extract-package-and-module-path.js' ) - -const { pathToFileURL, fileURLToPath } = require('url') +const hooks = require('../datadog-instrumentations/src/helpers/hooks.js') const { processModule, isESMFile } = require('./src/utils.js') const ESM_INTERCEPTED_SUFFIX = '._dd_esbuild_intercepted' @@ -36,11 +41,7 @@ for (const instrumentation of Object.values(instrumentations)) { } } -const RAW_BUILTINS = require('module').builtinModules const CHANNEL = 'dd-trace:bundler:load' -const path = require('path') -const fs = require('fs') -const { execSync } = require('child_process') const builtins = new Set() diff --git a/packages/datadog-esbuild/src/utils.js b/packages/datadog-esbuild/src/utils.js index 4d4c14f40a1..6778659494a 100644 --- a/packages/datadog-esbuild/src/utils.js +++ b/packages/datadog-esbuild/src/utils.js @@ -7,8 +7,20 @@ const path = require('node:path') const { NODE_MAJOR, NODE_MINOR } = require('../../../version.js') const getExportsImporting = (url) => import(url).then(Object.keys) +let getExportsModulePromise + +const loadGetExportsModule = () => { + if (!getExportsModulePromise) { + getExportsModulePromise = import('import-in-the-middle/lib/get-exports.mjs') + } + return getExportsModulePromise +} + const getExports = NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19) - ? require('import-in-the-middle/lib/get-exports.js') + ? async (srcUrl, context, getSource) => { + const mod = await loadGetExportsModule() + return mod.getExports(srcUrl, context, getSource) + } : getExportsImporting function isStarExportLine (line) { @@ -168,7 +180,7 @@ async function processModule ({ path, internal, context, excludeDefault }) { * * @param {string} fullPathToModule File to analize * @param {string} [modulePackageJsonPath] Path of the package.json - * @param {Object} [packageJson] The content of the module package.json + * @param {object} [packageJson] The content of the module package.json * @returns {boolean} */ function isESMFile (fullPathToModule, modulePackageJsonPath, packageJson = {}) { diff --git a/packages/datadog-esbuild/test/utils.spec.js b/packages/datadog-esbuild/test/utils.spec.js index d8513f39918..d79bb2b03fa 100644 --- a/packages/datadog-esbuild/test/utils.spec.js +++ b/packages/datadog-esbuild/test/utils.spec.js @@ -1,10 +1,10 @@ 'use strict' -const { processModule } = require('../src/utils.js') const assert = require('assert') const path = require('path') -const sinon = require('sinon') const fs = require('fs') +const sinon = require('sinon') +const { processModule } = require('../src/utils.js') describe('esbuild utils', () => { describe('processModule', () => { diff --git a/packages/datadog-instrumentations/src/aerospike.js b/packages/datadog-instrumentations/src/aerospike.js index ba310b6e2de..45666b6dccd 100644 --- a/packages/datadog-instrumentations/src/aerospike.js +++ b/packages/datadog-instrumentations/src/aerospike.js @@ -1,11 +1,12 @@ 'use strict' +const tracingChannel = require('dc-polyfill').tracingChannel + +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const tracingChannel = require('dc-polyfill').tracingChannel const ch = tracingChannel('apm:aerospike:command') function wrapCreateCommand (createCommand) { diff --git a/packages/datadog-instrumentations/src/ai.js b/packages/datadog-instrumentations/src/ai.js index 5d6f0750b6d..3a509ccb378 100644 --- a/packages/datadog-instrumentations/src/ai.js +++ b/packages/datadog-instrumentations/src/ai.js @@ -1,9 +1,9 @@ 'use strict' -const { addHook } = require('./helpers/instrument') +const { channel, tracingChannel } = require('dc-polyfill') const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') -const { channel, tracingChannel } = require('dc-polyfill') const toolCreationChannel = channel('dd-trace:vercel-ai:tool') const TRACED_FUNCTIONS = { diff --git a/packages/datadog-instrumentations/src/amqp10.js b/packages/datadog-instrumentations/src/amqp10.js index 2b24309a56a..f0fccd8675c 100644 --- a/packages/datadog-instrumentations/src/amqp10.js +++ b/packages/datadog-instrumentations/src/amqp10.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'amqp10', file: 'lib/sender_link.js', versions: ['>=3'] }, SenderLink => { const startCh = channel('apm:amqp10:send:start') diff --git a/packages/datadog-instrumentations/src/amqplib.js b/packages/datadog-instrumentations/src/amqplib.js index 0f8900c1ef1..7f33519d07e 100644 --- a/packages/datadog-instrumentations/src/amqplib.js +++ b/packages/datadog-instrumentations/src/amqplib.js @@ -1,13 +1,13 @@ 'use strict' -const { - channel, - addHook -} = require('./helpers/instrument') const kebabCase = require('../../datadog-core/src/utils/src/kebabcase') const shimmer = require('../../datadog-shimmer') const { NODE_MAJOR, NODE_MINOR } = require('../../../version') +const { + channel, + addHook +} = require('./helpers/instrument') const MIN_VERSION = ((NODE_MAJOR > 22) || (NODE_MAJOR === 22 && NODE_MINOR >= 2)) ? '>=0.5.3' : '>=0.5.0' const commandStartCh = channel('apm:amqplib:command:start') diff --git a/packages/datadog-instrumentations/src/anthropic.js b/packages/datadog-instrumentations/src/anthropic.js index 3dfeab95ff0..69530f3c7ef 100644 --- a/packages/datadog-instrumentations/src/anthropic.js +++ b/packages/datadog-instrumentations/src/anthropic.js @@ -1,8 +1,8 @@ 'use strict' -const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const { channel, tracingChannel } = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') const anthropicTracingChannel = tracingChannel('apm:anthropic:request') const onStreamedChunkCh = channel('apm:anthropic:request:chunk') diff --git a/packages/datadog-instrumentations/src/apollo-server-core.js b/packages/datadog-instrumentations/src/apollo-server-core.js index cd9749f8e4d..b00bf2dfa84 100644 --- a/packages/datadog-instrumentations/src/apollo-server-core.js +++ b/packages/datadog-instrumentations/src/apollo-server-core.js @@ -1,8 +1,8 @@ 'use strict' -const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const dc = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') const requestChannel = dc.tracingChannel('datadog:apollo-server-core:request') diff --git a/packages/datadog-instrumentations/src/apollo-server.js b/packages/datadog-instrumentations/src/apollo-server.js index 2dde75e0a39..ecd8c607822 100644 --- a/packages/datadog-instrumentations/src/apollo-server.js +++ b/packages/datadog-instrumentations/src/apollo-server.js @@ -2,8 +2,8 @@ const dc = require('dc-polyfill') -const { addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') const graphqlMiddlewareChannel = dc.tracingChannel('datadog:apollo:middleware') const apolloHttpServerChannel = dc.tracingChannel('datadog:apollo:httpserver') diff --git a/packages/datadog-instrumentations/src/apollo.js b/packages/datadog-instrumentations/src/apollo.js index 0e5719ea574..a6cf7ef0995 100644 --- a/packages/datadog-instrumentations/src/apollo.js +++ b/packages/datadog-instrumentations/src/apollo.js @@ -1,11 +1,12 @@ 'use strict' +const tracingChannel = require('dc-polyfill').tracingChannel + +const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const tracingChannel = require('dc-polyfill').tracingChannel const CHANNELS = { 'gateway.request': tracingChannel('apm:apollo:gateway:request'), diff --git a/packages/datadog-instrumentations/src/avsc.js b/packages/datadog-instrumentations/src/avsc.js index c37d8a4cb67..9228a59f67d 100644 --- a/packages/datadog-instrumentations/src/avsc.js +++ b/packages/datadog-instrumentations/src/avsc.js @@ -1,9 +1,9 @@ 'use strict' +const dc = require('dc-polyfill') const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const dc = require('dc-polyfill') const serializeChannel = dc.channel('apm:avsc:serialize-start') const deserializeChannel = dc.channel('apm:avsc:deserialize-end') diff --git a/packages/datadog-instrumentations/src/aws-sdk.js b/packages/datadog-instrumentations/src/aws-sdk.js index 9d223f8c7a9..67bedb21b52 100644 --- a/packages/datadog-instrumentations/src/aws-sdk.js +++ b/packages/datadog-instrumentations/src/aws-sdk.js @@ -1,7 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const patchedClientConfigProtocols = new WeakSet() diff --git a/packages/datadog-instrumentations/src/azure-event-hubs.js b/packages/datadog-instrumentations/src/azure-event-hubs.js index 2d339bcc7f1..f5239b45e56 100644 --- a/packages/datadog-instrumentations/src/azure-event-hubs.js +++ b/packages/datadog-instrumentations/src/azure-event-hubs.js @@ -1,10 +1,10 @@ 'use strict' +const dc = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const dc = require('dc-polyfill') const producerCh = dc.tracingChannel('apm:azure-event-hubs:send') @@ -13,6 +13,7 @@ addHook({ versions: ['>=6.0.0'] }, obj => { const EventHubProducerClient = obj.EventHubProducerClient + shimmer.wrap(EventHubProducerClient.prototype, 'createBatch', createBatch => async function () { const batch = await createBatch.apply(this, arguments) @@ -20,7 +21,7 @@ addHook({ tryAdd => function (eventData) { const config = this._context.config const functionName = tryAdd.name - return producerCh.tracePromise( + return producerCh.traceSync( tryAdd, { functionName, eventData, batch: this, config }, this, ...arguments) diff --git a/packages/datadog-instrumentations/src/azure-functions.js b/packages/datadog-instrumentations/src/azure-functions.js index 76090320e66..b4cd6aae30c 100644 --- a/packages/datadog-instrumentations/src/azure-functions.js +++ b/packages/datadog-instrumentations/src/azure-functions.js @@ -1,10 +1,10 @@ 'use strict' +const dc = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const dc = require('dc-polyfill') const azureFunctionsChannel = dc.tracingChannel('datadog:azure:functions:invoke') diff --git a/packages/datadog-instrumentations/src/azure-service-bus.js b/packages/datadog-instrumentations/src/azure-service-bus.js index 2109ba331b0..d7ef9d08a6a 100644 --- a/packages/datadog-instrumentations/src/azure-service-bus.js +++ b/packages/datadog-instrumentations/src/azure-service-bus.js @@ -1,12 +1,11 @@ 'use strict' +const dc = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const dc = require('dc-polyfill') - const producerCh = dc.tracingChannel('apm:azure-service-bus:send') const isItDefault = new WeakSet() @@ -38,7 +37,7 @@ addHook({ name: '@azure/service-bus', versions: ['>=7.9.2'] }, (obj) => { shimmer.wrap(batch, 'tryAddMessage', tryAddMessage => function (msg) { const functionName = tryAddMessage.name const config = this._context.config - return producerCh.tracePromise( + return producerCh.traceSync( tryAddMessage, { config, functionName, batch, msg }, this, ...arguments) }) return batch diff --git a/packages/datadog-instrumentations/src/bluebird.js b/packages/datadog-instrumentations/src/bluebird.js index cf0205dd5c0..ec35e20807d 100644 --- a/packages/datadog-instrumentations/src/bluebird.js +++ b/packages/datadog-instrumentations/src/bluebird.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') function createGetNewLibraryCopyWrap (originalLib) { return function wrapGetNewLibraryCopy (getNewLibraryCopy) { diff --git a/packages/datadog-instrumentations/src/bullmq.js b/packages/datadog-instrumentations/src/bullmq.js new file mode 100644 index 00000000000..e1fec35bf94 --- /dev/null +++ b/packages/datadog-instrumentations/src/bullmq.js @@ -0,0 +1,11 @@ +// AUTO-GENERATED by dd-compile +// Orchestrion rewriter handles the actual instrumentation via JSON config. +// This file registers the module hooks for the rewriter to process. + +'use strict' + +const { addHook, getHooks } = require('./helpers/instrument') + +for (const hook of getHooks('bullmq')) { + addHook(hook, exports => exports) +} diff --git a/packages/datadog-instrumentations/src/bunyan.js b/packages/datadog-instrumentations/src/bunyan.js index 039e7740c7a..40e06ac0fce 100644 --- a/packages/datadog-instrumentations/src/bunyan.js +++ b/packages/datadog-instrumentations/src/bunyan.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'bunyan', versions: ['>=1'] }, Logger => { const logCh = channel('apm:bunyan:log') diff --git a/packages/datadog-instrumentations/src/cassandra-driver.js b/packages/datadog-instrumentations/src/cassandra-driver.js index bb7d218b69c..e04a608d031 100644 --- a/packages/datadog-instrumentations/src/cassandra-driver.js +++ b/packages/datadog-instrumentations/src/cassandra-driver.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:cassandra-driver:query:start') const finishCh = channel('apm:cassandra-driver:query:finish') diff --git a/packages/datadog-instrumentations/src/child_process.js b/packages/datadog-instrumentations/src/child_process.js index 71c199b7991..adfe2538627 100644 --- a/packages/datadog-instrumentations/src/child_process.js +++ b/packages/datadog-instrumentations/src/child_process.js @@ -3,11 +3,11 @@ const { errorMonitor } = require('events') const util = require('util') +const dc = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const dc = require('dc-polyfill') const childProcessChannel = dc.tracingChannel('datadog:child_process:execution') diff --git a/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js b/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js index bd0d11605e6..20733a1168e 100644 --- a/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +++ b/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js @@ -1,12 +1,12 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') + +const log = require('../../dd-trace/src/log') const { addHook, channel } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') - -const log = require('../../dd-trace/src/log') // Create channels for Confluent Kafka JavaScript const channels = { diff --git a/packages/datadog-instrumentations/src/couchbase.js b/packages/datadog-instrumentations/src/couchbase.js index 7a648b7ef71..4cb38727feb 100644 --- a/packages/datadog-instrumentations/src/couchbase.js +++ b/packages/datadog-instrumentations/src/couchbase.js @@ -1,11 +1,11 @@ 'use strict' const { errorMonitor } = require('events') +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') function findCallbackIndex (args, lowerbound = 2) { for (let i = args.length - 1; i >= lowerbound; i--) { diff --git a/packages/datadog-instrumentations/src/crypto.js b/packages/datadog-instrumentations/src/crypto.js index 7c95614cee7..03bd7a8a4f5 100644 --- a/packages/datadog-instrumentations/src/crypto.js +++ b/packages/datadog-instrumentations/src/crypto.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const cryptoHashCh = channel('datadog:crypto:hashing:start') const cryptoCipherCh = channel('datadog:crypto:cipher:start') diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 9619cdb7c9b..c4696076217 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -1,10 +1,20 @@ 'use strict' -const { createCoverageMap } = require('../../../vendor/dist/istanbul-lib-coverage') -const { addHook, channel } = require('./helpers/instrument') +const { createCoverageMap } = require('../../../vendor/dist/istanbul-lib-coverage') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const { + getCoveredFilenamesFromCoverage, + resetCoverage, + mergeCoverage, + fromCoverageMapToCoverage, + getTestSuitePath, + CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, + getIsFaultyEarlyFlakeDetection +} = require('../../dd-trace/src/plugins/util/test') +const satisfies = require('../../../vendor/dist/semifies') +const { addHook, channel } = require('./helpers/instrument') const testStartCh = channel('ci:cucumber:test:start') const testRetryCh = channel('ci:cucumber:test:retry') @@ -34,17 +44,6 @@ const itrSkippedSuitesCh = channel('ci:cucumber:itr:skipped-suites') const getCodeCoverageCh = channel('ci:nyc:get-coverage') -const { - getCoveredFilenamesFromCoverage, - resetCoverage, - mergeCoverage, - fromCoverageMapToCoverage, - getTestSuitePath, - CUCUMBER_WORKER_TRACE_PAYLOAD_CODE, - getIsFaultyEarlyFlakeDetection -} = require('../../dd-trace/src/plugins/util/test') -const satisfies = require('../../../vendor/dist/semifies') - const isMarkedAsUnskippable = (pickle) => { return pickle.tags.some(tag => tag.name === '@datadog:unskippable') } diff --git a/packages/datadog-instrumentations/src/cypress.js b/packages/datadog-instrumentations/src/cypress.js index 57604d7356b..f5c16f0a25b 100644 --- a/packages/datadog-instrumentations/src/cypress.js +++ b/packages/datadog-instrumentations/src/cypress.js @@ -1,7 +1,7 @@ 'use strict' -const { addHook } = require('./helpers/instrument') const { DD_MAJOR } = require('../../../version') +const { addHook } = require('./helpers/instrument') // No handler because this is only useful for testing. // Cypress plugin does not patch any library. diff --git a/packages/datadog-instrumentations/src/dns.js b/packages/datadog-instrumentations/src/dns.js index ac37784c58d..d43d3e135a7 100644 --- a/packages/datadog-instrumentations/src/dns.js +++ b/packages/datadog-instrumentations/src/dns.js @@ -1,7 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const rrtypes = { resolveAny: 'ANY', diff --git a/packages/datadog-instrumentations/src/elasticsearch.js b/packages/datadog-instrumentations/src/elasticsearch.js index 9cc7434e7d9..5d010e0da2f 100644 --- a/packages/datadog-instrumentations/src/elasticsearch.js +++ b/packages/datadog-instrumentations/src/elasticsearch.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') addHook({ name: '@elastic/transport', file: 'lib/Transport.js', versions: ['>=8'] }, (exports) => { shimmer.wrap(exports.default.prototype, 'request', createWrapRequest('elasticsearch')) diff --git a/packages/datadog-instrumentations/src/express-mongo-sanitize.js b/packages/datadog-instrumentations/src/express-mongo-sanitize.js index 40d208fce4d..70c5d3ea086 100644 --- a/packages/datadog-instrumentations/src/express-mongo-sanitize.js +++ b/packages/datadog-instrumentations/src/express-mongo-sanitize.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const sanitizeMethodFinished = channel('datadog:express-mongo-sanitize:sanitize:finish') const sanitizeMiddlewareFinished = channel('datadog:express-mongo-sanitize:filter:finish') diff --git a/packages/datadog-instrumentations/src/express.js b/packages/datadog-instrumentations/src/express.js index d9f0743e085..23ac52a81ba 100644 --- a/packages/datadog-instrumentations/src/express.js +++ b/packages/datadog-instrumentations/src/express.js @@ -1,7 +1,7 @@ 'use strict' -const { createWrapRouterMethod } = require('./router') const shimmer = require('../../datadog-shimmer') +const { createWrapRouterMethod } = require('./router') const { addHook, channel, tracingChannel } = require('./helpers/instrument') const { setRouterMountPath, diff --git a/packages/datadog-instrumentations/src/fs.js b/packages/datadog-instrumentations/src/fs.js index e284fee4d62..9b72e0a68a4 100644 --- a/packages/datadog-instrumentations/src/fs.js +++ b/packages/datadog-instrumentations/src/fs.js @@ -1,8 +1,8 @@ 'use strict' const { errorMonitor } = require('events') -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const startChannel = channel('apm:fs:operation:start') const finishChannel = channel('apm:fs:operation:finish') diff --git a/packages/datadog-instrumentations/src/generic-pool.js b/packages/datadog-instrumentations/src/generic-pool.js index 067a9c7f2b3..6e4ba4bc89f 100644 --- a/packages/datadog-instrumentations/src/generic-pool.js +++ b/packages/datadog-instrumentations/src/generic-pool.js @@ -1,7 +1,7 @@ 'use strict' -const { addHook, AsyncResource } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { addHook, AsyncResource } = require('./helpers/instrument') function createWrapAcquire () { return function wrapAcquire (acquire) { diff --git a/packages/datadog-instrumentations/src/google-cloud-pubsub.js b/packages/datadog-instrumentations/src/google-cloud-pubsub.js index e8009479a0f..d1ba844e803 100644 --- a/packages/datadog-instrumentations/src/google-cloud-pubsub.js +++ b/packages/datadog-instrumentations/src/google-cloud-pubsub.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const requestStartCh = channel('apm:google-cloud-pubsub:request:start') const requestFinishCh = channel('apm:google-cloud-pubsub:request:finish') @@ -14,6 +14,21 @@ const receiveStartCh = channel('apm:google-cloud-pubsub:receive:start') const receiveFinishCh = channel('apm:google-cloud-pubsub:receive:finish') const receiveErrorCh = channel('apm:google-cloud-pubsub:receive:error') +/** + * Message-level channels for trace context injection and async context propagation: + * - messagePublishCh: Inject trace context when Topic.publish() is called + * - messageAckStoreCh: Store async context when message.ack() is called (PULL) + * - messageAckRetrieveCh: Retrieve stored context when acknowledge() API is called (PULL) + * - messageStoreCh: Propagate context when Subscription emits 'message' event (PULL) + */ +const messagePublishCh = channel('apm:google-cloud-pubsub:message:publish') +const messageAckStoreCh = channel('apm:google-cloud-pubsub:message:ack-store') +const messageAckRetrieveCh = channel('apm:google-cloud-pubsub:message:ack-retrieve') +const messageStoreCh = channel('apm:google-cloud-pubsub:message:store') + +// WeakMap for passing context between LeaseManager._dispense and LeaseManager.remove +const messageContexts = new WeakMap() + const publisherMethods = [ 'createTopic', 'updateTopic', @@ -65,6 +80,21 @@ function wrapMethod (method) { if (!requestStartCh.hasSubscribers) return method.apply(this, arguments) const ctx = { request, api, projectId: this.auth._cachedProjectId } + + /** + * For acknowledge/modifyAckDeadline: retrieve stored context from consumer plugin. + * These APIs only have ackIds (no Message objects), so async context is lost. + * Plugin sets ctx.storedContext, which client.js uses to link the acknowledge span. + */ + const isAckOperation = api === 'acknowledge' || api === 'modifyAckDeadline' + if (isAckOperation && request?.ackIds?.length > 0) { + messageAckRetrieveCh.publish({ + ackIds: request.ackIds, + api, + ctx + }) + } + return requestStartCh.runStores(ctx, () => { const cb = arguments[arguments.length - 1] @@ -74,12 +104,11 @@ function wrapMethod (method) { ctx.error = error requestErrorCh.publish(ctx) } - return requestFinishCh.runStores(ctx, cb, this, ...arguments) }) - return method.apply(this, arguments) } + return method.apply(this, arguments) .then( response => { @@ -108,12 +137,16 @@ function massWrap (obj, methods, wrapper) { addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'] }, (obj) => { const Subscription = obj.Subscription + /** + * PULL: Intercept 'message' events and propagate async context via runStores. + * This ensures the consumer plugin creates spans in the correct context. + */ shimmer.wrap(Subscription.prototype, 'emit', emit => function (eventName, message) { if (eventName !== 'message' || !message) return emit.apply(this, arguments) - const ctx = {} + const ctx = { message } try { - return emit.apply(this, arguments) + return messageStoreCh.runStores(ctx, emit, this, ...arguments) } catch (err) { ctx.error = err receiveErrorCh.publish(ctx) @@ -124,26 +157,70 @@ addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'] }, (obj) => { return obj }) +addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'], file: 'build/src/subscriber.js' }, (obj) => { + const Message = obj.Message + + /** + * PULL: Capture async context when message.ack() is called. + * This is our last chance before context is lost. The acknowledge() API call happens + * later (often batched), and we'll retrieve this stored context to link spans. + * Flow: message.ack() -> store context -> acknowledge() API -> retrieve context + */ + if (Message?.prototype?.ack) { + shimmer.wrap(Message.prototype, 'ack', originalAck => function () { + if (this.ackId) { + const ctx = { + message: this, + ackId: this.ackId + } + + return messageAckStoreCh.runStores(ctx, originalAck, this, ...arguments) + } + + return originalAck.apply(this, arguments) + }) + } + + return obj +}) + +/** + * PULL: Hook LeaseManager to track message lifecycle (dispense/remove/clear). + * _dispense: Message given to handler -> create span + * remove: Message removed from lease (ack/nack/timeout) -> finish span + * clear: Subscription closed -> finish all remaining spans + */ addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'], file: 'build/src/lease-manager.js' }, (obj) => { const LeaseManager = obj.LeaseManager - const ctx = {} + if (!LeaseManager) { + return obj + } shimmer.wrap(LeaseManager.prototype, '_dispense', dispense => function (message) { - if (receiveStartCh.hasSubscribers) { - ctx.message = message - return receiveStartCh.runStores(ctx, dispense, this, ...arguments) - } - return dispense.apply(this, arguments) + const ctx = { message } + messageContexts.set(message, ctx) + + return receiveStartCh.runStores(ctx, dispense, this, ...arguments) }) shimmer.wrap(LeaseManager.prototype, 'remove', remove => function (message) { - return receiveFinishCh.runStores(ctx, remove, this, ...arguments) + const ctx = messageContexts.get(message) + if (ctx) { + messageContexts.delete(message) + } + + return receiveFinishCh.runStores(ctx || { message }, remove, this, ...arguments) }) shimmer.wrap(LeaseManager.prototype, 'clear', clear => function () { - for (const message of this._messages) { - ctx.message = message - receiveFinishCh.publish(ctx) + if (this._messages) { + for (const message of this._messages.values()) { + const ctx = messageContexts.get(message) + if (ctx) { + receiveFinishCh.publish(ctx) + messageContexts.delete(message) + } + } } return clear.apply(this, arguments) }) @@ -151,6 +228,51 @@ addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'], file: 'build/src/le return obj }) +/** + * Inject trace context into individual messages via Topic.publish()/publishMessage(). + * Flow: User calls topic.publish() -> inject context (here) -> SDK batches messages -> + * publish() API called -> producer plugin creates batch span + metadata + */ +addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'] }, (obj) => { + if (!obj.Topic?.prototype) return obj + + if (typeof obj.Topic.prototype.publishMessage === 'function') { + shimmer.wrap(obj.Topic.prototype, 'publishMessage', publishMessage => { + return function (data, attributesOrCallback, callback) { + if (data && typeof data === 'object') { + if (!data.attributes) data.attributes = {} + messagePublishCh.publish({ + attributes: data.attributes, + pubsub: this.pubsub, + topicName: this.name + }) + } + return publishMessage.apply(this, arguments) + } + }) + } + + if (typeof obj.Topic.prototype.publish === 'function') { + shimmer.wrap(obj.Topic.prototype, 'publish', publish => function (buffer, attributesOrCallback, callback) { + if (typeof attributesOrCallback === 'function' || !attributesOrCallback) { + arguments[1] = {} + arguments[2] = attributesOrCallback + } + + messagePublishCh.publish({ + attributes: arguments[1], + pubsub: this.pubsub, + topicName: this.name, + buffer + }) + + return publish.apply(this, arguments) + }) + } + + return obj +}) + addHook({ name: '@google-cloud/pubsub', versions: ['>=1.2'] }, (obj) => { const { PublisherClient, SchemaServiceClient, SubscriberClient } = obj.v1 diff --git a/packages/datadog-instrumentations/src/google-cloud-vertexai.js b/packages/datadog-instrumentations/src/google-cloud-vertexai.js index af9c9440235..1c43eb5cf2e 100644 --- a/packages/datadog-instrumentations/src/google-cloud-vertexai.js +++ b/packages/datadog-instrumentations/src/google-cloud-vertexai.js @@ -1,9 +1,8 @@ 'use strict' -const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') - const vertexaiTracingChannel = require('dc-polyfill').tracingChannel('apm:vertexai:request') +const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') function wrapGenerate (generate) { return function (request) { diff --git a/packages/datadog-instrumentations/src/google-genai.js b/packages/datadog-instrumentations/src/google-genai.js index 5943448f255..2aee0cbae7a 100644 --- a/packages/datadog-instrumentations/src/google-genai.js +++ b/packages/datadog-instrumentations/src/google-genai.js @@ -1,9 +1,9 @@ 'use strict' -const { addHook } = require('./helpers/instrument') +const { channel, tracingChannel } = require('dc-polyfill') + const shimmer = require('../../datadog-shimmer') -const tracingChannel = require('dc-polyfill').tracingChannel -const channel = require('dc-polyfill').channel +const { addHook } = require('./helpers/instrument') const genaiTracingChannel = tracingChannel('apm:google:genai:request') const onStreamedChunkCh = channel('apm:google:genai:request:chunk') diff --git a/packages/datadog-instrumentations/src/graphql.js b/packages/datadog-instrumentations/src/graphql.js index 88f23e11291..cc2feaa64da 100644 --- a/packages/datadog-instrumentations/src/graphql.js +++ b/packages/datadog-instrumentations/src/graphql.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const ddGlobal = globalThis[Symbol.for('dd-trace')] ??= {} diff --git a/packages/datadog-instrumentations/src/grpc/client.js b/packages/datadog-instrumentations/src/grpc/client.js index d217872b0a4..78f4dabc88a 100644 --- a/packages/datadog-instrumentations/src/grpc/client.js +++ b/packages/datadog-instrumentations/src/grpc/client.js @@ -1,8 +1,8 @@ 'use strict' -const types = require('./types') const { addHook, channel } = require('../helpers/instrument') const shimmer = require('../../../datadog-shimmer') +const types = require('./types') const patched = new WeakSet() const instances = new WeakMap() diff --git a/packages/datadog-instrumentations/src/grpc/server.js b/packages/datadog-instrumentations/src/grpc/server.js index 1fb5fb2542c..ff8f5f40b01 100644 --- a/packages/datadog-instrumentations/src/grpc/server.js +++ b/packages/datadog-instrumentations/src/grpc/server.js @@ -1,8 +1,8 @@ 'use strict' -const types = require('./types') const { channel, addHook } = require('../helpers/instrument') const shimmer = require('../../../datadog-shimmer') +const types = require('./types') const startChannel = channel('apm:grpc:server:request:start') const asyncStartChannel = channel('apm:grpc:server:request:asyncStart') diff --git a/packages/datadog-instrumentations/src/helpers/bundler-register.js b/packages/datadog-instrumentations/src/helpers/bundler-register.js index 422d035127a..27088767c93 100644 --- a/packages/datadog-instrumentations/src/helpers/bundler-register.js +++ b/packages/datadog-instrumentations/src/helpers/bundler-register.js @@ -2,6 +2,7 @@ const dc = require('dc-polyfill') +const log = require('../../../dd-trace/src/log') const { filename, loadChannel, @@ -9,7 +10,6 @@ const { } = require('./register.js') const hooks = require('./hooks') const instrumentations = require('./instrumentations') -const log = require('../../../dd-trace/src/log') const CHANNEL = 'dd-trace:bundler:load' diff --git a/packages/datadog-instrumentations/src/helpers/hook.js b/packages/datadog-instrumentations/src/helpers/hook.js index dc3de38b301..1d8f0a44b1c 100644 --- a/packages/datadog-instrumentations/src/helpers/hook.js +++ b/packages/datadog-instrumentations/src/helpers/hook.js @@ -1,6 +1,6 @@ 'use strict' -const iitm = require('../../../dd-trace/src/iitm') const path = require('path') +const iitm = require('../../../dd-trace/src/iitm') const ritm = require('../../../dd-trace/src/ritm') /** diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 6006cce3c34..472285a88e4 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -29,7 +29,8 @@ module.exports = { '@node-redis/client': () => require('../redis'), '@opensearch-project/opensearch': () => require('../opensearch'), '@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'), - '@prisma/client': () => require('../prisma'), + '@prisma/client': { esmFirst: true, fn: () => require('../prisma') }, + './runtime/library.js': () => require('../prisma'), '@redis/client': () => require('../redis'), '@smithy/smithy-client': () => require('../aws-sdk'), '@vitest/runner': { esmFirst: true, fn: () => require('../vitest') }, @@ -41,6 +42,7 @@ module.exports = { 'aws-sdk': () => require('../aws-sdk'), bluebird: () => require('../bluebird'), 'body-parser': () => require('../body-parser'), + bullmq: () => require('../bullmq'), bunyan: () => require('../bunyan'), 'cassandra-driver': () => require('../cassandra-driver'), child_process: () => require('../child_process'), @@ -83,6 +85,7 @@ module.exports = { kafkajs: () => require('../kafkajs'), langchain: () => require('../langchain'), ldapjs: () => require('../ldapjs'), + 'light-my-request': () => require('../light-my-request'), 'limitd-client': () => require('../limitd-client'), lodash: () => require('../lodash'), mariadb: () => require('../mariadb'), diff --git a/packages/datadog-instrumentations/src/helpers/instrument.js b/packages/datadog-instrumentations/src/helpers/instrument.js index 477cde9fca1..6c7c493233e 100644 --- a/packages/datadog-instrumentations/src/helpers/instrument.js +++ b/packages/datadog-instrumentations/src/helpers/instrument.js @@ -1,9 +1,9 @@ 'use strict' +const { AsyncResource } = require('async_hooks') const dc = require('dc-polyfill') const instrumentations = require('./instrumentations') const rewriterInstrumentations = require('./rewriter/instrumentations') -const { AsyncResource } = require('async_hooks') const channelMap = {} exports.channel = function (name) { diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 85d588395cd..d4366795b97 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -1,15 +1,16 @@ 'use strict' -const { channel } = require('dc-polyfill') const path = require('path') +const { channel } = require('dc-polyfill') const satisfies = require('../../../../vendor/dist/semifies') -const Hook = require('./hook') const requirePackageJson = require('../../../dd-trace/src/require-package-json') const log = require('../../../dd-trace/src/log') -const checkRequireCache = require('./check-require-cache') const telemetry = require('../../../dd-trace/src/guardrails/telemetry') const { isInServerlessEnvironment } = require('../../../dd-trace/src/serverless') const { getEnvironmentVariables } = require('../../../dd-trace/src/config-helper') +const checkRequireCache = require('./check-require-cache') +const Hook = require('./hook') +const { isRelativeRequire } = require('./shared-utils') const rewriter = require('./rewriter') const envs = getEnvironmentVariables() @@ -104,6 +105,8 @@ for (const packageName of names) { hook[HOOK_SYMBOL] ??= new WeakSet() let matchesFile = moduleName === fullFilename + if (!matchesFile && isRelativeRequire(name)) matchesFile = true + if (fullFilePattern) { // Some libraries include a hash in their filenames when installed, // so our instrumentation has to include a '.*' to match them for more than a single version. @@ -219,7 +222,6 @@ function parseHookInstrumentationFileName (packageName) { hook = hook.fn } const hookString = hook.toString() - const regex = /require\('([^']*)'\)/ const match = hookString.match(regex) diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/index.js b/packages/datadog-instrumentations/src/helpers/rewriter/index.js index af8a8ab01cc..73031d0b6e9 100644 --- a/packages/datadog-instrumentations/src/helpers/rewriter/index.js +++ b/packages/datadog-instrumentations/src/helpers/rewriter/index.js @@ -34,11 +34,11 @@ Orchestrion-JS that will need to be backported: const { readFileSync } = require('fs') const { join } = require('path') const semifies = require('../../../../../vendor/dist/semifies') +const log = require('../../../../dd-trace/src/log') +const { getEnvironmentVariable } = require('../../../../dd-trace/src/config-helper') const transforms = require('./transforms') const { generate, parse, traverse } = require('./compiler') -const log = require('../../../../dd-trace/src/log') const instrumentations = require('./instrumentations') -const { getEnvironmentVariable } = require('../../../../dd-trace/src/config-helper') const NODE_OPTIONS = getEnvironmentVariable('NODE_OPTIONS') diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json new file mode 100644 index 00000000000..62671b7b87a --- /dev/null +++ b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json @@ -0,0 +1,106 @@ +[ + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/cjs/classes/queue.js" + }, + "functionQuery": { + "methodName": "add", + "className": "Queue", + "kind": "Async" + }, + "channelName": "Queue_add" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/cjs/classes/queue.js" + }, + "functionQuery": { + "methodName": "addBulk", + "className": "Queue", + "kind": "Async" + }, + "channelName": "Queue_addBulk" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/cjs/classes/worker.js" + }, + "functionQuery": { + "methodName": "callProcessJob", + "className": "Worker", + "kind": "Async" + }, + "channelName": "Worker_callProcessJob" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/cjs/classes/flow-producer.js" + }, + "functionQuery": { + "methodName": "add", + "className": "FlowProducer", + "kind": "Async" + }, + "channelName": "FlowProducer_add" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/esm/classes/queue.js" + }, + "functionQuery": { + "methodName": "add", + "className": "Queue", + "kind": "Async" + }, + "channelName": "Queue_add" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/esm/classes/queue.js" + }, + "functionQuery": { + "methodName": "addBulk", + "className": "Queue", + "kind": "Async" + }, + "channelName": "Queue_addBulk" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/esm/classes/worker.js" + }, + "functionQuery": { + "methodName": "callProcessJob", + "className": "Worker", + "kind": "Async" + }, + "channelName": "Worker_callProcessJob" + }, + { + "module": { + "name": "bullmq", + "versionRange": ">=5.66.0", + "filePath": "dist/esm/classes/flow-producer.js" + }, + "functionQuery": { + "methodName": "add", + "className": "FlowProducer", + "kind": "Async" + }, + "channelName": "FlowProducer_add" + } +] diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js index 70d684c4eea..74a57881adb 100644 --- a/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +++ b/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js @@ -1,5 +1,6 @@ 'use strict' module.exports = [ - ...require('./langchain') + ...require('./langchain'), + ...require('./bullmq') ] diff --git a/packages/datadog-instrumentations/src/helpers/router-helper.js b/packages/datadog-instrumentations/src/helpers/router-helper.js index d42b450ba42..aebe2771939 100644 --- a/packages/datadog-instrumentations/src/helpers/router-helper.js +++ b/packages/datadog-instrumentations/src/helpers/router-helper.js @@ -1,7 +1,7 @@ 'use strict' -const { channel } = require('./instrument') const shimmer = require('../../../datadog-shimmer') +const { channel } = require('./instrument') const routerMountPaths = new WeakMap() // to track mount paths for router instances const layerMatchers = new WeakMap() // to store layer matchers diff --git a/packages/datadog-instrumentations/src/helpers/shared-utils.js b/packages/datadog-instrumentations/src/helpers/shared-utils.js new file mode 100644 index 00000000000..471089eb2d8 --- /dev/null +++ b/packages/datadog-instrumentations/src/helpers/shared-utils.js @@ -0,0 +1,9 @@ +'use strict' + +function isRelativeRequire (moduleName) { + return moduleName.startsWith('./') || moduleName.startsWith('../') +} + +module.exports = { + isRelativeRequire +} diff --git a/packages/datadog-instrumentations/src/hono.js b/packages/datadog-instrumentations/src/hono.js index 7f1a8d40f04..233cca55f56 100644 --- a/packages/datadog-instrumentations/src/hono.js +++ b/packages/datadog-instrumentations/src/hono.js @@ -9,6 +9,10 @@ const { const routeChannel = channel('apm:hono:request:route') const handleChannel = channel('apm:hono:request:handle') const errorChannel = channel('apm:hono:request:error') +const nextChannel = channel('apm:hono:middleware:next') +const enterChannel = channel('apm:hono:middleware:enter') +const exitChannel = channel('apm:hono:middleware:exit') +const finishChannel = channel('apm:hono:middleware:finish') function wrapFetch (fetch) { return function (request, env, executionCtx) { @@ -34,21 +38,62 @@ function wrapCompose (compose) { const instrumentedMiddlewares = middlewares.map(h => { const [[fn, meta], params] = h - - const instrumentedFn = (...args) => { - const context = args[0] - routeChannel.publish({ - req: context.env.incoming, - route: meta?.path - }) - return fn(...args) - } - return [[instrumentedFn, meta], params] + return [[wrapMiddleware(fn, meta?.path), meta], params] }) return compose.call(this, instrumentedMiddlewares, instrumentedOnError, onNotFound) } } +function wrapNext (req, route, next) { + return shimmer.wrapFunction( + next, + (next) => + function () { + nextChannel.publish({ req, route }) + + return next.apply(this, arguments) + } + ) +} + +function wrapMiddleware (middleware, route) { + const name = middleware.name + return shimmer.wrapFunction( + middleware, + (middleware) => + function (context, next) { + const req = context.env.incoming + routeChannel.publish({ req, route }) + enterChannel.publish({ req, name, route }) + if (typeof next === 'function') { + arguments[1] = wrapNext(req, route, next) + } + try { + const result = middleware.apply(this, arguments) + if (result && typeof result.then === 'function') { + return result.then( + (result) => { + finishChannel.publish({ req }) + return result + }, + (error) => { + errorChannel.publish({ req, error }) + throw error + } + ) + } + finishChannel.publish({ req }) + return result + } catch (error) { + errorChannel.publish({ req, error }) + throw error + } finally { + exitChannel.publish({ req, route }) + } + } + ) +} + addHook({ name: 'hono', versions: ['>=4'], diff --git a/packages/datadog-instrumentations/src/ioredis.js b/packages/datadog-instrumentations/src/ioredis.js index a39aacc9940..1e0e372733e 100644 --- a/packages/datadog-instrumentations/src/ioredis.js +++ b/packages/datadog-instrumentations/src/ioredis.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:ioredis:command:start') const finishCh = channel('apm:ioredis:command:finish') diff --git a/packages/datadog-instrumentations/src/iovalkey.js b/packages/datadog-instrumentations/src/iovalkey.js index 413e22e6fc5..22c78b668e9 100644 --- a/packages/datadog-instrumentations/src/iovalkey.js +++ b/packages/datadog-instrumentations/src/iovalkey.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:iovalkey:command:start') const finishCh = channel('apm:iovalkey:command:finish') diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 95db563d24d..8948f8707b8 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -1,9 +1,8 @@ 'use strict' -const { addHook, channel } = require('./helpers/instrument') +const path = require('path') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') -const path = require('path') const { getCoveredFilenamesFromCoverage, JEST_WORKER_TRACE_PAYLOAD_CODE, @@ -21,6 +20,7 @@ const { getJestTestName, getJestSuitesToRun } = require('../../datadog-plugin-jest/src/util') +const { addHook, channel } = require('./helpers/instrument') const testSessionStartCh = channel('ci:jest:session:start') const testSessionFinishCh = channel('ci:jest:session:finish') diff --git a/packages/datadog-instrumentations/src/kafkajs.js b/packages/datadog-instrumentations/src/kafkajs.js index 430e1e9d0ca..1215ac0ae3d 100644 --- a/packages/datadog-instrumentations/src/kafkajs.js +++ b/packages/datadog-instrumentations/src/kafkajs.js @@ -1,12 +1,12 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') + +const log = require('../../dd-trace/src/log') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') - -const log = require('../../dd-trace/src/log') const producerStartCh = channel('apm:kafkajs:produce:start') const producerCommitCh = channel('apm:kafkajs:produce:commit') diff --git a/packages/datadog-instrumentations/src/knex.js b/packages/datadog-instrumentations/src/knex.js index 7f6c2f3c896..a6d7323a2e0 100644 --- a/packages/datadog-instrumentations/src/knex.js +++ b/packages/datadog-instrumentations/src/knex.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') const startRawQueryCh = channel('datadog:knex:raw:start') const rawQuerySubscribes = channel('datadog:knex:raw:subscribes') diff --git a/packages/datadog-instrumentations/src/ldapjs.js b/packages/datadog-instrumentations/src/ldapjs.js index fa35348e161..2710ac95e26 100644 --- a/packages/datadog-instrumentations/src/ldapjs.js +++ b/packages/datadog-instrumentations/src/ldapjs.js @@ -1,11 +1,11 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook, AsyncResource } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') function isString (value) { // eslint-disable-next-line unicorn/no-instanceof-builtins diff --git a/packages/datadog-instrumentations/src/light-my-request.js b/packages/datadog-instrumentations/src/light-my-request.js new file mode 100644 index 00000000000..1f3dbcb5e9b --- /dev/null +++ b/packages/datadog-instrumentations/src/light-my-request.js @@ -0,0 +1,93 @@ +'use strict' + +/** + * light-my-request instrumentation + * + * This instrumentation enables dd-trace to capture spans for Fastify inject() calls + * which use light-my-request internally. Without this, inject() bypasses the HTTP + * server instrumentation since it doesn't go through http.Server.emit('request'). + * + * This is critical for platforms like Platformatic that use undici-thread-interceptor + * to route requests between worker threads using Fastify inject(). + */ + +const shimmer = require('../../datadog-shimmer') +const { + channel, + addHook +} = require('./helpers/instrument') + +// Reuse the same channels as HTTP server instrumentation +const startServerCh = channel('apm:http:server:request:start') +const exitServerCh = channel('apm:http:server:request:exit') +const errorServerCh = channel('apm:http:server:request:error') +const finishServerCh = channel('apm:http:server:request:finish') + +addHook({ name: 'light-my-request', versions: ['>=3'] }, (lightMyRequest) => { + // Wrap the inject function + return shimmer.wrapFunction(lightMyRequest, lightMyRequest => { + return function wrappedInject (dispatchFunc, options, callback) { + // If no subscribers, use original behavior + if (!startServerCh.hasSubscribers) { + return lightMyRequest.apply(this, arguments) + } + + // Wrap the dispatch function to add tracing + const wrappedDispatch = wrapDispatchFunc(dispatchFunc) + + // Call original with wrapped dispatch + return lightMyRequest.call(this, wrappedDispatch, options, callback) + } + }) +}) + +function wrapDispatchFunc (dispatchFunc) { + return function tracedDispatch (req, res) { + const abortController = new AbortController() + + // Link res.req like HTTP instrumentation does + res.req = req + + // Publish start event (same as HTTP server) + startServerCh.publish({ req, res, abortController }) + + // Track when response finishes via 'finish' event (like HTTP instrumentation) + let finishCalled = false + const onFinish = () => { + if (!finishCalled) { + finishCalled = true + finishServerCh.publish({ req }) + } + } + + // light-my-request Response emits 'finish' when done + if (res.on && typeof res.on === 'function') { + res.on('finish', onFinish) + res.on('close', onFinish) + } + + // Also wrap end() as fallback + const originalEnd = res.end + if (originalEnd) { + res.end = function wrappedEnd () { + const result = originalEnd.apply(this, arguments) + // Trigger finish if events don't fire + setImmediate(onFinish) + return result + } + } + + try { + if (abortController.signal.aborted) { + return + } + + return dispatchFunc.call(this, req, res) + } catch (err) { + errorServerCh.publish(err) + throw err + } finally { + exitServerCh.publish({ req }) + } + } +} diff --git a/packages/datadog-instrumentations/src/limitd-client.js b/packages/datadog-instrumentations/src/limitd-client.js index e0684c406b6..621fcbf80bf 100644 --- a/packages/datadog-instrumentations/src/limitd-client.js +++ b/packages/datadog-instrumentations/src/limitd-client.js @@ -1,7 +1,7 @@ 'use strict' -const { addHook, AsyncResource } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { addHook, AsyncResource } = require('./helpers/instrument') function wrapRequest (original) { return function () { diff --git a/packages/datadog-instrumentations/src/lodash.js b/packages/datadog-instrumentations/src/lodash.js index 2ac9a1afd64..bda8073b814 100644 --- a/packages/datadog-instrumentations/src/lodash.js +++ b/packages/datadog-instrumentations/src/lodash.js @@ -1,8 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') - const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') addHook({ name: 'lodash', versions: ['>=4'] }, lodash => { const lodashOperationCh = channel('datadog:lodash:operation') diff --git a/packages/datadog-instrumentations/src/mariadb.js b/packages/datadog-instrumentations/src/mariadb.js index bd3bb1334f2..238fd606ec6 100644 --- a/packages/datadog-instrumentations/src/mariadb.js +++ b/packages/datadog-instrumentations/src/mariadb.js @@ -1,8 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') - const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const commandAddCh = channel('apm:mariadb:command:add') const connectionStartCh = channel('apm:mariadb:connection:start') diff --git a/packages/datadog-instrumentations/src/memcached.js b/packages/datadog-instrumentations/src/memcached.js index 96b10d2e293..f4d5a7a3b2a 100644 --- a/packages/datadog-instrumentations/src/memcached.js +++ b/packages/datadog-instrumentations/src/memcached.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'memcached', versions: ['>=2.2'] }, Memcached => { const startCh = channel('apm:memcached:command:start') diff --git a/packages/datadog-instrumentations/src/mongodb-core.js b/packages/datadog-instrumentations/src/mongodb-core.js index b6f187ae542..5c6e7fb67ea 100644 --- a/packages/datadog-instrumentations/src/mongodb-core.js +++ b/packages/datadog-instrumentations/src/mongodb-core.js @@ -1,11 +1,11 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook, AsyncResource } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:mongodb:query:start') const finishCh = channel('apm:mongodb:query:finish') diff --git a/packages/datadog-instrumentations/src/mongodb.js b/packages/datadog-instrumentations/src/mongodb.js index dcc1b2ca96b..a24d2be1455 100644 --- a/packages/datadog-instrumentations/src/mongodb.js +++ b/packages/datadog-instrumentations/src/mongodb.js @@ -2,11 +2,11 @@ require('./mongodb-core') +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') // collection methods with filter const collectionMethodsWithFilter = [ diff --git a/packages/datadog-instrumentations/src/mongoose.js b/packages/datadog-instrumentations/src/mongoose.js index 8f95f492ebe..3739d9fce1a 100644 --- a/packages/datadog-instrumentations/src/mongoose.js +++ b/packages/datadog-instrumentations/src/mongoose.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') const startCh = channel('datadog:mongoose:model:filter:start') const finishCh = channel('datadog:mongoose:model:filter:finish') diff --git a/packages/datadog-instrumentations/src/mquery.js b/packages/datadog-instrumentations/src/mquery.js index c300736f3fe..282a26461b3 100644 --- a/packages/datadog-instrumentations/src/mquery.js +++ b/packages/datadog-instrumentations/src/mquery.js @@ -1,11 +1,11 @@ 'use strict' const dc = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const prepareCh = channel('datadog:mquery:filter:prepare') const tracingCh = dc.tracingChannel('datadog:mquery:filter') diff --git a/packages/datadog-instrumentations/src/mysql.js b/packages/datadog-instrumentations/src/mysql.js index 5710b81d34a..ce072dbd7c7 100644 --- a/packages/datadog-instrumentations/src/mysql.js +++ b/packages/datadog-instrumentations/src/mysql.js @@ -1,7 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connection => { const startCh = channel('apm:mysql:query:start') diff --git a/packages/datadog-instrumentations/src/mysql2.js b/packages/datadog-instrumentations/src/mysql2.js index 4616c3f6085..6644d87eb36 100644 --- a/packages/datadog-instrumentations/src/mysql2.js +++ b/packages/datadog-instrumentations/src/mysql2.js @@ -2,9 +2,9 @@ const { errorMonitor } = require('node:events') -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const satisfies = require('../../../vendor/dist/semifies') +const { channel, addHook } = require('./helpers/instrument') function wrapConnection (Connection, version) { const startCh = channel('apm:mysql2:query:start') diff --git a/packages/datadog-instrumentations/src/net.js b/packages/datadog-instrumentations/src/net.js index b0b6163efc7..6425f247afe 100644 --- a/packages/datadog-instrumentations/src/net.js +++ b/packages/datadog-instrumentations/src/net.js @@ -2,8 +2,8 @@ const { errorMonitor } = require('events') -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const startICPCh = channel('apm:net:ipc:start') const finishICPCh = channel('apm:net:ipc:finish') diff --git a/packages/datadog-instrumentations/src/next.js b/packages/datadog-instrumentations/src/next.js index fe35fc18420..7d9ceca6ff0 100644 --- a/packages/datadog-instrumentations/src/next.js +++ b/packages/datadog-instrumentations/src/next.js @@ -1,7 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const startChannel = channel('apm:next:request:start') const finishChannel = channel('apm:next:request:finish') diff --git a/packages/datadog-instrumentations/src/nyc.js b/packages/datadog-instrumentations/src/nyc.js index 4459c6e36a7..4388b4cb0c8 100644 --- a/packages/datadog-instrumentations/src/nyc.js +++ b/packages/datadog-instrumentations/src/nyc.js @@ -1,8 +1,8 @@ 'use strict' -const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const { addHook, channel } = require('./helpers/instrument') const codeCoverageWrapCh = channel('ci:nyc:wrap') diff --git a/packages/datadog-instrumentations/src/openai.js b/packages/datadog-instrumentations/src/openai.js index 4d1540b6193..99550ee2928 100644 --- a/packages/datadog-instrumentations/src/openai.js +++ b/packages/datadog-instrumentations/src/openai.js @@ -1,9 +1,9 @@ 'use strict' -const { addHook } = require('./helpers/instrument') +const dc = require('dc-polyfill') const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') -const dc = require('dc-polyfill') const ch = dc.tracingChannel('apm:openai:request') const onStreamedChunkCh = dc.channel('apm:openai:request:chunk') diff --git a/packages/datadog-instrumentations/src/opensearch.js b/packages/datadog-instrumentations/src/opensearch.js index 9bb8d6aa064..982f62d7556 100644 --- a/packages/datadog-instrumentations/src/opensearch.js +++ b/packages/datadog-instrumentations/src/opensearch.js @@ -1,7 +1,7 @@ 'use strict' -const { addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { addHook } = require('./helpers/instrument') const { createWrapRequest, createWrapGetConnection } = require('./elasticsearch') addHook({ name: '@opensearch-project/opensearch', file: 'lib/Transport.js', versions: ['>=1'] }, Transport => { diff --git a/packages/datadog-instrumentations/src/oracledb.js b/packages/datadog-instrumentations/src/oracledb.js index 655cb4ee001..738a27cce1c 100644 --- a/packages/datadog-instrumentations/src/oracledb.js +++ b/packages/datadog-instrumentations/src/oracledb.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const connectionAttributes = new WeakMap() const poolAttributes = new WeakMap() diff --git a/packages/datadog-instrumentations/src/otel-sdk-trace.js b/packages/datadog-instrumentations/src/otel-sdk-trace.js index 90c9f30f475..fb50755b4fd 100644 --- a/packages/datadog-instrumentations/src/otel-sdk-trace.js +++ b/packages/datadog-instrumentations/src/otel-sdk-trace.js @@ -1,9 +1,9 @@ 'use strict' -const { addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const tracer = require('../../dd-trace') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const { addHook } = require('./helpers/instrument') const otelSdkEnabled = getEnvironmentVariable('DD_TRACE_OTEL_ENABLED') || getEnvironmentVariable('OTEL_SDK_DISABLED') diff --git a/packages/datadog-instrumentations/src/pg.js b/packages/datadog-instrumentations/src/pg.js index 1dca93d5ed3..442a33f65f3 100644 --- a/packages/datadog-instrumentations/src/pg.js +++ b/packages/datadog-instrumentations/src/pg.js @@ -1,10 +1,12 @@ 'use strict' +const { errorMonitor } = require('node:events') + +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:pg:query:start') const finishCh = channel('apm:pg:query:finish') @@ -13,8 +15,6 @@ const errorCh = channel('apm:pg:query:error') const startPoolQueryCh = channel('datadog:pg:pool:query:start') const finishPoolQueryCh = channel('datadog:pg:pool:query:finish') -const { errorMonitor } = require('node:events') - addHook({ name: 'pg', versions: ['>=8.0.3'], file: 'lib/native/client.js' }, Client => { shimmer.wrap(Client.prototype, 'query', query => wrapQuery(query)) return Client diff --git a/packages/datadog-instrumentations/src/pino.js b/packages/datadog-instrumentations/src/pino.js index 958e8c858c8..c8b3cc89711 100644 --- a/packages/datadog-instrumentations/src/pino.js +++ b/packages/datadog-instrumentations/src/pino.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') function wrapPino (symbol, wrapper, pino) { return function pinoWithTrace () { diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index 3c4d1957e43..e419f5890de 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -2,7 +2,6 @@ const satisfies = require('../../../vendor/dist/semifies') -const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const { parseAnnotations, @@ -15,6 +14,7 @@ const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const { DD_MAJOR } = require('../../../version') +const { addHook, channel } = require('./helpers/instrument') const testStartCh = channel('ci:playwright:test:start') const testFinishCh = channel('ci:playwright:test:finish') diff --git a/packages/datadog-instrumentations/src/prisma.js b/packages/datadog-instrumentations/src/prisma.js index ab2b79e0323..8c5a564e3bc 100644 --- a/packages/datadog-instrumentations/src/prisma.js +++ b/packages/datadog-instrumentations/src/prisma.js @@ -1,12 +1,9 @@ 'use strict' -const { - channel, - addHook -} = require('./helpers/instrument') +const tracingChannel = require('dc-polyfill').tracingChannel +const { channel, addHook } = require('./helpers/instrument') const prismaEngineStart = channel('apm:prisma:engine:start') -const tracingChannel = require('dc-polyfill').tracingChannel const clientCH = tracingChannel('apm:prisma:client') const allowedClientSpanOperations = new Set([ @@ -15,14 +12,20 @@ const allowedClientSpanOperations = new Set([ 'transaction' ]) -class TracingHelper { +class DatadogTracingHelper { dbConfig = null + + constructor (dbConfig = null) { + this.dbConfig = dbConfig + } + isEnabled () { return true } // needs a sampled tracecontext to generate engine spans getTraceParent (context) { + // TODO: Fix the id return '00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01' // valid sampled traceparent } @@ -34,7 +37,9 @@ class TracingHelper { } } - getActiveContext () {} + getActiveContext () { + // TODO: Fix context + } runInChildSpan (options, callback) { if (typeof options === 'string') { @@ -63,42 +68,52 @@ class TracingHelper { } } -addHook({ name: '@prisma/client', versions: ['>=6.1.0'] }, (prisma, version) => { - const tracingHelper = new TracingHelper() - - // we need to patch the prototype to get db config since this works for ESM and CJS alike. - const originalRequest = prisma.PrismaClient.prototype._request - prisma.PrismaClient.prototype._request = function () { - if (!tracingHelper.dbConfig) { - const inlineDatasources = this._engine?.config.inlineDatasources - const overrideDatasources = this._engine?.config.overrideDatasources - const datasources = inlineDatasources?.db.url?.value ?? overrideDatasources?.db?.url - if (datasources) { - const result = parseDBString(datasources) - tracingHelper.setDbString(result) +const prismaHook = (runtime, versions, name, isIitm) => { + const originalGetPrismaClient = runtime.getPrismaClient + + if (!originalGetPrismaClient) return runtime + const datadogTracingHelper = new DatadogTracingHelper() + + const wrappedGetPrismaClient = function (config) { + const datasources = config.inlineDatasources?.db.url?.value + if (datasources) { + const dbConfig = parseDBString(datasources) + datadogTracingHelper.setDbString(dbConfig) + } + + const PrismaClient = originalGetPrismaClient.call(this, config) + return class WrappedPrismaClientClass extends PrismaClient { + constructor (clientConfig) { + super(clientConfig) + this._tracingHelper = datadogTracingHelper + this._engine.tracingHelper = datadogTracingHelper } } - return originalRequest.apply(this, arguments) } - /* - * This is taking advantage of the built in tracing support from Prisma. - * The below variable is setting a global tracing helper that Prisma uses - * to enable OpenTelemetry. - */ - // https://github.com/prisma/prisma/blob/478293bbfce91e41ceff02f2a0b03bb8acbca03e/packages/instrumentation/src/PrismaInstrumentation.ts#L42 - const versions = version.split('.') - if (versions[0] === '6' && versions[1] < 4) { - global.PRISMA_INSTRUMENTATION = { - helper: tracingHelper - } - } else { - global[`V${versions[0]}_PRISMA_INSTRUMENTATION`] = { - helper: tracingHelper - } + if (isIitm) { + runtime.getPrismaClient = wrappedGetPrismaClient + return runtime } - return prisma + return new Proxy(runtime, { + get (target, prop) { + if (prop === 'getPrismaClient') { + return wrappedGetPrismaClient + } + return target[prop] + } + }) +} + +const prismaConfigs = [ + { name: '@prisma/client', versions: ['>=6.1.0 <7.0.0'], filePattern: 'runtime/library.*' }, + { name: './runtime/library.js', versions: ['>=6.1.0 <7.0.0'], file: 'runtime/library.js' }, + { name: '@prisma/client', versions: ['>=7.0.0'], filePattern: 'runtime/client.*' } +] + +prismaConfigs.forEach(config => { + addHook(config, prismaHook) }) function parseDBString (dbString) { diff --git a/packages/datadog-instrumentations/src/process.js b/packages/datadog-instrumentations/src/process.js index 429b0d8f574..273f0ac25b2 100644 --- a/packages/datadog-instrumentations/src/process.js +++ b/packages/datadog-instrumentations/src/process.js @@ -1,7 +1,7 @@ 'use strict' -const shimmer = require('../../datadog-shimmer') const { channel } = require('dc-polyfill') +const shimmer = require('../../datadog-shimmer') const startSetUncaughtExceptionCaptureCallback = channel('datadog:process:setUncaughtExceptionCaptureCallback:start') diff --git a/packages/datadog-instrumentations/src/promise-js.js b/packages/datadog-instrumentations/src/promise-js.js index e88e2d6af42..b82a6c99184 100644 --- a/packages/datadog-instrumentations/src/promise-js.js +++ b/packages/datadog-instrumentations/src/promise-js.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'promise-js', diff --git a/packages/datadog-instrumentations/src/promise.js b/packages/datadog-instrumentations/src/promise.js index 28c60462546..46b5cad4634 100644 --- a/packages/datadog-instrumentations/src/promise.js +++ b/packages/datadog-instrumentations/src/promise.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'promise', diff --git a/packages/datadog-instrumentations/src/protobufjs.js b/packages/datadog-instrumentations/src/protobufjs.js index c11706582d6..93b5b9f4942 100644 --- a/packages/datadog-instrumentations/src/protobufjs.js +++ b/packages/datadog-instrumentations/src/protobufjs.js @@ -1,9 +1,9 @@ 'use strict' +const dc = require('dc-polyfill') const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const dc = require('dc-polyfill') const serializeChannel = dc.channel('apm:protobufjs:serialize-start') const deserializeChannel = dc.channel('apm:protobufjs:deserialize-end') diff --git a/packages/datadog-instrumentations/src/q.js b/packages/datadog-instrumentations/src/q.js index d96b54bc0e6..5fa6988ddee 100644 --- a/packages/datadog-instrumentations/src/q.js +++ b/packages/datadog-instrumentations/src/q.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'q', diff --git a/packages/datadog-instrumentations/src/redis.js b/packages/datadog-instrumentations/src/redis.js index 2685802d467..20a29e11a18 100644 --- a/packages/datadog-instrumentations/src/redis.js +++ b/packages/datadog-instrumentations/src/redis.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const startCh = channel('apm:redis:command:start') const finishCh = channel('apm:redis:command:finish') diff --git a/packages/datadog-instrumentations/src/rhea.js b/packages/datadog-instrumentations/src/rhea.js index fc190a81b5e..8b7d69543b2 100644 --- a/packages/datadog-instrumentations/src/rhea.js +++ b/packages/datadog-instrumentations/src/rhea.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const circularBufferConstructor = Symbol('circularBufferConstructor') const inFlightDeliveries = Symbol('inFlightDeliveries') diff --git a/packages/datadog-instrumentations/src/selenium.js b/packages/datadog-instrumentations/src/selenium.js index 7904aa7bfe9..4a79b9db6ad 100644 --- a/packages/datadog-instrumentations/src/selenium.js +++ b/packages/datadog-instrumentations/src/selenium.js @@ -1,8 +1,8 @@ 'use strict' -const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const { addHook, channel } = require('./helpers/instrument') const ciSeleniumDriverGetStartCh = channel('ci:selenium:driver:get') diff --git a/packages/datadog-instrumentations/src/sequelize.js b/packages/datadog-instrumentations/src/sequelize.js index 48939654a4e..4582e1e1c3d 100644 --- a/packages/datadog-instrumentations/src/sequelize.js +++ b/packages/datadog-instrumentations/src/sequelize.js @@ -1,12 +1,11 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') - addHook({ name: 'sequelize', versions: ['>=4'], file: ['lib/sequelize.js'] }, Sequelize => { const startCh = channel('datadog:sequelize:query:start') const finishCh = channel('datadog:sequelize:query:finish') diff --git a/packages/datadog-instrumentations/src/sharedb.js b/packages/datadog-instrumentations/src/sharedb.js index a92b628205f..eadb519b400 100644 --- a/packages/datadog-instrumentations/src/sharedb.js +++ b/packages/datadog-instrumentations/src/sharedb.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') /** * @description The enum values in this map are not exposed from ShareDB, so the keys are hard-coded here. diff --git a/packages/datadog-instrumentations/src/tedious.js b/packages/datadog-instrumentations/src/tedious.js index 24179be335f..bca26df2d4b 100644 --- a/packages/datadog-instrumentations/src/tedious.js +++ b/packages/datadog-instrumentations/src/tedious.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'tedious', versions: ['>=1.0.0'] }, tedious => { const startCh = channel('apm:tedious:request:start') diff --git a/packages/datadog-instrumentations/src/undici.js b/packages/datadog-instrumentations/src/undici.js index cd3207ea9c3..8a76757d2c8 100644 --- a/packages/datadog-instrumentations/src/undici.js +++ b/packages/datadog-instrumentations/src/undici.js @@ -1,15 +1,15 @@ 'use strict' +const tracingChannel = require('dc-polyfill').tracingChannel + +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') +const { createWrapFetch } = require('./helpers/fetch') -const tracingChannel = require('dc-polyfill').tracingChannel const ch = tracingChannel('apm:undici:fetch') -const { createWrapFetch } = require('./helpers/fetch') - addHook({ name: 'undici', versions: ['^4.4.1', '5', '>=6.0.0'] diff --git a/packages/datadog-instrumentations/src/url.js b/packages/datadog-instrumentations/src/url.js index 57248099b4e..79552b4b48f 100644 --- a/packages/datadog-instrumentations/src/url.js +++ b/packages/datadog-instrumentations/src/url.js @@ -1,7 +1,7 @@ 'use strict' -const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { addHook, channel } = require('./helpers/instrument') const names = ['url', 'node:url'] const parseFinishedChannel = channel('datadog:url:parse:finish') diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index 1d0ca1b0edd..91d4463ac45 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -1,12 +1,12 @@ 'use strict' -const { addHook, channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') const { VITEST_WORKER_TRACE_PAYLOAD_CODE, VITEST_WORKER_LOGS_PAYLOAD_CODE } = require('../../dd-trace/src/plugins/util/test') +const { addHook, channel } = require('./helpers/instrument') // test hooks const testStartCh = channel('ci:vitest:test:start') diff --git a/packages/datadog-instrumentations/src/vm.js b/packages/datadog-instrumentations/src/vm.js index 9df229556fa..545e797085f 100644 --- a/packages/datadog-instrumentations/src/vm.js +++ b/packages/datadog-instrumentations/src/vm.js @@ -1,7 +1,7 @@ 'use strict' -const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') const names = ['vm', 'node:vm'] const runScriptStartChannel = channel('datadog:vm:run-script:start') diff --git a/packages/datadog-instrumentations/src/when.js b/packages/datadog-instrumentations/src/when.js index 05fc6adbe8a..75ecb30bf8e 100644 --- a/packages/datadog-instrumentations/src/when.js +++ b/packages/datadog-instrumentations/src/when.js @@ -1,8 +1,8 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') const { wrapThen } = require('./helpers/promise') -const shimmer = require('../../datadog-shimmer') addHook({ name: 'when', diff --git a/packages/datadog-instrumentations/src/winston.js b/packages/datadog-instrumentations/src/winston.js index 9b9c4e811aa..911e91598f7 100644 --- a/packages/datadog-instrumentations/src/winston.js +++ b/packages/datadog-instrumentations/src/winston.js @@ -1,10 +1,10 @@ 'use strict' +const shimmer = require('../../datadog-shimmer') const { channel, addHook } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') const patched = new WeakSet() diff --git a/packages/datadog-instrumentations/src/ws.js b/packages/datadog-instrumentations/src/ws.js index 1e41c5c94fe..fe7633a7910 100644 --- a/packages/datadog-instrumentations/src/ws.js +++ b/packages/datadog-instrumentations/src/ws.js @@ -1,12 +1,13 @@ 'use strict' +const tracingChannel = require('dc-polyfill').tracingChannel + +const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') -const tracingChannel = require('dc-polyfill').tracingChannel const serverCh = tracingChannel('ws:server:connect') const producerCh = tracingChannel('ws:send') const receiverCh = tracingChannel('ws:receive') diff --git a/packages/datadog-instrumentations/test/cookie-parser.spec.js b/packages/datadog-instrumentations/test/cookie-parser.spec.js index 838f90e722c..a0827412d36 100644 --- a/packages/datadog-instrumentations/test/cookie-parser.spec.js +++ b/packages/datadog-instrumentations/test/cookie-parser.spec.js @@ -1,12 +1,11 @@ 'use strict' +const assert = require('node:assert') const axios = require('axios') const dc = require('dc-polyfill') const { describe, it, beforeEach, before, after } = require('mocha') const sinon = require('sinon') -const assert = require('node:assert') - const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') diff --git a/packages/datadog-instrumentations/test/express-mongo-sanitize.spec.js b/packages/datadog-instrumentations/test/express-mongo-sanitize.spec.js index 99692597bb8..a1326b268a6 100644 --- a/packages/datadog-instrumentations/test/express-mongo-sanitize.spec.js +++ b/packages/datadog-instrumentations/test/express-mongo-sanitize.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict') const axios = require('axios') -const { expect } = require('chai') + const { channel } = require('dc-polyfill') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') @@ -195,7 +195,7 @@ describe('express-mongo-sanitize', () => { const sanitizedObject = expressMongoSanitize.sanitize(objectToSanitize) assert.strictEqual(sanitizedObject.safeKey, objectToSanitize.safeKey) - expect(subscription).to.be.calledOnceWith({ sanitizedObject }) + sinon.assert.calledOnceWithMatch(subscription, { sanitizedObject }) }) it('it works as expected with modifications', () => { @@ -212,7 +212,7 @@ describe('express-mongo-sanitize', () => { assert.strictEqual(sanitizedObject.safeKey, objectToSanitize.safeKey) assert.strictEqual(sanitizedObject.unsafeKey.$ne, undefined) - expect(subscription).to.be.calledOnceWith({ sanitizedObject }) + sinon.assert.calledOnceWithMatch(subscription, { sanitizedObject }) }) }) }) diff --git a/packages/datadog-instrumentations/test/express-session.spec.js b/packages/datadog-instrumentations/test/express-session.spec.js index 00595200737..dd5bbc03df3 100644 --- a/packages/datadog-instrumentations/test/express-session.spec.js +++ b/packages/datadog-instrumentations/test/express-session.spec.js @@ -1,12 +1,11 @@ 'use strict' +const assert = require('node:assert') const axios = require('axios') const dc = require('dc-polyfill') const { describe, it, beforeEach, afterEach, before, after } = require('mocha') const sinon = require('sinon') -const assert = require('node:assert') - const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') diff --git a/packages/datadog-instrumentations/test/helpers/check-require-cache.spec.js b/packages/datadog-instrumentations/test/helpers/check-require-cache.spec.js index 64618c4d3eb..43e0c102a66 100644 --- a/packages/datadog-instrumentations/test/helpers/check-require-cache.spec.js +++ b/packages/datadog-instrumentations/test/helpers/check-require-cache.spec.js @@ -3,7 +3,6 @@ const assert = require('node:assert/strict') const { exec } = require('node:child_process') -const { expect } = require('chai') const { describe, it } = require('mocha') describe('check-require-cache', () => { @@ -17,7 +16,7 @@ describe('check-require-cache', () => { it('should be no warnings when tracer is loaded first', (done) => { exec(`${process.execPath} ./check-require-cache/good-order.js`, opts, (error, stdout, stderr) => { assert.strictEqual(error, null) - expect(stderr).to.not.include("Package 'express' was loaded") + assert.doesNotMatch(stderr, /Package 'express' was loaded/) done() }) }) diff --git a/packages/datadog-instrumentations/test/helpers/rewriter/index.spec.js b/packages/datadog-instrumentations/test/helpers/rewriter/index.spec.js index ddfa52ee582..a9da1df1c1d 100644 --- a/packages/datadog-instrumentations/test/helpers/rewriter/index.spec.js +++ b/packages/datadog-instrumentations/test/helpers/rewriter/index.spec.js @@ -1,10 +1,10 @@ 'use strict' -const proxyquire = require('proxyquire') -const { beforeEach, describe, it } = require('mocha') const { readFileSync } = require('node:fs') const { resolve, join } = require('node:path') const Module = require('node:module') +const { beforeEach, describe, it } = require('mocha') +const proxyquire = require('proxyquire') const { tracingChannel } = require('dc-polyfill') // TODO: Test actual functionality and not just the start channel. diff --git a/packages/datadog-instrumentations/test/hono.spec.js b/packages/datadog-instrumentations/test/hono.spec.js new file mode 100644 index 00000000000..30bb2bce1dd --- /dev/null +++ b/packages/datadog-instrumentations/test/hono.spec.js @@ -0,0 +1,142 @@ +'use strict' + +const assert = require('assert/strict') + +const axios = require('axios') +const dc = require('dc-polyfill') +const { describe, it, beforeEach, before, after } = require('mocha') +const sinon = require('sinon') + +const agent = require('../../dd-trace/test/plugins/agent') +const { withVersions } = require('../../dd-trace/test/setup/mocha') + +withVersions('hono', 'hono', version => { + describe('hono instrumentation', () => { + let routeChannelCb, handleChannelCb, errorChannelCb, nextChannelCb + let enterChannelCb, exitChannelCb, finishChannelCb + let port, server, middlewareCalled + + const routeChannel = dc.channel('apm:hono:request:route') + const handleChannel = dc.channel('apm:hono:request:handle') + const errorChannel = dc.channel('apm:hono:request:error') + const nextChannel = dc.channel('apm:hono:middleware:next') + const enterChannel = dc.channel('apm:hono:middleware:enter') + const exitChannel = dc.channel('apm:hono:middleware:exit') + const finishChannel = dc.channel('apm:hono:middleware:finish') + + before(() => { + return agent.load('hono', { client: false }) + }) + + before((done) => { + const { Hono } = require(`../../../versions/hono@${version}`).get() + const { serve } = require('../../../versions/@hono/node-server').get() + const app = new Hono() + + // Add a middleware + app.use(async function named (_c, next) { + middlewareCalled() + await next() + }) + + // Add a route + app.get('/test', (c) => { + return c.text('OK') + }) + + // Add an error route + app.get('/error', (_c) => { + throw new Error('test error') + }) + + server = serve({ port: 0, fetch: app.fetch }, (info) => { + port = info.port + done() + }) + }) + + beforeEach(() => { + routeChannelCb = sinon.stub() + handleChannelCb = sinon.stub() + errorChannelCb = sinon.stub() + nextChannelCb = sinon.stub() + enterChannelCb = sinon.stub() + exitChannelCb = sinon.stub() + finishChannelCb = sinon.stub() + middlewareCalled = sinon.stub() + + routeChannel.subscribe(routeChannelCb) + handleChannel.subscribe(handleChannelCb) + errorChannel.subscribe(errorChannelCb) + nextChannel.subscribe(nextChannelCb) + enterChannel.subscribe(enterChannelCb) + exitChannel.subscribe(exitChannelCb) + finishChannel.subscribe(finishChannelCb) + }) + + afterEach(() => { + routeChannel.unsubscribe(routeChannelCb) + handleChannel.unsubscribe(handleChannelCb) + errorChannel.unsubscribe(errorChannelCb) + nextChannel.unsubscribe(nextChannelCb) + enterChannel.unsubscribe(enterChannelCb) + exitChannel.unsubscribe(exitChannelCb) + finishChannel.unsubscribe(finishChannelCb) + }) + + after(() => { + server.close() + return agent.close({ ritmReset: false }) + }) + + it('should publish to handleChannel on request', async () => { + const res = await axios.get(`http://localhost:${port}/test`) + + assert.strictEqual(res.data, 'OK') + sinon.assert.called(handleChannelCb) + }) + + it('should publish to middleware channels', async () => { + const res = await axios.get(`http://localhost:${port}/test`) + + sinon.assert.called(routeChannelCb) + sinon.assert.calledOnce(middlewareCalled) + + assert.strictEqual(res.data, 'OK') + sinon.assert.called(enterChannelCb) + let callArgs = enterChannelCb.firstCall.args[0] + assert.deepStrictEqual(Object.keys(callArgs), ['req', 'name', 'route']) + assert.strictEqual(callArgs.req.url, '/test') + assert.strictEqual(callArgs.name, 'named') + + sinon.assert.called(nextChannelCb) + callArgs = nextChannelCb.firstCall.args[0] + assert.deepStrictEqual(Object.keys(callArgs), ['req', 'route']) + assert.strictEqual(callArgs.req.url, '/test') + + sinon.assert.called(exitChannelCb) + callArgs = exitChannelCb.firstCall.args[0] + assert.deepStrictEqual(Object.keys(callArgs), ['req', 'route']) + assert.strictEqual(callArgs.req.url, '/test') + + sinon.assert.called(finishChannelCb) + callArgs = finishChannelCb.firstCall.args[0] + assert.deepStrictEqual(Object.keys(callArgs), ['req']) + assert.strictEqual(callArgs.req.url, '/test') + }) + + it('should publish to errorChannel when middleware throws', async () => { + try { + await axios.get(`http://localhost:${port}/error`) + } catch (e) { + // Expected to fail + } + + sinon.assert.called(errorChannelCb) + const callArgs = errorChannelCb.firstCall.args[0] + assert.deepStrictEqual(Object.keys(callArgs), ['req', 'error']) + assert.strictEqual(callArgs.req.url, '/error') + assert.strictEqual(callArgs.error.message, 'test error') + }) + }) +}) diff --git a/packages/datadog-instrumentations/test/http.spec.js b/packages/datadog-instrumentations/test/http.spec.js index 0fc2bdee34f..a6e8c798bb1 100644 --- a/packages/datadog-instrumentations/test/http.spec.js +++ b/packages/datadog-instrumentations/test/http.spec.js @@ -1,11 +1,10 @@ 'use strict' +const assert = require('node:assert') const dc = require('dc-polyfill') const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') -const assert = require('node:assert') - const agent = require('../../dd-trace/test/plugins/agent') describe('client', () => { diff --git a/packages/datadog-instrumentations/test/light-my-request.spec.js b/packages/datadog-instrumentations/test/light-my-request.spec.js new file mode 100644 index 00000000000..682f0847a91 --- /dev/null +++ b/packages/datadog-instrumentations/test/light-my-request.spec.js @@ -0,0 +1,278 @@ +'use strict' + +const assert = require('node:assert') +const dc = require('dc-polyfill') +const { describe, it, before, after, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') + +const agent = require('../../dd-trace/test/plugins/agent') + +describe('light-my-request instrumentation', () => { + const startServerCh = dc.channel('apm:http:server:request:start') + const exitServerCh = dc.channel('apm:http:server:request:exit') + const finishServerCh = dc.channel('apm:http:server:request:finish') + const errorServerCh = dc.channel('apm:http:server:request:error') + + let startStub, exitStub, finishStub, errorStub + let inject, Fastify + + before(async () => { + await agent.load(['http', 'fastify', 'light-my-request'], { client: false }) + inject = require('light-my-request') + Fastify = require('fastify') + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + startStub = sinon.stub() + exitStub = sinon.stub() + finishStub = sinon.stub() + errorStub = sinon.stub() + + startServerCh.subscribe(startStub) + exitServerCh.subscribe(exitStub) + finishServerCh.subscribe(finishStub) + errorServerCh.subscribe(errorStub) + }) + + afterEach(() => { + startServerCh.unsubscribe(startStub) + exitServerCh.unsubscribe(exitStub) + finishServerCh.unsubscribe(finishStub) + errorServerCh.unsubscribe(errorStub) + }) + + describe('with Fastify inject()', () => { + let app + + beforeEach(async () => { + app = Fastify() + app.get('/test', async (req, reply) => { + return { success: true } + }) + app.get('/error', async (req, reply) => { + throw new Error('Test error') + }) + await app.ready() + }) + + afterEach(async () => { + await app.close() + }) + + it('should publish to start channel on inject', async () => { + await app.inject({ + method: 'GET', + url: '/test' + }) + + sinon.assert.called(startStub) + + // Find the call for our /test request (filter out any dd-trace internal requests) + const testCall = startStub.getCalls().find(call => { + const { req } = call.args[0] + return req.url === '/test' + }) + + assert(testCall, 'start channel should be called for /test request') + const { req, res, abortController } = testCall.args[0] + assert.strictEqual(req.url, '/test') + assert.strictEqual(req.method, 'GET') + assert(res, 'res should be provided') + assert(abortController instanceof AbortController, 'abortController should be provided') + }) + + it('should publish to exit channel after inject dispatch', async () => { + await app.inject({ + method: 'GET', + url: '/test' + }) + + sinon.assert.called(exitStub) + + const testCall = exitStub.getCalls().find(call => { + const { req } = call.args[0] + return req.url === '/test' + }) + + assert(testCall, 'exit channel should be called for /test request') + }) + + it('should publish to finish channel when response completes', async () => { + await app.inject({ + method: 'GET', + url: '/test' + }) + + // Wait a tick for finish event to propagate + await new Promise(resolve => setImmediate(resolve)) + + sinon.assert.called(finishStub) + + const testCall = finishStub.getCalls().find(call => { + const { req } = call.args[0] + return req.url === '/test' + }) + + assert(testCall, 'finish channel should be called for /test request') + }) + + it('should link res.req for context tracking', async () => { + let capturedRes + + const handler = ({ req, res }) => { + if (req.url === '/test') { + capturedRes = res + } + } + startServerCh.subscribe(handler) + + try { + await app.inject({ + method: 'GET', + url: '/test' + }) + + assert(capturedRes, 'response should be captured') + assert(capturedRes.req, 'res.req should be set') + assert.strictEqual(capturedRes.req.url, '/test') + } finally { + startServerCh.unsubscribe(handler) + } + }) + + it('should provide abortController to subscribers', async () => { + let capturedAbortController + + const handler = ({ req, abortController }) => { + if (req.url === '/test') { + capturedAbortController = abortController + } + } + startServerCh.subscribe(handler) + + try { + await app.inject({ + method: 'GET', + url: '/test' + }) + + assert(capturedAbortController, 'abortController should be captured') + assert(capturedAbortController instanceof AbortController, 'should be an AbortController') + assert.strictEqual(typeof capturedAbortController.abort, 'function', 'should have abort method') + } finally { + startServerCh.unsubscribe(handler) + } + }) + + it('should pass request headers correctly', async () => { + let capturedHeaders + + const handler = ({ req }) => { + if (req.url === '/test') { + capturedHeaders = req.headers + } + } + startServerCh.subscribe(handler) + + try { + await app.inject({ + method: 'GET', + url: '/test', + headers: { + 'x-custom-header': 'test-value', + 'x-trace-id': '12345' + } + }) + + assert(capturedHeaders, 'headers should be captured') + assert.strictEqual(capturedHeaders['x-custom-header'], 'test-value') + assert.strictEqual(capturedHeaders['x-trace-id'], '12345') + } finally { + startServerCh.unsubscribe(handler) + } + }) + + it('should work with different HTTP methods', async () => { + const methods = [] + + // Create a new app with all routes registered before ready() + const multiMethodApp = Fastify() + multiMethodApp.get('/multi', async () => ({ method: 'GET' })) + multiMethodApp.post('/multi', async () => ({ method: 'POST' })) + multiMethodApp.put('/multi', async () => ({ method: 'PUT' })) + multiMethodApp.delete('/multi', async () => ({ method: 'DELETE' })) + await multiMethodApp.ready() + + const handler = ({ req }) => { + if (req.url === '/multi') { + methods.push(req.method) + } + } + startServerCh.subscribe(handler) + + try { + await multiMethodApp.inject({ method: 'GET', url: '/multi' }) + await multiMethodApp.inject({ method: 'POST', url: '/multi' }) + await multiMethodApp.inject({ method: 'PUT', url: '/multi' }) + await multiMethodApp.inject({ method: 'DELETE', url: '/multi' }) + + assert.deepStrictEqual(methods, ['GET', 'POST', 'PUT', 'DELETE']) + } finally { + startServerCh.unsubscribe(handler) + await multiMethodApp.close() + } + }) + }) + + describe('with standalone light-my-request', () => { + it('should instrument direct inject() calls', async () => { + const dispatchFunc = (req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ ok: true })) + } + + await inject(dispatchFunc, { + method: 'GET', + url: '/standalone-test' + }) + + sinon.assert.called(startStub) + + const testCall = startStub.getCalls().find(call => { + const { req } = call.args[0] + return req.url === '/standalone-test' + }) + + assert(testCall, 'start channel should be called for standalone inject') + }) + + it('should work with callback style', (done) => { + const dispatchFunc = (req, res) => { + res.writeHead(200) + res.end('OK') + } + + inject(dispatchFunc, { method: 'GET', url: '/callback-test' }, (err, response) => { + if (err) return done(err) + + try { + sinon.assert.called(startStub) + + const testCall = startStub.getCalls().find(call => { + const { req } = call.args[0] + return req.url === '/callback-test' + }) + + assert(testCall, 'start channel should be called for callback-style inject') + done() + } catch (e) { + done(e) + } + }) + }) + }) +}) diff --git a/packages/datadog-instrumentations/test/multer.spec.js b/packages/datadog-instrumentations/test/multer.spec.js index c767664fc92..8119a097388 100644 --- a/packages/datadog-instrumentations/test/multer.spec.js +++ b/packages/datadog-instrumentations/test/multer.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict') const axios = require('axios') -const { expect } = require('chai') + const dc = require('dc-polyfill') const { after, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') @@ -52,7 +52,7 @@ withVersions('multer', 'multer', version => { it('should not abort the request by default', async () => { const res = await axios.post(`http://localhost:${port}/`, formData) - expect(middlewareProcessBodyStub).to.be.calledOnceWithExactly(formData.get('key')) + sinon.assert.calledOnceWithExactly(middlewareProcessBodyStub, formData.get('key')) assert.strictEqual(res.data, 'DONE') }) @@ -63,7 +63,7 @@ withVersions('multer', 'multer', version => { try { const res = await axios.post(`http://localhost:${port}/`, formData) - expect(middlewareProcessBodyStub).to.be.calledOnceWithExactly(formData.get('key')) + sinon.assert.calledOnceWithExactly(middlewareProcessBodyStub, formData.get('key')) assert.strictEqual(res.data, 'DONE') } finally { multerReadCh.unsubscribe(noop) @@ -104,7 +104,7 @@ withVersions('multer', 'multer', version => { assert.strictEqual(store.res, payload.res) assert.ok(Object.hasOwn(store, 'span')) - expect(middlewareProcessBodyStub).to.be.calledOnceWithExactly(formData.get('key')) + sinon.assert.calledOnceWithExactly(middlewareProcessBodyStub, formData.get('key')) assert.strictEqual(res.data, 'DONE') } finally { multerReadCh.unsubscribe(handler) diff --git a/packages/datadog-instrumentations/test/passport-http.spec.js b/packages/datadog-instrumentations/test/passport-http.spec.js index 8d8cd7121c6..11d0e631e90 100644 --- a/packages/datadog-instrumentations/test/passport-http.spec.js +++ b/packages/datadog-instrumentations/test/passport-http.spec.js @@ -2,7 +2,7 @@ const assert = require('node:assert/strict') const axios = require('axios').create({ validateStatus: null }) -const { expect } = require('chai') + const dc = require('dc-polyfill') const { after, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') @@ -131,7 +131,7 @@ withVersions('passport-http', 'passport-http', version => { assert.strictEqual(res.status, 200) assert.strictEqual(res.data, 'Granted') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-basic', login: 'test', user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, @@ -150,7 +150,7 @@ withVersions('passport-http', 'passport-http', version => { assert.strictEqual(res.status, 200) assert.strictEqual(res.data, 'Granted') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-basic', login: 'test', user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, @@ -169,7 +169,7 @@ withVersions('passport-http', 'passport-http', version => { assert.strictEqual(res.status, 200) assert.strictEqual(res.data, 'Denied') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-basic', login: 'test', user: false, @@ -193,7 +193,7 @@ withVersions('passport-http', 'passport-http', version => { assert.strictEqual(res.status, 403) assert.strictEqual(res.data, 'Blocked') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-basic', login: 'test', user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, diff --git a/packages/datadog-instrumentations/test/passport-local.spec.js b/packages/datadog-instrumentations/test/passport-local.spec.js index 77eeb8fe1b0..c05ba30f71e 100644 --- a/packages/datadog-instrumentations/test/passport-local.spec.js +++ b/packages/datadog-instrumentations/test/passport-local.spec.js @@ -2,13 +2,12 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const dc = require('dc-polyfill') const { after, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') -const agent = require('../../dd-trace/test/plugins/agent') const axios = require('axios').create({ validateStatus: null }) +const agent = require('../../dd-trace/test/plugins/agent') const { storage } = require('../../datadog-core') const { withVersions } = require('../../dd-trace/test/setup/mocha') @@ -122,7 +121,7 @@ withVersions('passport-local', 'passport-local', version => { assert.strictEqual(res.status, 200) assert.strictEqual(res.data, 'Granted') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-local', login: 'test', user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, @@ -136,7 +135,7 @@ withVersions('passport-local', 'passport-local', version => { assert.strictEqual(res.status, 200) assert.strictEqual(res.data, 'Granted') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-local', login: 'test', user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, @@ -150,7 +149,7 @@ withVersions('passport-local', 'passport-local', version => { assert.strictEqual(res.status, 200) assert.strictEqual(res.data, 'Denied') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-local', login: 'test', user: false, @@ -169,7 +168,7 @@ withVersions('passport-local', 'passport-local', version => { assert.strictEqual(res.status, 403) assert.strictEqual(res.data, 'Blocked') - expect(subscriberStub).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(subscriberStub, { framework: 'passport-local', login: 'test', user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, diff --git a/packages/datadog-instrumentations/test/passport.spec.js b/packages/datadog-instrumentations/test/passport.spec.js index 8109250f382..4a59d04cb36 100644 --- a/packages/datadog-instrumentations/test/passport.spec.js +++ b/packages/datadog-instrumentations/test/passport.spec.js @@ -1,13 +1,12 @@ 'use strict' +const assert = require('node:assert') const dc = require('dc-polyfill') const { describe, it, beforeEach, afterEach, before, after } = require('mocha') const sinon = require('sinon') -const assert = require('node:assert') - -const agent = require('../../dd-trace/test/plugins/agent') const axios = require('axios').create({ validateStatus: null }) +const agent = require('../../dd-trace/test/plugins/agent') const { storage } = require('../../datadog-core') const { withVersions } = require('../../dd-trace/test/setup/mocha') diff --git a/packages/datadog-instrumentations/test/pg.spec.js b/packages/datadog-instrumentations/test/pg.spec.js index 802f4c8b2ec..af98dd20436 100644 --- a/packages/datadog-instrumentations/test/pg.spec.js +++ b/packages/datadog-instrumentations/test/pg.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') +const dc = require('dc-polyfill') const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') -const dc = require('dc-polyfill') const clients = { pg: pg => pg.Client } diff --git a/packages/datadog-plugin-aerospike/test/index.spec.js b/packages/datadog-plugin-aerospike/test/index.spec.js index b7cd3935f1e..cf8f1fdfa07 100644 --- a/packages/datadog-plugin-aerospike/test/index.spec.js +++ b/packages/datadog-plugin-aerospike/test/index.spec.js @@ -9,6 +9,7 @@ const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/c const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const { expectedSchema, rawExpectedSchema } = require('./naming') + describe('Plugin', () => { let aerospike let config diff --git a/packages/datadog-plugin-ai/test/index.spec.js b/packages/datadog-plugin-ai/test/index.spec.js index 29b19187613..314cf4b2671 100644 --- a/packages/datadog-plugin-ai/test/index.spec.js +++ b/packages/datadog-plugin-ai/test/index.spec.js @@ -1,9 +1,9 @@ 'use strict' -const agent = require('../../dd-trace/test/plugins/agent') -const { useEnv } = require('../../../integration-tests/helpers') const assert = require('node:assert') const semifies = require('semifies') +const agent = require('../../dd-trace/test/plugins/agent') +const { assertObjectContains, useEnv } = require('../../../integration-tests/helpers') const { withVersions } = require('../../dd-trace/test/setup/mocha') const { NODE_MAJOR } = require('../../../version') @@ -85,15 +85,23 @@ describe('Plugin', () => { const generateTextSpan = traces[0][0] const doGenerateSpan = traces[0][1] - assert.strictEqual(generateTextSpan.name, 'ai.generateText') - assert.strictEqual(generateTextSpan.resource, 'ai.generateText') - assert.strictEqual(generateTextSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(generateTextSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(generateTextSpan, { + name: 'ai.generateText', + resource: 'ai.generateText', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateText.doGenerate', + resource: 'ai.generateText.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const experimentalTelemetry = { isEnabled: true } @@ -118,15 +126,23 @@ describe('Plugin', () => { const generateTextSpan = traces[0][0] const doGenerateSpan = traces[0][1] - assert.strictEqual(generateTextSpan.name, 'ai.generateText') - assert.strictEqual(generateTextSpan.resource, 'ai.generateText') - assert.strictEqual(generateTextSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(generateTextSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(generateTextSpan, { + name: 'ai.generateText', + resource: 'ai.generateText', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateText.doGenerate', + resource: 'ai.generateText.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const experimentalTelemetry = { tracer: myTracer } @@ -152,15 +168,23 @@ describe('Plugin', () => { const generateTextSpan = traces[0][0] const doGenerateSpan = traces[0][1] - assert.strictEqual(generateTextSpan.name, 'ai.generateText') - assert.strictEqual(generateTextSpan.resource, 'ai.generateText') - assert.strictEqual(generateTextSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(generateTextSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(generateTextSpan, { + name: 'ai.generateText', + resource: 'ai.generateText', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateText.doGenerate', + resource: 'ai.generateText.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const experimentalTelemetry = { isEnabled: true, tracer: myTracer } @@ -186,15 +210,23 @@ describe('Plugin', () => { const generateTextSpan = traces[0][0] const doGenerateSpan = traces[0][1] - assert.strictEqual(generateTextSpan.name, 'ai.generateText') - assert.strictEqual(generateTextSpan.resource, 'ai.generateText') - assert.strictEqual(generateTextSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(generateTextSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(generateTextSpan, { + name: 'ai.generateText', + resource: 'ai.generateText', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateText.doGenerate', + resource: 'ai.generateText.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const result = await ai.generateText({ @@ -215,15 +247,23 @@ describe('Plugin', () => { const generateObjectSpan = traces[0][0] const doGenerateSpan = traces[0][1] - assert.strictEqual(generateObjectSpan.name, 'ai.generateObject') - assert.strictEqual(generateObjectSpan.resource, 'ai.generateObject') - assert.strictEqual(generateObjectSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(generateObjectSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(generateObjectSpan, { + name: 'ai.generateObject', + resource: 'ai.generateObject', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateObject.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'ai.generateObject.doGenerate') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateObject.doGenerate', + resource: 'ai.generateObject.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const schema = ai.jsonSchema({ @@ -233,7 +273,8 @@ describe('Plugin', () => { age: { type: 'number' }, height: { type: 'string' } }, - required: ['name', 'age', 'height'] + required: ['name', 'age', 'height'], + additionalProperties: false }) const result = await ai.generateObject({ @@ -252,15 +293,23 @@ describe('Plugin', () => { const embedSpan = traces[0][0] const doEmbedSpan = traces[0][1] - assert.strictEqual(embedSpan.name, 'ai.embed') - assert.strictEqual(embedSpan.resource, 'ai.embed') - assert.strictEqual(embedSpan.meta['ai.request.model'], 'text-embedding-ada-002') - assert.strictEqual(embedSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(embedSpan, { + name: 'ai.embed', + resource: 'ai.embed', + meta: { + 'ai.request.model': 'text-embedding-ada-002', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doEmbedSpan.name, 'ai.embed.doEmbed') - assert.strictEqual(doEmbedSpan.resource, 'ai.embed.doEmbed') - assert.strictEqual(doEmbedSpan.meta['ai.request.model'], 'text-embedding-ada-002') - assert.strictEqual(doEmbedSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doEmbedSpan, { + name: 'ai.embed.doEmbed', + resource: 'ai.embed.doEmbed', + meta: { + 'ai.request.model': 'text-embedding-ada-002', + 'ai.request.model_provider': 'openai' + } + }) }) const result = await ai.embed({ @@ -278,15 +327,23 @@ describe('Plugin', () => { const embedManySpan = traces[0][0] const doEmbedSpan = traces[0][1] - assert.strictEqual(embedManySpan.name, 'ai.embedMany') - assert.strictEqual(embedManySpan.resource, 'ai.embedMany') - assert.strictEqual(embedManySpan.meta['ai.request.model'], 'text-embedding-ada-002') - assert.strictEqual(embedManySpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(embedManySpan, { + name: 'ai.embedMany', + resource: 'ai.embedMany', + meta: { + 'ai.request.model': 'text-embedding-ada-002', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doEmbedSpan.name, 'ai.embedMany.doEmbed') - assert.strictEqual(doEmbedSpan.resource, 'ai.embedMany.doEmbed') - assert.strictEqual(doEmbedSpan.meta['ai.request.model'], 'text-embedding-ada-002') - assert.strictEqual(doEmbedSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doEmbedSpan, { + name: 'ai.embedMany.doEmbed', + resource: 'ai.embedMany.doEmbed', + meta: { + 'ai.request.model': 'text-embedding-ada-002', + 'ai.request.model_provider': 'openai' + } + }) }) const result = await ai.embedMany({ @@ -304,15 +361,23 @@ describe('Plugin', () => { const streamTextSpan = traces[0][0] const doStreamSpan = traces[0][1] - assert.strictEqual(streamTextSpan.name, 'ai.streamText') - assert.strictEqual(streamTextSpan.resource, 'ai.streamText') - assert.strictEqual(streamTextSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(streamTextSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(streamTextSpan, { + name: 'ai.streamText', + resource: 'ai.streamText', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doStreamSpan.name, 'ai.streamText.doStream') - assert.strictEqual(doStreamSpan.resource, 'ai.streamText.doStream') - assert.strictEqual(doStreamSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doStreamSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doStreamSpan, { + name: 'ai.streamText.doStream', + resource: 'ai.streamText.doStream', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const result = await ai.streamText({ @@ -339,15 +404,23 @@ describe('Plugin', () => { const streamObjectSpan = traces[0][0] const doStreamSpan = traces[0][1] - assert.strictEqual(streamObjectSpan.name, 'ai.streamObject') - assert.strictEqual(streamObjectSpan.resource, 'ai.streamObject') - assert.strictEqual(streamObjectSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(streamObjectSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(streamObjectSpan, { + name: 'ai.streamObject', + resource: 'ai.streamObject', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doStreamSpan.name, 'ai.streamObject.doStream') - assert.strictEqual(doStreamSpan.resource, 'ai.streamObject.doStream') - assert.strictEqual(doStreamSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doStreamSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doStreamSpan, { + name: 'ai.streamObject.doStream', + resource: 'ai.streamObject.doStream', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const schema = ai.jsonSchema({ @@ -357,7 +430,8 @@ describe('Plugin', () => { age: { type: 'number' }, height: { type: 'string' } }, - required: ['name', 'age', 'height'] + required: ['name', 'age', 'height'], + additionalProperties: false }) const result = await ai.streamObject({ @@ -384,23 +458,35 @@ describe('Plugin', () => { const toolCallSpan2 = traces[0][2] const doGenerateSpan2 = traces[0][3] - assert.strictEqual(toolCallSpan.name, 'ai.generateText') - assert.strictEqual(toolCallSpan.resource, 'ai.generateText') - assert.strictEqual(toolCallSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(toolCallSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(toolCallSpan, { + name: 'ai.generateText', + resource: 'ai.generateText', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateText.doGenerate', + resource: 'ai.generateText.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) assert.strictEqual(toolCallSpan2.name, 'ai.toolCall') assert.strictEqual(toolCallSpan2.resource, 'ai.toolCall') - assert.strictEqual(doGenerateSpan2.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan2.resource, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan2.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan2.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan2, { + name: 'ai.generateText.doGenerate', + resource: 'ai.generateText.doGenerate', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) let tools @@ -462,15 +548,23 @@ describe('Plugin', () => { const generateTextSpan = traces[0][0] const doGenerateSpan = traces[0][1] - assert.strictEqual(generateTextSpan.name, 'ai.generateText') - assert.strictEqual(generateTextSpan.resource, 'test') - assert.strictEqual(generateTextSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(generateTextSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(generateTextSpan, { + name: 'ai.generateText', + resource: 'test', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) - assert.strictEqual(doGenerateSpan.name, 'ai.generateText.doGenerate') - assert.strictEqual(doGenerateSpan.resource, 'test') - assert.strictEqual(doGenerateSpan.meta['ai.request.model'], 'gpt-4o-mini') - assert.strictEqual(doGenerateSpan.meta['ai.request.model_provider'], 'openai') + assertObjectContains(doGenerateSpan, { + name: 'ai.generateText.doGenerate', + resource: 'test', + meta: { + 'ai.request.model': 'gpt-4o-mini', + 'ai.request.model_provider': 'openai' + } + }) }) const result = await ai.generateText({ diff --git a/packages/datadog-plugin-ai/test/integration-test/client.spec.js b/packages/datadog-plugin-ai/test/integration-test/client.spec.js index ab89d1da692..b7bd61ad950 100644 --- a/packages/datadog-plugin-ai/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-ai/test/integration-test/client.spec.js @@ -2,13 +2,13 @@ const assert = require('node:assert/strict') +const semifies = require('semifies') const { FakeAgent, sandboxCwd, useSandbox, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') -const semifies = require('semifies') const { withVersions } = require('../../../dd-trace/test/setup/mocha') function getOpenaiVersion (realVersion) { @@ -57,7 +57,7 @@ describe('esm', () => { assert.fail('No ai spans found') }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, null, { + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, { NODE_OPTIONS: '--import dd-trace/initialize.mjs' }) diff --git a/packages/datadog-plugin-ai/test/integration-test/server.mjs b/packages/datadog-plugin-ai/test/integration-test/server.mjs index 8bf8d74e854..77f0216ae3f 100644 --- a/packages/datadog-plugin-ai/test/integration-test/server.mjs +++ b/packages/datadog-plugin-ai/test/integration-test/server.mjs @@ -1,6 +1,6 @@ +import assert from 'node:assert' import { generateText } from 'ai' import { createOpenAI } from '@ai-sdk/openai' -import assert from 'node:assert' const openai = createOpenAI({ baseURL: 'http://127.0.0.1:9126/vcr/openai', diff --git a/packages/datadog-plugin-amqp10/src/index.js b/packages/datadog-plugin-amqp10/src/index.js index fe1b07a0034..96054d59a1a 100644 --- a/packages/datadog-plugin-amqp10/src/index.js +++ b/packages/datadog-plugin-amqp10/src/index.js @@ -1,8 +1,8 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const ProducerPlugin = require('./producer') const ConsumerPlugin = require('./consumer') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class Amqp10Plugin extends CompositePlugin { static id = 'amqp10' diff --git a/packages/datadog-plugin-amqp10/test/integration-test/client.spec.js b/packages/datadog-plugin-amqp10/test/integration-test/client.spec.js index ccb23b23efd..eca9faba0ce 100644 --- a/packages/datadog-plugin-amqp10/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-amqp10/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'amqp.send'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-amqplib/src/index.js b/packages/datadog-plugin-amqplib/src/index.js index 806718e4da5..e275e03ea43 100644 --- a/packages/datadog-plugin-amqplib/src/index.js +++ b/packages/datadog-plugin-amqplib/src/index.js @@ -1,9 +1,9 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const ProducerPlugin = require('./producer') const ConsumerPlugin = require('./consumer') const ClientPlugin = require('./client') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') // TODO: Consider splitting channels for publish/receive in the instrumentation. class AmqplibPlugin extends CompositePlugin { diff --git a/packages/datadog-plugin-amqplib/test/dsm.spec.js b/packages/datadog-plugin-amqplib/test/dsm.spec.js new file mode 100644 index 00000000000..6d6a75f02d7 --- /dev/null +++ b/packages/datadog-plugin-amqplib/test/dsm.spec.js @@ -0,0 +1,281 @@ +'use strict' + +const assert = require('node:assert/strict') +const { Buffer } = require('node:buffer') + +const { afterEach, beforeEach, describe, it } = require('mocha') + +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') +const id = require('../../dd-trace/src/id') +const agent = require('../../dd-trace/test/plugins/agent') +const { withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') + +describe('Plugin', () => { + let connection + let channel + let queue + + describe('amqplib', () => { + withVersions('amqplib', 'amqplib', version => { + beforeEach(() => { + process.env.DD_DATA_STREAMS_ENABLED = 'true' + queue = `test-${id()}` + }) + + afterEach(() => { + connection.close() + }) + + describe('data stream monitoring', function () { + this.timeout(10000) + + let expectedProducerHashWithTopic + let expectedProducerHashWithExchange + let expectedConsumerHash + + beforeEach(done => { + agent.load('amqplib').then(() => { + require(`../../../versions/amqplib@${version}`).get('amqplib/callback_api') + .connect((err, conn) => { + connection = conn + + if (err != null) { + return done(err) + } + + conn.createChannel((err, ch) => { + channel = ch + return done(err) + }) + }) + }) + }) + + afterEach(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + const producerHashWithTopic = computePathwayHash('test', 'tester', [ + 'direction:out', + 'has_routing_key:true', + `topic:${queue}`, + 'type:rabbitmq' + ], ENTRY_PARENT_HASH) + + expectedProducerHashWithTopic = producerHashWithTopic.readBigUInt64LE(0).toString() + + expectedProducerHashWithExchange = computePathwayHash('test', 'tester', [ + 'direction:out', + 'exchange:namedExchange', + 'has_routing_key:true', + 'type:rabbitmq' + ], ENTRY_PARENT_HASH).readBigUInt64LE(0).toString() + + expectedConsumerHash = computePathwayHash('test', 'tester', [ + 'direction:in', + `topic:${queue}`, + 'type:rabbitmq' + ], producerHashWithTopic).readBigUInt64LE(0).toString() + }) + + it('Should emit DSM stats to the agent when sending a message on an unnamed exchange', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = [] + // we should have 1 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) + }) + } + }) + assert.ok(statsPointsReceived.length >= 1) + assert.deepStrictEqual(statsPointsReceived[0].EdgeTags, [ + 'direction:out', + 'has_routing_key:true', + `topic:${queue}`, + 'type:rabbitmq' + ]) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHashWithTopic), true) + }, { timeoutMs: 10000 }).then(done, done) + + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) + }) + }) + + it('Should emit DSM stats to the agent when sending a message on an named exchange', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = [] + // we should have 1 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) + }) + } + }) + assert.ok(statsPointsReceived.length >= 1) + assert.deepStrictEqual(statsPointsReceived[0].EdgeTags, [ + 'direction:out', + 'exchange:namedExchange', + 'has_routing_key:true', + 'type:rabbitmq' + ]) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHashWithExchange), true) + }, { timeoutMs: 10000 }).then(done, done) + + channel.assertExchange('namedExchange', 'direct', {}, (err, ok) => { + if (err) return done(err) + + channel.publish('namedExchange', 'anyOldRoutingKey', Buffer.from('DSM pathway test')) + }) + }) + + it('Should emit DSM stats to the agent when receiving a message', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = [] + // we should have 2 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) + }) + } + }) + assert.strictEqual(statsPointsReceived.length, 2) + assert.deepStrictEqual(statsPointsReceived[1].EdgeTags, + ['direction:in', `topic:${queue}`, 'type:rabbitmq']) + assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) + }, { timeoutMs: 10000 }).then(done, done) + + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) + channel.consume(ok.queue, () => {}, {}, (err, ok) => { + if (err) done(err) + }) + }) + }) + + it('Should emit DSM stats to the agent when sending another message', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = [] + // we should have 1 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) + }) + } + }) + assert.strictEqual(statsPointsReceived.length, 1) + assert.deepStrictEqual(statsPointsReceived[0].EdgeTags, [ + 'direction:out', + 'has_routing_key:true', + `topic:${queue}`, + 'type:rabbitmq' + ]) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHashWithTopic), true) + }, { timeoutMs: 10000 }).then(done, done) + + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) + }) + }) + + it('Should emit DSM stats to the agent when receiving a message with get', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = [] + // we should have 2 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) + }) + } + }) + assert.strictEqual(statsPointsReceived.length, 2) + assert.deepStrictEqual(statsPointsReceived[1].EdgeTags, + ['direction:in', `topic:${queue}`, 'type:rabbitmq']) + assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) + }, { timeoutMs: 10000 }).then(done, done) + + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) + channel.get(ok.queue, {}, (err, ok) => { + if (err) done(err) + }) + }) + }) + + it('regression test: should handle basic.get when queue is empty', done => { + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.get(ok.queue, {}, (err, msg) => { + if (err) return done(err) + assert.strictEqual(msg, false) + done() + }) + }) + }) + + it('Should set pathway hash tag on a span when producing', (done) => { + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.sendToQueue(ok.queue, Buffer.from('dsm test')) + + let produceSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.resource.startsWith('basic.publish')) { + produceSpanMeta = span.meta + } + + assertObjectContains(produceSpanMeta, { + 'pathway.hash': expectedProducerHashWithTopic + }) + }, { timeoutMs: 10000 }).then(done, done) + }) + }) + + it('Should set pathway hash tag on a span when consuming', (done) => { + channel.assertQueue(queue, {}, (err, ok) => { + if (err) return done(err) + + channel.sendToQueue(ok.queue, Buffer.from('dsm test')) + channel.consume(ok.queue, () => {}, {}, (err, ok) => { + if (err) return done(err) + + let consumeSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.resource.startsWith('basic.deliver')) { + consumeSpanMeta = span.meta + } + + assertObjectContains(consumeSpanMeta, { + 'pathway.hash': expectedConsumerHash + }) + }, { timeoutMs: 10000 }).then(done, done) + }) + }) + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-amqplib/test/index.spec.js b/packages/datadog-plugin-amqplib/test/index.spec.js index 741a23b4f9c..ab70b3e9a8b 100644 --- a/packages/datadog-plugin-amqplib/test/index.spec.js +++ b/packages/datadog-plugin-amqplib/test/index.spec.js @@ -6,13 +6,10 @@ const { Buffer } = require('node:buffer') const { afterEach, beforeEach, describe, it } = require('mocha') const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') -const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') -const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') const id = require('../../dd-trace/src/id') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const { expectedSchema, rawExpectedSchema } = require('./naming') -const { assertObjectContains } = require('../../../integration-tests/helpers') describe('Plugin', () => { let tracer @@ -309,233 +306,6 @@ describe('Plugin', () => { .catch(done) }) }) - - describe('when data streams monitoring is enabled', function () { - this.timeout(10000) - - let expectedProducerHashWithTopic - let expectedProducerHashWithExchange - let expectedConsumerHash - - beforeEach(() => { - const producerHashWithTopic = computePathwayHash('test', 'tester', [ - 'direction:out', - 'has_routing_key:true', - `topic:${queue}`, - 'type:rabbitmq' - ], ENTRY_PARENT_HASH) - - expectedProducerHashWithTopic = producerHashWithTopic.readBigUInt64LE(0).toString() - - expectedProducerHashWithExchange = computePathwayHash('test', 'tester', [ - 'direction:out', - 'exchange:namedExchange', - 'has_routing_key:true', - 'type:rabbitmq' - ], ENTRY_PARENT_HASH).readBigUInt64LE(0).toString() - - expectedConsumerHash = computePathwayHash('test', 'tester', [ - 'direction:in', - `topic:${queue}`, - 'type:rabbitmq' - ], producerHashWithTopic).readBigUInt64LE(0).toString() - }) - - it('Should emit DSM stats to the agent when sending a message on an unnamed exchange', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = [] - // we should have 1 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) - }) - } - }) - assert.ok(statsPointsReceived.length >= 1) - assert.deepStrictEqual(statsPointsReceived[0].EdgeTags, [ - 'direction:out', - 'has_routing_key:true', - `topic:${queue}`, - 'type:rabbitmq' - ]) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHashWithTopic), true) - }, { timeoutMs: 10000 }).then(done, done) - - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) - }) - }) - - it('Should emit DSM stats to the agent when sending a message on an named exchange', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = [] - // we should have 1 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) - }) - } - }) - assert.ok(statsPointsReceived.length >= 1) - assert.deepStrictEqual(statsPointsReceived[0].EdgeTags, [ - 'direction:out', - 'exchange:namedExchange', - 'has_routing_key:true', - 'type:rabbitmq' - ]) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHashWithExchange), true) - }, { timeoutMs: 10000 }).then(done, done) - - channel.assertExchange('namedExchange', 'direct', {}, (err, ok) => { - if (err) return done(err) - - channel.publish('namedExchange', 'anyOldRoutingKey', Buffer.from('DSM pathway test')) - }) - }) - - it('Should emit DSM stats to the agent when receiving a message', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = [] - // we should have 2 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) - }) - } - }) - assert.strictEqual(statsPointsReceived.length, 2) - assert.deepStrictEqual(statsPointsReceived[1].EdgeTags, - ['direction:in', `topic:${queue}`, 'type:rabbitmq']) - assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) - }, { timeoutMs: 10000 }).then(done, done) - - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) - channel.consume(ok.queue, () => {}, {}, (err, ok) => { - if (err) done(err) - }) - }) - }) - - it('Should emit DSM stats to the agent when sending another message', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = [] - // we should have 1 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) - }) - } - }) - assert.strictEqual(statsPointsReceived.length, 1) - assert.deepStrictEqual(statsPointsReceived[0].EdgeTags, [ - 'direction:out', - 'has_routing_key:true', - `topic:${queue}`, - 'type:rabbitmq' - ]) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHashWithTopic), true) - }, { timeoutMs: 10000 }).then(done, done) - - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) - }) - }) - - it('Should emit DSM stats to the agent when receiving a message with get', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = [] - // we should have 2 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived = statsPointsReceived.concat(statsBuckets.Stats) - }) - } - }) - assert.strictEqual(statsPointsReceived.length, 2) - assert.deepStrictEqual(statsPointsReceived[1].EdgeTags, - ['direction:in', `topic:${queue}`, 'type:rabbitmq']) - assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) - }, { timeoutMs: 10000 }).then(done, done) - - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.sendToQueue(ok.queue, Buffer.from('DSM pathway test')) - channel.get(ok.queue, {}, (err, ok) => { - if (err) done(err) - }) - }) - }) - - it('regression test: should handle basic.get when queue is empty', done => { - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.get(ok.queue, {}, (err, msg) => { - if (err) return done(err) - assert.strictEqual(msg, false) - done() - }) - }) - }) - - it('Should set pathway hash tag on a span when producing', (done) => { - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.sendToQueue(ok.queue, Buffer.from('dsm test')) - - let produceSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.resource.startsWith('basic.publish')) { - produceSpanMeta = span.meta - } - - assertObjectContains(produceSpanMeta, { - 'pathway.hash': expectedProducerHashWithTopic - }) - }, { timeoutMs: 10000 }).then(done, done) - }) - }) - - it('Should set pathway hash tag on a span when consuming', (done) => { - channel.assertQueue(queue, {}, (err, ok) => { - if (err) return done(err) - - channel.sendToQueue(ok.queue, Buffer.from('dsm test')) - channel.consume(ok.queue, () => {}, {}, (err, ok) => { - if (err) return done(err) - - let consumeSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.resource.startsWith('basic.deliver')) { - consumeSpanMeta = span.meta - } - - assertObjectContains(consumeSpanMeta, { - 'pathway.hash': expectedConsumerHash - }) - }, { timeoutMs: 10000 }).then(done, done) - }) - }) - }) - }) }) describe('with configuration', () => { diff --git a/packages/datadog-plugin-amqplib/test/integration-test/client.spec.js b/packages/datadog-plugin-amqplib/test/integration-test/client.spec.js index ec0cb79118a..7a940b681eb 100644 --- a/packages/datadog-plugin-amqplib/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-amqplib/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -42,7 +42,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'amqp.command'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-amqplib/test/suite.js b/packages/datadog-plugin-amqplib/test/suite.js index d038886c7d1..441416e32fd 100644 --- a/packages/datadog-plugin-amqplib/test/suite.js +++ b/packages/datadog-plugin-amqplib/test/suite.js @@ -2,5 +2,4 @@ const suiteTest = require('../../dd-trace/test/plugins/suite') -// TODO: restore use of latest branch when it gets a matching tag -suiteTest('amqplib', 'amqp-node/amqplib', 'v0.9.0') +suiteTest('amqplib', 'amqp-node/amqplib', 'latest') diff --git a/packages/datadog-plugin-anthropic/src/index.js b/packages/datadog-plugin-anthropic/src/index.js index 54b0856a5ae..61ec6351e10 100644 --- a/packages/datadog-plugin-anthropic/src/index.js +++ b/packages/datadog-plugin-anthropic/src/index.js @@ -1,8 +1,8 @@ 'use strict' const CompositePlugin = require('../../dd-trace/src/plugins/composite') -const AnthropicTracingPlugin = require('./tracing') const AnthropicLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/anthropic') +const AnthropicTracingPlugin = require('./tracing') class AnthropicPlugin extends CompositePlugin { static id = 'anthropic' diff --git a/packages/datadog-plugin-anthropic/test/index.spec.js b/packages/datadog-plugin-anthropic/test/index.spec.js index 6e1637634d6..b93209e4acd 100644 --- a/packages/datadog-plugin-anthropic/test/index.spec.js +++ b/packages/datadog-plugin-anthropic/test/index.spec.js @@ -1,9 +1,9 @@ 'use strict' +const assert = require('node:assert') const { describe, before, after, it } = require('mocha') const { withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') -const assert = require('node:assert') const { useEnv } = require('../../../integration-tests/helpers') describe('Plugin', () => { diff --git a/packages/datadog-plugin-anthropic/test/integration-test/client.spec.js b/packages/datadog-plugin-anthropic/test/integration-test/client.spec.js index 060c4f6c7ae..3cb3f2f1955 100644 --- a/packages/datadog-plugin-anthropic/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-anthropic/test/integration-test/client.spec.js @@ -2,15 +2,15 @@ const assert = require('node:assert/strict') +const { describe, it, beforeEach, afterEach } = require('mocha') const { FakeAgent, sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const { describe, it, beforeEach, afterEach } = require('mocha') describe('esm', () => { let agent @@ -39,7 +39,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'anthropic.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, null, { + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, { NODE_OPTIONS: '--import dd-trace/initialize.mjs', ANTHROPIC_API_KEY: '' }) diff --git a/packages/datadog-plugin-apollo/test/index.spec.js b/packages/datadog-plugin-apollo/test/index.spec.js index 7bb8f82a9fe..570328305b6 100644 --- a/packages/datadog-plugin-apollo/test/index.spec.js +++ b/packages/datadog-plugin-apollo/test/index.spec.js @@ -7,6 +7,7 @@ const axios = require('axios') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants.js') const agent = require('../../dd-trace/test/plugins/agent.js') const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') const accounts = require('./fixtures.js') const { expectedSchema, rawExpectedSchema } = require('./naming.js') @@ -148,47 +149,63 @@ describe('Plugin', () => { agent .assertSomeTraces((traces) => { // the spans are in order of execution - assert.strictEqual(traces[0][0].name, expectedSchema.server.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][0].resource, 'query MyQuery{hello(name:"")}') - assert.strictEqual(traces[0][0].type, 'web') - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['graphql.operation.name'], operationName) + assertObjectContains(traces[0][0], { + name: expectedSchema.server.opName, + service: expectedSchema.server.serviceName, + resource: 'query MyQuery{hello(name:"")}', + type: 'web', + error: 0, + meta: { + 'graphql.operation.name': operationName, + 'graphql.operation.type': 'query', + component: 'apollo.gateway', + '_dd.integration': 'apollo.gateway' + } + }) assert.ok(!('graphql.source' in traces[0][0].meta)) - assert.strictEqual(traces[0][0].meta['graphql.operation.type'], 'query') - assert.strictEqual(traces[0][0].meta.component, 'apollo.gateway') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'apollo.gateway') - assert.strictEqual(traces[0][1].name, 'apollo.gateway.validate') - assert.strictEqual(traces[0][1].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][1].type, 'web') - assert.strictEqual(traces[0][1].error, 0) - assert.strictEqual(traces[0][1].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][1], { + name: 'apollo.gateway.validate', + service: expectedSchema.server.serviceName, + type: 'web', + error: 0, + meta: { component: 'apollo.gateway' } + }) - assert.strictEqual(traces[0][2].name, 'apollo.gateway.plan') - assert.strictEqual(traces[0][2].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][2].type, 'web') - assert.strictEqual(traces[0][2].error, 0) - assert.strictEqual(traces[0][2].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][2], { + name: 'apollo.gateway.plan', + service: expectedSchema.server.serviceName, + type: 'web', + error: 0, + meta: { component: 'apollo.gateway' } + }) - assert.strictEqual(traces[0][3].name, 'apollo.gateway.execute') - assert.strictEqual(traces[0][3].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][3].type, 'web') - assert.strictEqual(traces[0][3].error, 0) - assert.strictEqual(traces[0][3].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][3], { + name: 'apollo.gateway.execute', + service: expectedSchema.server.serviceName, + type: 'web', + error: 0, + meta: { component: 'apollo.gateway' } + }) - assert.strictEqual(traces[0][4].name, 'apollo.gateway.fetch') - assert.strictEqual(traces[0][4].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][4].type, 'web') - assert.strictEqual(traces[0][4].error, 0) - assert.strictEqual(traces[0][4].meta.serviceName, 'accounts') - assert.strictEqual(traces[0][4].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][4], { + name: 'apollo.gateway.fetch', + service: expectedSchema.server.serviceName, + type: 'web', + error: 0, + meta: { + serviceName: 'accounts', + component: 'apollo.gateway' + } + }) - assert.strictEqual(traces[0][5].name, 'apollo.gateway.postprocessing') - assert.strictEqual(traces[0][5].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][5].type, 'web') - assert.strictEqual(traces[0][5].error, 0) - assert.strictEqual(traces[0][5].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][5], { + name: 'apollo.gateway.postprocessing', + service: expectedSchema.server.serviceName, + type: 'web', + error: 0, + meta: { component: 'apollo.gateway' } + }) }) .then(done) .catch(done) @@ -209,8 +226,10 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].type, 'web') assert.strictEqual(traces[0][0].error, 0) assert.ok(!('graphql.source' in traces[0][0].meta)) - assert.strictEqual(traces[0][0].meta['graphql.operation.type'], 'query') - assert.strictEqual(traces[0][0].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][0].meta, { + 'graphql.operation.type': 'query', + component: 'apollo.gateway' + }) }) .then(done) .catch(done) @@ -241,8 +260,10 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].type, 'web') assert.strictEqual(traces[0][0].error, 0) assert.ok(!('graphql.source' in traces[0][0].meta)) - assert.strictEqual(traces[0][0].meta['graphql.operation.type'], 'query') - assert.strictEqual(traces[0][0].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][0].meta, { + 'graphql.operation.type': 'query', + component: 'apollo.gateway' + }) }) .then(done) .catch(done) @@ -256,10 +277,11 @@ describe('Plugin', () => { it('should instrument mutations', done => { const source = 'mutation { human { name } }' - agent - .assertSomeTraces((traces) => { - assert.strictEqual(traces[0][0].meta['graphql.operation.type'], 'mutation') - }) + agent.assertFirstTraceSpan({ + meta: { + 'graphql.operation.type': 'mutation' + } + }) .then(done) .catch(done) @@ -294,21 +316,29 @@ describe('Plugin', () => { agent .assertSomeTraces((traces) => { assert.strictEqual(traces[0].length, 2) - assert.strictEqual(traces[0][0].name, expectedSchema.server.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][0].error, 1) - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][0], { + name: expectedSchema.server.opName, + service: expectedSchema.server.serviceName, + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'apollo.gateway' + } + }) - assert.strictEqual(traces[0][1].name, 'apollo.gateway.validate') - assert.strictEqual(traces[0][1].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][1].error, 1) - assert.strictEqual(traces[0][1].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][1].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][1].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][1].meta.component, 'apollo.gateway') + assertObjectContains(traces[0][1], { + name: 'apollo.gateway.validate', + service: expectedSchema.server.serviceName, + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'apollo.gateway' + } + }) }) .then(done) .catch(done) @@ -336,12 +366,16 @@ describe('Plugin', () => { assert.strictEqual(traces[0][1].name, 'apollo.gateway.validate') assert.strictEqual(traces[0][1].error, 0) - assert.strictEqual(traces[0][2].name, 'apollo.gateway.plan') - assert.strictEqual(traces[0][2].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][2].error, 1) - assert.strictEqual(traces[0][2].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][2].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][2].meta[ERROR_STACK], error.stack) + assertObjectContains(traces[0][2], { + name: 'apollo.gateway.plan', + service: expectedSchema.server.serviceName, + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + } + }) }) .then(done) .catch(done) @@ -363,11 +397,15 @@ describe('Plugin', () => { const variableValues = { who: 'world' } agent .assertSomeTraces((traces) => { - assert.strictEqual(traces[0][0].name, expectedSchema.server.opName) - assert.strictEqual(traces[0][0].error, 1) - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) + assertObjectContains(traces[0][0], { + name: expectedSchema.server.opName, + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + } + }) assert.strictEqual(traces[0][1].name, 'apollo.gateway.validate') assert.strictEqual(traces[0][1].error, 0) @@ -382,18 +420,26 @@ describe('Plugin', () => { // in version 2.3.0, there is no recordExceptions method thus we can't ever attach an error to the // fetch span but instead the error will be propagated to the request span and be set there if (version > '2.3.0') { - assert.strictEqual(traces[0][3].error, 1) - assert.strictEqual(traces[0][3].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][3].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][3].meta[ERROR_STACK], error.stack) + assertObjectContains(traces[0][3], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + } + }) } else { assert.strictEqual(traces[0][3].error, 0) } - assert.strictEqual(traces[0][4].name, 'apollo.gateway.fetch') - assert.strictEqual(traces[0][4].service, expectedSchema.server.serviceName) - assert.strictEqual(traces[0][4].error, 1) - assert.strictEqual(traces[0][4].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][4].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][4].meta[ERROR_STACK], error.stack) + assertObjectContains(traces[0][4], { + name: 'apollo.gateway.fetch', + service: expectedSchema.server.serviceName, + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + } + }) assert.strictEqual(traces[0][5].name, 'apollo.gateway.postprocessing') assert.strictEqual(traces[0][5].error, 0) diff --git a/packages/datadog-plugin-avsc/test/index.spec.js b/packages/datadog-plugin-avsc/test/index.spec.js index 46fe7056640..805aca15ac1 100644 --- a/packages/datadog-plugin-avsc/test/index.spec.js +++ b/packages/datadog-plugin-avsc/test/index.spec.js @@ -4,6 +4,7 @@ const fs = require('fs') const assert = require('node:assert/strict') const path = require('path') +const sinon = require('sinon') const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') const { @@ -14,10 +15,9 @@ const { SCHEMA_WEIGHT, SCHEMA_TYPE } = require('../../dd-trace/src/constants') -const sinon = require('sinon') -const { loadMessage } = require('./helpers') const { SchemaBuilder } = require('../../dd-trace/src/datastreams/schemas/schema_builder') const { NODE_MAJOR } = require('../../../version') +const { loadMessage } = require('./helpers') const BASIC_USER_SCHEMA_DEF = JSON.parse( fs.readFileSync(path.join(__dirname, 'schemas/expected_user_schema.json'), 'utf8') diff --git a/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js b/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js index 900767f082e..482f61022bc 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +++ b/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js @@ -1,8 +1,8 @@ 'use strict' const CompositePlugin = require('../../../../dd-trace/src/plugins/composite') -const BedrockRuntimeTracing = require('./tracing') const BedrockRuntimeLLMObsPlugin = require('../../../../dd-trace/src/llmobs/plugins/bedrockruntime') +const BedrockRuntimeTracing = require('./tracing') class BedrockRuntimePlugin extends CompositePlugin { static id = 'bedrockruntime' diff --git a/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js b/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js index bdd5594f1f4..449f5eeba35 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +++ b/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js @@ -109,7 +109,7 @@ class DynamoDb extends BaseAwsSdkPlugin { /** * Parses primary key config from the `DD_TRACE_DYNAMODB_TABLE_PRIMARY_KEYS` env var. * Only runs when needed, and warns when missing or invalid config. - * @returns {Object|undefined} Parsed config from env var or undefined if empty/missing/invalid config. + * @returns {object | undefined} Parsed config from env var or undefined if empty/missing/invalid config. */ getPrimaryKeyConfig () { if (this.dynamoPrimaryKeyConfig) { @@ -151,8 +151,8 @@ class DynamoDb extends BaseAwsSdkPlugin { /** * Calculates a hash for DynamoDB PutItem operations using table's configured primary keys. * @param {string} tableName - Name of the DynamoDB table. - * @param {Object} item - Complete PutItem item parameter to be put. - * @param {Object.>} primaryKeyConfig - Mapping of table names to an Array of primary key names + * @param {object} item - Complete PutItem item parameter to be put. + * @param {Record>} primaryKeyConfig - Mapping of table names to an Array of primary key names * loaded from DD_TRACE_DYNAMODB_TABLE_PRIMARY_KEYS. * @returns {string|undefined} Hash combining table name and primary key/value pairs, or undefined if unable. */ @@ -174,7 +174,7 @@ class DynamoDb extends BaseAwsSdkPlugin { /** * Calculates a hash for DynamoDB operations that have keys provided (UpdateItem, DeleteItem). * @param {string} tableName - Name of the DynamoDB table. - * @param {Object} keysObject - Object containing primary key/value attributes in DynamoDB format. + * @param {object} keysObject - Object containing primary key/value attributes in DynamoDB format. * (e.g., { userId: { S: "123" }, sortKey: { N: "456" } }) * @returns {string|undefined} Hash value combining table name and primary key/value pairs, or undefined if unable. * diff --git a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js index 874911756fd..6d36d713c27 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +++ b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js @@ -18,8 +18,8 @@ class EventBridge extends BaseAwsSdkPlugin { /** * requestInject - * @param {*} span - * @param {*} request + * @param {import('../../../..').Span} span + * @param {object} request * * Docs: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEventsRequestEntry.html * We cannot use the traceHeader field as that's reserved for X-Ray. diff --git a/packages/datadog-plugin-aws-sdk/src/util.js b/packages/datadog-plugin-aws-sdk/src/util.js index 99901e3e78b..c2e008e55ff 100644 --- a/packages/datadog-plugin-aws-sdk/src/util.js +++ b/packages/datadog-plugin-aws-sdk/src/util.js @@ -18,7 +18,7 @@ function generatePointerHash (components) { /** * Encodes a DynamoDB attribute value to Buffer for span pointer hashing. - * @param {Object} valueObject - DynamoDB value in AWS format ({ S: string } or { N: string } or { B: Buffer }) + * @param {object} valueObject - DynamoDB value in AWS format ({ S: string } or { N: string } or { B: Buffer }) * @returns {Buffer|undefined} Encoded value as Buffer, or undefined if invalid input. * * @example @@ -55,7 +55,7 @@ function encodeValue (valueObject) { * Handles tables with single-key and two-key scenarios. * * @param {Array} keyNames - Set of primary key names. - * @param {Object} keyValuePairs - Object containing key/value pairs. + * @param {object} keyValuePairs - Object containing key/value pairs. * @returns {Array|undefined} [key1Name, key1Value, key2Name, key2Value], or undefined if invalid input. * key2 entries are empty strings in the single-key case. * @example @@ -90,7 +90,7 @@ const extractPrimaryKeys = (keyNames, keyValuePairs) => { * Automatically detects AWS partitions (standard, China, GovCloud) from region. * * @param {string} queueURL - SQS queue URL in any supported format - * @returns {Object|null} Object with queueName and arn, or null if URL format is invalid + * @returns {object | null} Object with queueName and arn, or null if URL format is invalid * * @example * // Modern AWS SQS URLs 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 3edc445f41b..00342bc8fea 100644 --- a/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/aws-sdk.spec.js @@ -8,8 +8,8 @@ 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 { setup, sort } = require('./spec_helpers') const { assertObjectContains } = require('../../../integration-tests/helpers') +const { setup, sort } = require('./spec_helpers') describe('Plugin', () => { // TODO: use the Request class directly for generic tests @@ -285,10 +285,12 @@ describe('Plugin', () => { resource: 'listBuckets', service: 'test' }) - assert.strictEqual(span.error, 0) - assertObjectContains(span.meta, { - 'hook.operation': 'listBuckets', - component: 'aws-sdk' + assertObjectContains(span, { + error: 0, + meta: { + 'hook.operation': 'listBuckets', + component: 'aws-sdk' + } }) }).then(done, done) diff --git a/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js b/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js index 1e69801b103..085635d8130 100644 --- a/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js @@ -1,14 +1,12 @@ 'use strict' +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 { models } = require('./fixtures/bedrockruntime') -const { withVersions } = require('../../dd-trace/test/setup/mocha') -const assert = require('node:assert') -const { assertObjectContains } = require('../../../integration-tests/helpers') - const serviceName = 'bedrock-service-name-test' describe('Plugin', () => { @@ -59,13 +57,12 @@ describe('Plugin', () => { const command = new AWS.InvokeModelCommand(request) - const tracesPromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - assertObjectContains(span.meta, { + const tracesPromise = agent.assertFirstTraceSpan({ + meta: { 'aws.operation': 'invokeModel', 'aws.bedrock.request.model': model.modelId.split('.')[1], 'aws.bedrock.request.model_provider': model.provider.toLowerCase(), - }) + } }) await bedrockRuntimeClient.send(command) @@ -82,13 +79,12 @@ describe('Plugin', () => { const command = new AWS.InvokeModelWithResponseStreamCommand(request) - const tracesPromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - assertObjectContains(span.meta, { + const tracesPromise = agent.assertFirstTraceSpan({ + meta: { 'aws.operation': 'invokeModelWithResponseStream', 'aws.bedrock.request.model': model.modelId.split('.')[1], 'aws.bedrock.request.model_provider': model.provider.toLowerCase(), - }) + } }) const stream = await bedrockRuntimeClient.send(command) diff --git a/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js b/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js index 38d2fe176c4..3ec43832968 100644 --- a/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/dynamodb.spec.js @@ -13,8 +13,6 @@ 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 { assertObjectContains } = require('../../../integration-tests/helpers') - /* eslint-disable no-console */ async function resetLocalStackDynamo () { try { @@ -160,18 +158,16 @@ describe('Plugin', () => { describe('with payload tagging', () => { it('adds request and response payloads as flattened tags for putItem', async () => { - const agentPromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `putItem ${oneKeyTableName}`) - assertObjectContains(span.meta, { + const agentPromise = agent.assertFirstTraceSpan({ + resource: `putItem ${oneKeyTableName}`, + meta: { 'aws.dynamodb.table_name': oneKeyTableName, aws_service: 'DynamoDB', region: 'us-east-1', 'aws.request.body.TableName': oneKeyTableName, 'aws.request.body.Item.name': 'redacted', 'aws.request.body.Item.data.S': 'test-data' - }) + } }) const operation = () => promisify(dynamo.putItem)({ @@ -186,18 +182,16 @@ describe('Plugin', () => { }) it('adds request and response payloads as flattened tags for updateItem', async () => { - const agentPromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `updateItem ${oneKeyTableName}`) - assertObjectContains(span.meta, { + const agentPromise = agent.assertFirstTraceSpan({ + resource: `updateItem ${oneKeyTableName}`, + meta: { 'aws.dynamodb.table_name': oneKeyTableName, aws_service: 'DynamoDB', region: 'us-east-1', 'aws.request.body.TableName': oneKeyTableName, 'aws.request.body.Key.name.S': 'test-name', 'aws.request.body.AttributeUpdates.data.Value.S': 'updated-data' - }) + } }) const operation = () => promisify(dynamo.updateItem)({ @@ -217,17 +211,15 @@ describe('Plugin', () => { }) it('adds request and response payloads as flattened tags for deleteItem', async () => { - const agentPromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `deleteItem ${oneKeyTableName}`) - assertObjectContains(span.meta, { + const agentPromise = agent.assertFirstTraceSpan({ + resource: `deleteItem ${oneKeyTableName}`, + meta: { 'aws.dynamodb.table_name': oneKeyTableName, aws_service: 'DynamoDB', region: 'us-east-1', 'aws.request.body.TableName': oneKeyTableName, 'aws.request.body.Key.name.S': 'test-name' - }) + } }) const operation = () => promisify(dynamo.deleteItem)({ @@ -253,11 +245,9 @@ describe('Plugin', () => { // Wait a bit to ensure the put completes await wait(100) - const agentPromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `getItem ${oneKeyTableName}`) - assertObjectContains(span.meta, { + const agentPromise = agent.assertFirstTraceSpan({ + resource: `getItem ${oneKeyTableName}`, + meta: { 'aws.dynamodb.table_name': oneKeyTableName, aws_service: 'DynamoDB', region: 'us-east-1', @@ -265,7 +255,7 @@ describe('Plugin', () => { 'aws.request.body.Key.name.S': 'test-get-name', 'aws.response.body.Item.name.S': 'test-get-name', 'aws.response.body.Item.data': 'redacted' - }) + } }) const operation = () => promisify(dynamo.getItem)({ 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 20d4fd3d5ae..91b36557ae8 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 @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,12 +34,10 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'aws.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, undefined, - { - AWS_SECRET_ACCESS_KEY: '0000000000/00000000000000000000000000000', - AWS_ACCESS_KEY_ID: '00000000000000000000' - } - ) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, { + AWS_SECRET_ACCESS_KEY: '0000000000/00000000000000000000000000000', + AWS_ACCESS_KEY_ID: '00000000000000000000' + }) await res }).timeout(20000) 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 40d6776543d..4581c117da2 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 @@ -1,16 +1,15 @@ 'use strict' +const assert = require('node:assert') const { FakeAgent, sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const assert = require('node:assert') - describe('recursion regression test', () => { let agent let proc @@ -35,13 +34,10 @@ describe('recursion regression test', () => { assert.strictEqual(checkSpansForServiceName(payload, 'aws.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'recursion.mjs', agent.port, undefined, - { - AWS_SECRET_ACCESS_KEY: '0000000000/00000000000000000000000000000', - AWS_ACCESS_KEY_ID: '00000000000000000000', - execArgv: ['--stack-size=128'] - } - ) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'recursion.mjs', agent.port, { + AWS_SECRET_ACCESS_KEY: '0000000000/00000000000000000000000000000', + AWS_ACCESS_KEY_ID: '00000000000000000000' + }, ['--stack-size=128']) await res }).timeout(20000) diff --git a/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js b/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js new file mode 100644 index 00000000000..094c567656e --- /dev/null +++ b/packages/datadog-plugin-aws-sdk/test/kinesis.dsm.spec.js @@ -0,0 +1,242 @@ +'use strict' + +const assert = require('node:assert/strict') + +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 helpers = require('./kinesis_helpers') +const { setup } = require('./spec_helpers') + +describe('Kinesis', function () { + this.timeout(10000) + setup() + + withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + let AWS + let kinesis + let tracer + + const kinesisClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-kinesis' : 'aws-sdk' + + function createResources (streamName, cb) { + AWS = require(`../../../versions/${kinesisClientName}@${version}`).get() + + const params = { + endpoint: 'http://127.0.0.1:4566', + region: 'us-east-1' + } + + if (moduleName === '@aws-sdk/smithy-client') { + const { NodeHttpHandler } = require(`../../../versions/@aws-sdk/node-http-handler@${version}`).get() + + params.requestHandler = new NodeHttpHandler() + } + + kinesis = new AWS.Kinesis(params) + + kinesis.createStream({ + StreamName: streamName, + ShardCount: 1 + }, (err, res) => { + if (err) return cb(err) + + helpers.waitForActiveStream(kinesis, streamName, cb) + }) + } + + describe('DSM Context Propagation', () => { + let expectedProducerHash + let expectedConsumerHash + let nowStub + let streamNameDSM + + beforeEach(() => { + return agent.load('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) + }) + + beforeEach(done => { + tracer = require('../../dd-trace') + tracer.use('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) + + streamNameDSM = `MyStreamDSM-${id()}` + + const producerHash = computePathwayHash( + 'test', + 'tester', + ['direction:out', 'topic:' + streamNameDSM, 'type:kinesis'], + ENTRY_PARENT_HASH + ) + + expectedProducerHash = producerHash.readBigUInt64LE(0).toString() + expectedConsumerHash = computePathwayHash( + 'test', + 'tester', + ['direction:in', 'topic:' + streamNameDSM, 'type:kinesis'], + producerHash + ).readBigUInt64LE(0).toString() + + createResources(streamNameDSM, done) + }) + + afterEach(done => { + kinesis.deleteStream({ + StreamName: streamNameDSM + }, (err, res) => { + if (err) return done(err) + + helpers.waitForDeletedStream(kinesis, streamNameDSM, done) + }) + }) + + afterEach(() => { + try { + nowStub.restore() + } catch { + // pass + } + agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) + }) + + it('injects DSM pathway hash during Kinesis getRecord to the span', done => { + let getRecordSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.name === 'aws.response') { + getRecordSpanMeta = span.meta + } + + assertObjectContains(getRecordSpanMeta, { + 'pathway.hash': expectedConsumerHash + }) + }, { timeoutMs: 10000 }).then(done, done) + + helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { + if (err) return done(err) + + helpers.getTestData(kinesis, streamNameDSM, data, () => {}) + }) + }) + + it('injects DSM pathway hash during Kinesis putRecord to the span', done => { + let putRecordSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.resource.startsWith('putRecord')) { + putRecordSpanMeta = span.meta + } + + assertObjectContains(putRecordSpanMeta, { + 'pathway.hash': expectedProducerHash + }) + }).then(done, done) + + helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, () => {}) + }) + + it('emits DSM stats to the agent during Kinesis putRecord', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have only have 1 stats point since we only had 1 put operation + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 1) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) + }, { timeoutMs: 10000 }).then(done, done) + + helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, () => {}) + }) + + it('emits DSM stats to the agent during Kinesis getRecord', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have only have 1 stats point since we only had 1 put operation + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }, { timeoutMs: 10000 }) + assert.ok(statsPointsReceived >= 2) + assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) + }, { timeoutMs: 10000 }).then(done, done) + + helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { + if (err) return done(err) + + helpers.getTestData(kinesis, streamNameDSM, data, () => {}) + }) + }) + + // eslint-disable-next-line @stylistic/max-len + it('emits DSM stats to the agent during Kinesis getRecord when the putRecord was done without DSM enabled', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have only have 1 stats point since we only had 1 put operation + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }, { timeoutMs: 10000 }) + assert.strictEqual(statsPointsReceived, 1) + assert.strictEqual(agent.dsmStatsExistWithParentHash(agent, '0'), true) + }, { timeoutMs: 10000 }).then(done, done) + + // TODO: Fix this. The third argument is not used. Check all usages of agent.reload. + agent.reload('aws-sdk', { kinesis: { dsmEnabled: false } }, { dsmEnabled: false }) + helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { + if (err) return done(err) + + // TODO: Fix this. The third argument is not used. Check all usages of agent.reload. + agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) + helpers.getTestData(kinesis, streamNameDSM, data, () => {}) + }) + }) + + it('emits DSM stats to the agent during Kinesis putRecords', done => { + // we need to stub Date.now() to ensure a new stats bucket is created for each call + // otherwise, all stats checkpoints will be combined into a single stats points + let now = Date.now() + nowStub = sinon.stub(Date, 'now') + nowStub.callsFake(() => { + now += 1000000 + return now + }) + + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have only have 3 stats points since we only had 3 records published + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 3) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) + }, { timeoutMs: 10000 }).then(done, done) + + helpers.putTestRecords(kinesis, streamNameDSM, (err, data) => { + // Swallow the error as it doesn't matter for this test. + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js index 3baf4c8c6f4..6d5f0267b3a 100644 --- a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js @@ -3,17 +3,14 @@ const assert = require('node:assert/strict') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') -const sinon = require('sinon') const { assertObjectContains } = require('../../../integration-tests/helpers') const { withNamingSchema, withVersions } = 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 helpers = require('./kinesis_helpers') const { rawExpectedSchema } = require('./kinesis-naming') -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') describe('Kinesis', function () { this.timeout(10000) @@ -22,7 +19,6 @@ describe('Kinesis', function () { withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { let AWS let kinesis - let tracer const kinesisClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-kinesis' : 'aws-sdk' @@ -194,193 +190,5 @@ describe('Kinesis', function () { }) }) }) - - describe('DSM Context Propagation', () => { - let expectedProducerHash - let expectedConsumerHash - let nowStub - let streamNameDSM - - beforeEach(() => { - return agent.load('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) - }) - - beforeEach(done => { - tracer = require('../../dd-trace') - tracer.use('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) - - streamNameDSM = `MyStreamDSM-${id()}` - - const producerHash = computePathwayHash( - 'test', - 'tester', - ['direction:out', 'topic:' + streamNameDSM, 'type:kinesis'], - ENTRY_PARENT_HASH - ) - - expectedProducerHash = producerHash.readBigUInt64LE(0).toString() - expectedConsumerHash = computePathwayHash( - 'test', - 'tester', - ['direction:in', 'topic:' + streamNameDSM, 'type:kinesis'], - producerHash - ).readBigUInt64LE(0).toString() - - createResources(streamNameDSM, done) - }) - - afterEach(done => { - kinesis.deleteStream({ - StreamName: streamNameDSM - }, (err, res) => { - if (err) return done(err) - - helpers.waitForDeletedStream(kinesis, streamNameDSM, done) - }) - }) - - afterEach(() => { - try { - nowStub.restore() - } catch { - // pass - } - agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) - }) - - it('injects DSM pathway hash during Kinesis getRecord to the span', done => { - let getRecordSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.name === 'aws.response') { - getRecordSpanMeta = span.meta - } - - assertObjectContains(getRecordSpanMeta, { - 'pathway.hash': expectedConsumerHash - }) - }, { timeoutMs: 10000 }).then(done, done) - - helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { - if (err) return done(err) - - helpers.getTestData(kinesis, streamNameDSM, data, () => {}) - }) - }) - - it('injects DSM pathway hash during Kinesis putRecord to the span', done => { - let putRecordSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.resource.startsWith('putRecord')) { - putRecordSpanMeta = span.meta - } - - assertObjectContains(putRecordSpanMeta, { - 'pathway.hash': expectedProducerHash - }) - }).then(done, done) - - helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, () => {}) - }) - - it('emits DSM stats to the agent during Kinesis putRecord', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have only have 1 stats point since we only had 1 put operation - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 1) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) - }, { timeoutMs: 10000 }).then(done, done) - - helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, () => {}) - }) - - it('emits DSM stats to the agent during Kinesis getRecord', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have only have 1 stats point since we only had 1 put operation - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }, { timeoutMs: 10000 }) - assert.ok(statsPointsReceived >= 2) - assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) - }, { timeoutMs: 10000 }).then(done, done) - - helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { - if (err) return done(err) - - helpers.getTestData(kinesis, streamNameDSM, data, () => {}) - }) - }) - - // eslint-disable-next-line @stylistic/max-len - it('emits DSM stats to the agent during Kinesis getRecord when the putRecord was done without DSM enabled', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have only have 1 stats point since we only had 1 put operation - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }, { timeoutMs: 10000 }) - assert.strictEqual(statsPointsReceived, 1) - assert.strictEqual(agent.dsmStatsExistWithParentHash(agent, '0'), true) - }, { timeoutMs: 10000 }).then(done, done) - - // TODO: Fix this. The third argument is not used. Check all usages of agent.reload. - agent.reload('aws-sdk', { kinesis: { dsmEnabled: false } }, { dsmEnabled: false }) - helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { - if (err) return done(err) - - // TODO: Fix this. The third argument is not used. Check all usages of agent.reload. - agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) - helpers.getTestData(kinesis, streamNameDSM, data, () => {}) - }) - }) - - it('emits DSM stats to the agent during Kinesis putRecords', done => { - // we need to stub Date.now() to ensure a new stats bucket is created for each call - // otherwise, all stats checkpoints will be combined into a single stats points - let now = Date.now() - nowStub = sinon.stub(Date, 'now') - nowStub.callsFake(() => { - now += 1000000 - return now - }) - - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have only have 3 stats points since we only had 3 records published - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 3) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) - }, { timeoutMs: 10000 }).then(done, done) - - helpers.putTestRecords(kinesis, streamNameDSM, (err, data) => { - // Swallow the error as it doesn't matter for this test. - }) - }) - }) }) }) diff --git a/packages/datadog-plugin-aws-sdk/test/lambda.spec.js b/packages/datadog-plugin-aws-sdk/test/lambda.spec.js index 2736917caf9..e45ea81e81b 100644 --- a/packages/datadog-plugin-aws-sdk/test/lambda.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/lambda.spec.js @@ -7,9 +7,9 @@ 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 { assertObjectContains } = require('../../../integration-tests/helpers') const { rawExpectedSchema } = require('./lambda-naming') const { setup } = require('./spec_helpers') -const { assertObjectContains } = require('../../../integration-tests/helpers') const zip = new JSZip() diff --git a/packages/datadog-plugin-aws-sdk/test/s3.spec.js b/packages/datadog-plugin-aws-sdk/test/s3.spec.js index 1809b13fc53..834ad8ae1f3 100644 --- a/packages/datadog-plugin-aws-sdk/test/s3.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/s3.spec.js @@ -8,9 +8,9 @@ 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 { assertObjectContains } = require('../../../integration-tests/helpers') const { rawExpectedSchema } = require('./s3-naming') const { setup } = require('./spec_helpers') -const { assertObjectContains } = require('../../../integration-tests/helpers') const bucketName = 's3-bucket-name-test' diff --git a/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js b/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js new file mode 100644 index 00000000000..508f033283b --- /dev/null +++ b/packages/datadog-plugin-aws-sdk/test/sns.dsm.spec.js @@ -0,0 +1,269 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { after, afterEach, before, describe, it } = require('mocha') +const sinon = require('sinon') +const semver = require('semver') + +const { assertObjectContains } = require('../../../integration-tests/helpers') +const { withVersions } = require('../../dd-trace/test/setup/mocha') +const agent = require('../../dd-trace/test/plugins/agent') +const { setup } = require('./spec_helpers') + +describe('Sns', function () { + setup() + this.timeout(20000) + + withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + let sns + let sqs + let subParams + let receiveParams + let TopicArn + let QueueUrl + let tracer + + const snsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sns' : 'aws-sdk' + const sqsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sqs' : 'aws-sdk' + + function createResources (queueName, topicName, cb) { + const { SNS } = require(`../../../versions/${snsClientName}@${version}`).get() + const { SQS } = require(`../../../versions/${sqsClientName}@${version}`).get() + + sns = new SNS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' }) + sqs = new SQS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' }) + + sns.createTopic({ Name: topicName }, (err, data) => { + if (err) return cb(err) + + TopicArn = data.TopicArn + + sqs.createQueue({ QueueName: queueName }, (err, data) => { + if (err) return cb(err) + + QueueUrl = data.QueueUrl + + sqs.getQueueAttributes({ + QueueUrl, + AttributeNames: ['QueueArn'] + }, (err, data) => { + if (err) return cb(err) + + const QueueArn = data.Attributes.QueueArn + + subParams = { + Protocol: 'sqs', + TopicArn, + Endpoint: QueueArn + } + + receiveParams = { + QueueUrl, + MessageAttributeNames: ['.*'], + WaitTimeSeconds: 1 + } + + cb() + }) + }) + }) + } + + describe('Data Streams Monitoring', () => { + const expectedProducerHash = '15386798273908484982' + const expectedConsumerHash = '15162998336469814920' + let nowStub + + before(() => { + return agent.load('aws-sdk', { sns: { dsmEnabled: true }, sqs: { dsmEnabled: true } }, { dsmEnabled: true }) + }) + + before(done => { + process.env.DD_DATA_STREAMS_ENABLED = 'true' + tracer = require('../../dd-trace') + tracer.use('aws-sdk', { sns: { dsmEnabled: true }, sqs: { dsmEnabled: true } }) + + createResources('TestQueueDSM', 'TestTopicDSM', done) + }) + + after(done => { + sns.deleteTopic({ TopicArn }, done) + }) + + after(done => { + sqs.deleteQueue({ QueueUrl }, done) + }) + + after(() => { + return agent.close({ ritmReset: false, wipe: true }) + }) + + afterEach(() => { + try { + nowStub.restore() + } catch { + // pass + } + // TODO: Fix this. The third argument is not used. + agent.reload('aws-sdk', { sns: { dsmEnabled: true, batchPropagationEnabled: true } }, { dsmEnabled: true }) + }) + + it('injects DSM pathway hash to SNS publish span', done => { + sns.subscribe(subParams, (err, data) => { + if (err) return done(err) + + sns.publish( + { TopicArn, Message: 'message DSM' }, + (err) => { + if (err) return done(err) + + let publishSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.resource.startsWith('publish')) { + publishSpanMeta = span.meta + } + + assertObjectContains(publishSpanMeta, { + 'pathway.hash': expectedProducerHash + }) + }).then(done, done) + }) + }) + }) + + it('injects DSM pathway hash to SQS receive span from SNS topic', done => { + sns.subscribe(subParams, (err, data) => { + if (err) return done(err) + + sns.publish( + { TopicArn, Message: 'message DSM' }, + (err) => { + if (err) return done(err) + }) + + sqs.receiveMessage( + receiveParams, + (err, res) => { + if (err) return done(err) + + let consumeSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.name === 'aws.response') { + consumeSpanMeta = span.meta + } + + assertObjectContains(consumeSpanMeta, { + 'pathway.hash': expectedConsumerHash + }) + }).then(done, done) + }) + }) + }) + + it('outputs DSM stats to the agent when publishing a message', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 1 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 1) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) + }).then(done, done) + + sns.subscribe(subParams, () => { + sns.publish({ TopicArn, Message: 'message DSM' }, () => {}) + }) + }) + + it('outputs DSM stats to the agent when consuming a message', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 2 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 2) + assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) + }).then(done, done) + + sns.subscribe(subParams, () => { + sns.publish({ TopicArn, Message: 'message DSM' }, () => { + sqs.receiveMessage(receiveParams, () => {}) + }) + }) + }) + + it('outputs DSM stats to the agent when publishing batch messages', function (done) { + // publishBatch was released with version 2.1031.0 for the aws-sdk + // publishBatch does not work with smithy-client 3.0.0, unable to find compatible version it + // was released for, but works on 3.374.0 + if ( + (moduleName === '@aws-sdk/smithy-client' && semver.intersects(version, '>=3.374.0')) || + (moduleName === 'aws-sdk' && semver.intersects(version, '>=2.1031.0')) + ) { + // we need to stub Date.now() to ensure a new stats bucket is created for each call + // otherwise, all stats checkpoints will be combined into a single stats points + let now = Date.now() + nowStub = sinon.stub(Date, 'now') + nowStub.callsFake(() => { + now += 1000000 + return now + }) + + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 3 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 3) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) + }, { timeoutMs: 2000 }).then(done, done) + + sns.subscribe(subParams, () => { + sns.publishBatch( + { + TopicArn, + PublishBatchRequestEntries: [ + { + Id: '1', + Message: 'message DSM 1' + }, + { + Id: '2', + Message: 'message DSM 2' + }, + { + Id: '3', + Message: 'message DSM 3' + } + ] + }, () => { + nowStub.restore() + }) + }) + } else { + this.skip() + } + }) + }) + }) +}) diff --git a/packages/datadog-plugin-aws-sdk/test/sns.spec.js b/packages/datadog-plugin-aws-sdk/test/sns.spec.js index 3565c1495d5..8f8e4497c0b 100644 --- a/packages/datadog-plugin-aws-sdk/test/sns.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sns.spec.js @@ -2,8 +2,7 @@ const assert = require('node:assert/strict') -const { after, afterEach, before, describe, it } = require('mocha') -const sinon = require('sinon') +const { after, before, describe, it } = require('mocha') const semver = require('semver') const { assertObjectContains } = require('../../../integration-tests/helpers') @@ -118,11 +117,9 @@ describe('Sns', function () { }) it('adds request and response payloads as flattened tags', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `publish ${TopicArn}`) - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: `publish ${TopicArn}`, + meta: { 'aws.sns.topic_arn': TopicArn, topicname: 'TestTopic', aws_service: 'SNS', @@ -136,7 +133,7 @@ describe('Sns', function () { 'aws.request.body.MessageAttributes.keyTwo.DataType': 'String', 'aws.request.body.MessageAttributes.keyTwo.StringValue': 'keyTwo', 'aws.response.body.MessageId': 'redacted' - }) + } }, { timeoutMs: 20000 }).then(done, done) sns.publish({ @@ -151,11 +148,9 @@ describe('Sns', function () { }) it('expands and redacts keys identified as expandable', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `publish ${TopicArn}`) - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: `publish ${TopicArn}`, + meta: { 'aws.sns.topic_arn': TopicArn, topicname: 'TestTopic', aws_service: 'SNS', @@ -166,7 +161,7 @@ describe('Sns', function () { 'aws.request.body.MessageAttributes.unredacted.StringValue.foo': 'bar', 'aws.request.body.MessageAttributes.unredacted.StringValue.baz': 'yup', 'aws.response.body.MessageId': 'redacted' - }) + } }, { timeoutMs: 20000 }).then(done, done) sns.publish({ @@ -184,20 +179,23 @@ describe('Sns', function () { agent.assertSomeTraces(traces => { const span = traces[0][0] - assert.strictEqual(span.resource, `publish ${TopicArn}`) - assertObjectContains(span.meta, { - 'aws.sns.topic_arn': TopicArn, - topicname: 'TestTopic', - aws_service: 'SNS', - region: 'us-east-1', - 'aws.request.body.TopicArn': TopicArn, - 'aws.request.body.Message': 'message 1', - 'aws.request.body.MessageAttributes.foo': 'redacted', - 'aws.request.body.MessageAttributes.keyOne.DataType': 'String', - 'aws.request.body.MessageAttributes.keyOne.StringValue': 'keyOne', - 'aws.request.body.MessageAttributes.keyTwo.DataType': 'String', - 'aws.request.body.MessageAttributes.keyTwo.StringValue': 'keyTwo' + assertObjectContains(span, { + resource: `publish ${TopicArn}`, + meta: { + 'aws.sns.topic_arn': TopicArn, + topicname: 'TestTopic', + aws_service: 'SNS', + region: 'us-east-1', + 'aws.request.body.TopicArn': TopicArn, + 'aws.request.body.Message': 'message 1', + 'aws.request.body.MessageAttributes.foo': 'redacted', + 'aws.request.body.MessageAttributes.keyOne.DataType': 'String', + 'aws.request.body.MessageAttributes.keyOne.StringValue': 'keyOne', + 'aws.request.body.MessageAttributes.keyTwo.DataType': 'String', + 'aws.request.body.MessageAttributes.keyTwo.StringValue': 'keyTwo' + } }) + assert.ok(Object.hasOwn(span.meta, 'aws.response.body.MessageId')) }, { timeoutMs: 20000 }).then(done, done) @@ -214,17 +212,16 @@ describe('Sns', function () { // TODO add response tests it('redacts user-defined keys to suppress in response', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - assert.strictEqual(span.resource, `getTopicAttributes ${TopicArn}`) - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: `getTopicAttributes ${TopicArn}`, + meta: { 'aws.sns.topic_arn': TopicArn, topicname: 'TestTopic', aws_service: 'SNS', region: 'us-east-1', 'aws.request.body.TopicArn': TopicArn, 'aws.response.body.Attributes.DisplayName': 'redacted' - }) + } }, { timeoutMs: 20000 }).then(done, done) sns.getTopicAttributes({ TopicArn }, e => e && done(e)) @@ -258,16 +255,14 @@ describe('Sns', function () { }) it('redacts phone numbers in request', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, 'publish') - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: 'publish', + meta: { aws_service: 'SNS', region: 'us-east-1', 'aws.request.body.PhoneNumber': 'redacted', 'aws.request.body.Message': 'message 1' - }) + } }, { timeoutMs: 20000 }).then(done, done) sns.publish({ @@ -277,15 +272,13 @@ describe('Sns', function () { }) it('redacts phone numbers in response', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, 'publish') - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: 'publish', + meta: { aws_service: 'SNS', region: 'us-east-1', 'aws.response.body.PhoneNumber': 'redacted' - }) + } }, { timeoutMs: 20000 }).then(done, done) sns.listSMSSandboxPhoneNumbers({ @@ -298,18 +291,16 @@ describe('Sns', function () { describe('subscription confirmation tokens', () => { it('redacts tokens in request', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `confirmSubscription ${TopicArn}`) - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: `confirmSubscription ${TopicArn}`, + meta: { aws_service: 'SNS', 'aws.sns.topic_arn': TopicArn, topicname: 'TestTopic', region: 'us-east-1', 'aws.request.body.Token': 'redacted', 'aws.request.body.TopicArn': TopicArn - }) + } }).then(done, done) sns.confirmSubscription({ @@ -338,7 +329,6 @@ describe('Sns', function () { }) before(done => { - process.env.DD_DATA_STREAMS_ENABLED = 'true' tracer = require('../../dd-trace') tracer.use('aws-sdk', { sns: { dsmEnabled: false, batchPropagationEnabled: true } }) @@ -487,216 +477,18 @@ describe('Sns', function () { } it('generates tags for proper publish calls', done => { - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - assert.strictEqual(span.resource, `publish ${TopicArn}`) - assertObjectContains(span.meta, { + agent.assertFirstTraceSpan({ + resource: `publish ${TopicArn}`, + meta: { 'aws.sns.topic_arn': TopicArn, topicname: 'TestTopic', aws_service: 'SNS', region: 'us-east-1' - }) + } }).then(done, done) sns.publish({ TopicArn, Message: 'message 1' }, e => e && done(e)) }) }) - - describe('Data Streams Monitoring', () => { - const expectedProducerHash = '15386798273908484982' - const expectedConsumerHash = '15162998336469814920' - let nowStub - - before(() => { - return agent.load('aws-sdk', { sns: { dsmEnabled: true }, sqs: { dsmEnabled: true } }, { dsmEnabled: true }) - }) - - before(done => { - process.env.DD_DATA_STREAMS_ENABLED = 'true' - tracer = require('../../dd-trace') - tracer.use('aws-sdk', { sns: { dsmEnabled: true }, sqs: { dsmEnabled: true } }) - - createResources('TestQueueDSM', 'TestTopicDSM', done) - }) - - after(done => { - sns.deleteTopic({ TopicArn }, done) - }) - - after(done => { - sqs.deleteQueue({ QueueUrl }, done) - }) - - after(() => { - return agent.close({ ritmReset: false, wipe: true }) - }) - - afterEach(() => { - try { - nowStub.restore() - } catch { - // pass - } - // TODO: Fix this. The third argument is not used. - agent.reload('aws-sdk', { sns: { dsmEnabled: true, batchPropagationEnabled: true } }, { dsmEnabled: true }) - }) - - it('injects DSM pathway hash to SNS publish span', done => { - sns.subscribe(subParams, (err, data) => { - if (err) return done(err) - - sns.publish( - { TopicArn, Message: 'message DSM' }, - (err) => { - if (err) return done(err) - - let publishSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.resource.startsWith('publish')) { - publishSpanMeta = span.meta - } - - assertObjectContains(publishSpanMeta, { - 'pathway.hash': expectedProducerHash - }) - }).then(done, done) - }) - }) - }) - - it('injects DSM pathway hash to SQS receive span from SNS topic', done => { - sns.subscribe(subParams, (err, data) => { - if (err) return done(err) - - sns.publish( - { TopicArn, Message: 'message DSM' }, - (err) => { - if (err) return done(err) - }) - - sqs.receiveMessage( - receiveParams, - (err, res) => { - if (err) return done(err) - - let consumeSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.name === 'aws.response') { - consumeSpanMeta = span.meta - } - - assertObjectContains(consumeSpanMeta, { - 'pathway.hash': expectedConsumerHash - }) - }).then(done, done) - }) - }) - }) - - it('outputs DSM stats to the agent when publishing a message', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 1 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 1) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) - }).then(done, done) - - sns.subscribe(subParams, () => { - sns.publish({ TopicArn, Message: 'message DSM' }, () => {}) - }) - }) - - it('outputs DSM stats to the agent when consuming a message', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 2 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 2) - assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) - }).then(done, done) - - sns.subscribe(subParams, () => { - sns.publish({ TopicArn, Message: 'message DSM' }, () => { - sqs.receiveMessage(receiveParams, () => {}) - }) - }) - }) - - it('outputs DSM stats to the agent when publishing batch messages', function (done) { - // publishBatch was released with version 2.1031.0 for the aws-sdk - // publishBatch does not work with smithy-client 3.0.0, unable to find compatible version it - // was released for, but works on 3.374.0 - if ( - (moduleName === '@aws-sdk/smithy-client' && semver.intersects(version, '>=3.374.0')) || - (moduleName === 'aws-sdk' && semver.intersects(version, '>=2.1031.0')) - ) { - // we need to stub Date.now() to ensure a new stats bucket is created for each call - // otherwise, all stats checkpoints will be combined into a single stats points - let now = Date.now() - nowStub = sinon.stub(Date, 'now') - nowStub.callsFake(() => { - now += 1000000 - return now - }) - - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 3 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 3) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) - }, { timeoutMs: 2000 }).then(done, done) - - sns.subscribe(subParams, () => { - sns.publishBatch( - { - TopicArn, - PublishBatchRequestEntries: [ - { - Id: '1', - Message: 'message DSM 1' - }, - { - Id: '2', - Message: 'message DSM 2' - }, - { - Id: '3', - Message: 'message DSM 3' - } - ] - }, () => { - nowStub.restore() - }) - }) - } else { - this.skip() - } - }) - }) }) }) diff --git a/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js b/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js new file mode 100644 index 00000000000..57f086f94a0 --- /dev/null +++ b/packages/datadog-plugin-aws-sdk/test/sqs.dsm.spec.js @@ -0,0 +1,307 @@ +'use strict' + +const assert = require('node:assert/strict') +const { randomUUID } = require('node:crypto') + +const { after, afterEach, before, beforeEach, describe, it } = require('mocha') +const semver = require('semver') +const sinon = require('sinon') + +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') +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 getQueueParams = (queueName) => { + return { + QueueName: queueName, + Attributes: { + MessageRetentionPeriod: '86400' + } + } +} + +describe('Plugin', () => { + describe('aws-sdk (sqs)', function () { + this.timeout(10000) + setup() + + withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => { + let AWS + let sqs + let queueNameDSM + let queueNameDSMConsumerOnly + let queueOptionsDsm + let queueOptionsDsmConsumerOnly + let QueueUrlDsm + let QueueUrlDsmConsumerOnly + let tracer + + const sqsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sqs' : 'aws-sdk' + + beforeEach(() => { + const id = randomUUID() + queueNameDSM = `SQS_QUEUE_NAME_DSM-${id}` + queueNameDSMConsumerOnly = `SQS_QUEUE_NAME_DSM_CONSUMER_ONLY-${id}` + queueOptionsDsm = getQueueParams(queueNameDSM) + queueOptionsDsmConsumerOnly = getQueueParams(queueNameDSMConsumerOnly) + QueueUrlDsm = `http://127.0.0.1:4566/00000000000000000000/${queueNameDSM}` + QueueUrlDsmConsumerOnly = `http://127.0.0.1:4566/00000000000000000000/${queueNameDSMConsumerOnly}` + }) + + describe('data stream monitoring', () => { + let expectedProducerHash + let expectedConsumerHash + let nowStub + + before(() => { + process.env.DD_DATA_STREAMS_ENABLED = 'true' + tracer = require('../../dd-trace') + tracer.use('aws-sdk', { sqs: { dsmEnabled: true } }) + }) + + before(async () => { + return agent.load('aws-sdk', { + sqs: { + dsmEnabled: true + } + }, + { dsmEnabled: true }) + }) + + before(() => { + AWS = require(`../../../versions/${sqsClientName}@${version}`).get() + sqs = new AWS.SQS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' }) + }) + + beforeEach(() => { + const producerHash = computePathwayHash( + 'test', + 'tester', + ['direction:out', 'topic:' + queueNameDSM, 'type:sqs'], + ENTRY_PARENT_HASH + ) + + expectedProducerHash = producerHash.readBigUInt64LE(0).toString() + expectedConsumerHash = computePathwayHash( + 'test', + 'tester', + ['direction:in', 'topic:' + queueNameDSM, 'type:sqs'], + producerHash + ).readBigUInt64LE(0).toString() + }) + + beforeEach(done => { + sqs.createQueue(queueOptionsDsm, (err, res) => err ? done(err) : done()) + }) + + beforeEach(done => { + sqs.createQueue(queueOptionsDsmConsumerOnly, (err, res) => err ? done(err) : done()) + }) + + afterEach(done => { + sqs.deleteQueue({ QueueUrl: QueueUrlDsm }, done) + }) + + afterEach(done => { + sqs.deleteQueue({ QueueUrl: QueueUrlDsmConsumerOnly }, done) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + afterEach(() => { + try { + nowStub.restore() + } catch { + // pass + } + agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) + }) + + it('Should set pathway hash tag on a span when producing', (done) => { + sqs.sendMessage({ + MessageBody: 'test DSM', + QueueUrl: QueueUrlDsm + }, (err) => { + if (err) return done(err) + + let produceSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.resource.startsWith('sendMessage')) { + produceSpanMeta = span.meta + } + + assertObjectContains(produceSpanMeta, { + 'pathway.hash': expectedProducerHash + }) + }).then(done, done) + }) + }) + + it('Should set pathway hash tag on a span when consuming', (done) => { + sqs.sendMessage({ + MessageBody: 'test DSM', + QueueUrl: QueueUrlDsm + }, (err) => { + if (err) return done(err) + + sqs.receiveMessage({ + QueueUrl: QueueUrlDsm, + MessageAttributeNames: ['.*'] + }, (err) => { + if (err) return done(err) + + let consumeSpanMeta = {} + agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.name === 'aws.response') { + consumeSpanMeta = span.meta + } + + assertObjectContains(consumeSpanMeta, { + 'pathway.hash': expectedConsumerHash + }) + }).then(done, done) + }) + }) + }) + + if (sqsClientName === 'aws-sdk' && semver.intersects(version, '>=2.3')) { + it('Should set pathway hash tag on a span when consuming and promise() was used over a callback', + async () => { + let consumeSpanMeta = {} + const tracePromise = agent.assertSomeTraces(traces => { + const span = traces[0][0] + + if (span.name === 'aws.request' && span.meta['aws.operation'] === 'receiveMessage') { + consumeSpanMeta = span.meta + } + + assertObjectContains(consumeSpanMeta, { + 'pathway.hash': expectedConsumerHash + }) + }) + + await sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsm }).promise() + await sqs.receiveMessage({ QueueUrl: QueueUrlDsm }).promise() + + return tracePromise + }) + } + + it('Should emit DSM stats to the agent when sending a message', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 1 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 1) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) + }).then(done, done) + + sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsm }, () => {}) + }) + + it('Should emit DSM stats to the agent when receiving a message', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 2 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 2) + assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) + }, { timeoutMs: 5000 }).then(done, done) + + sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsm }, () => { + sqs.receiveMessage({ QueueUrl: QueueUrlDsm, MessageAttributeNames: ['.*'] }, () => {}) + }) + }) + + it('Should emit DSM stats when receiving a message when the producer was not instrumented', done => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 2 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.strictEqual(statsPointsReceived, 1) + assert.strictEqual(agent.dsmStatsExistWithParentHash(agent, '0'), true) + }).then(done, done) + + agent.reload('aws-sdk', { sqs: { dsmEnabled: false } }, { dsmEnabled: false }) + sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsmConsumerOnly }, () => { + agent.reload('aws-sdk', { sqs: { dsmEnabled: true } }, { dsmEnabled: true }) + sqs.receiveMessage({ QueueUrl: QueueUrlDsmConsumerOnly, MessageAttributeNames: ['.*'] }, () => {}) + }) + }) + + it('Should emit DSM stats to the agent when sending batch messages', done => { + // we need to stub Date.now() to ensure a new stats bucket is created for each call + // otherwise, all stats checkpoints will be combined into a single stats points + let now = Date.now() + nowStub = sinon.stub(Date, 'now') + nowStub.callsFake(() => { + now += 1000000 + return now + }) + + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 3 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 3) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) + }).then(done, done) + + sqs.sendMessageBatch( + { + Entries: [ + { + Id: '1', + MessageBody: 'test DSM 1' + }, + { + Id: '2', + MessageBody: 'test DSM 2' + }, + { + Id: '3', + MessageBody: 'test DSM 3' + } + ], + QueueUrl: QueueUrlDsm + }, () => { + nowStub.restore() + }) + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-aws-sdk/test/sqs.spec.js b/packages/datadog-plugin-aws-sdk/test/sqs.spec.js index 1b8b6950604..b055c8fc989 100644 --- a/packages/datadog-plugin-aws-sdk/test/sqs.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sqs.spec.js @@ -3,16 +3,12 @@ const assert = require('node:assert/strict') const { randomUUID } = require('node:crypto') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') -const semver = require('semver') -const sinon = require('sinon') -const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') -const { ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { setup } = require('./spec_helpers') const { rawExpectedSchema } = require('./sqs-naming') -const { assertObjectContains } = require('../../../integration-tests/helpers') const getQueueParams = (queueName) => { return { @@ -32,14 +28,8 @@ describe('Plugin', () => { let AWS let sqs let queueName - let queueNameDSM - let queueNameDSMConsumerOnly let queueOptions - let queueOptionsDsm - let queueOptionsDsmConsumerOnly let QueueUrl - let QueueUrlDsm - let QueueUrlDsmConsumerOnly let tracer const sqsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sqs' : 'aws-sdk' @@ -48,21 +38,14 @@ describe('Plugin', () => { const id = randomUUID() queueName = `SQS_QUEUE_NAME-${id}` - queueNameDSM = `SQS_QUEUE_NAME_DSM-${id}` - queueNameDSMConsumerOnly = `SQS_QUEUE_NAME_DSM_CONSUMER_ONLY-${id}` queueOptions = getQueueParams(queueName) - queueOptionsDsm = getQueueParams(queueNameDSM) - queueOptionsDsmConsumerOnly = getQueueParams(queueNameDSMConsumerOnly) QueueUrl = `http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME-${id}` - QueueUrlDsm = `http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME_DSM-${id}` - QueueUrlDsmConsumerOnly = `http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME_DSM_CONSUMER_ONLY-${id}` }) describe('without configuration', () => { before(() => { - process.env.DD_DATA_STREAMS_ENABLED = 'true' tracer = require('../../dd-trace') tracer.use('aws-sdk', { sqs: { batchPropagationEnabled: true } }) @@ -414,258 +397,6 @@ describe('Plugin', () => { }, 250) }) }) - - describe('data stream monitoring', () => { - let expectedProducerHash - let expectedConsumerHash - let nowStub - - before(() => { - process.env.DD_DATA_STREAMS_ENABLED = 'true' - tracer = require('../../dd-trace') - tracer.use('aws-sdk', { sqs: { dsmEnabled: true } }) - }) - - before(async () => { - return agent.load('aws-sdk', { - sqs: { - dsmEnabled: true - } - }, - { dsmEnabled: true }) - }) - - before(() => { - AWS = require(`../../../versions/${sqsClientName}@${version}`).get() - sqs = new AWS.SQS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' }) - }) - - beforeEach(() => { - const producerHash = computePathwayHash( - 'test', - 'tester', - ['direction:out', 'topic:' + queueNameDSM, 'type:sqs'], - ENTRY_PARENT_HASH - ) - - expectedProducerHash = producerHash.readBigUInt64LE(0).toString() - expectedConsumerHash = computePathwayHash( - 'test', - 'tester', - ['direction:in', 'topic:' + queueNameDSM, 'type:sqs'], - producerHash - ).readBigUInt64LE(0).toString() - }) - - beforeEach(done => { - sqs.createQueue(queueOptionsDsm, (err, res) => err ? done(err) : done()) - }) - - beforeEach(done => { - sqs.createQueue(queueOptionsDsmConsumerOnly, (err, res) => err ? done(err) : done()) - }) - - afterEach(done => { - sqs.deleteQueue({ QueueUrl: QueueUrlDsm }, done) - }) - - afterEach(done => { - sqs.deleteQueue({ QueueUrl: QueueUrlDsmConsumerOnly }, done) - }) - - after(() => { - return agent.close({ ritmReset: false }) - }) - - afterEach(() => { - try { - nowStub.restore() - } catch { - // pass - } - agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) - }) - - it('Should set pathway hash tag on a span when producing', (done) => { - sqs.sendMessage({ - MessageBody: 'test DSM', - QueueUrl: QueueUrlDsm - }, (err) => { - if (err) return done(err) - - let produceSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.resource.startsWith('sendMessage')) { - produceSpanMeta = span.meta - } - - assertObjectContains(produceSpanMeta, { - 'pathway.hash': expectedProducerHash - }) - }).then(done, done) - }) - }) - - it('Should set pathway hash tag on a span when consuming', (done) => { - sqs.sendMessage({ - MessageBody: 'test DSM', - QueueUrl: QueueUrlDsm - }, (err) => { - if (err) return done(err) - - sqs.receiveMessage({ - QueueUrl: QueueUrlDsm, - MessageAttributeNames: ['.*'] - }, (err) => { - if (err) return done(err) - - let consumeSpanMeta = {} - agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.name === 'aws.response') { - consumeSpanMeta = span.meta - } - - assertObjectContains(consumeSpanMeta, { - 'pathway.hash': expectedConsumerHash - }) - }).then(done, done) - }) - }) - }) - - if (sqsClientName === 'aws-sdk' && semver.intersects(version, '>=2.3')) { - it('Should set pathway hash tag on a span when consuming and promise() was used over a callback', - async () => { - let consumeSpanMeta = {} - const tracePromise = agent.assertSomeTraces(traces => { - const span = traces[0][0] - - if (span.name === 'aws.request' && span.meta['aws.operation'] === 'receiveMessage') { - consumeSpanMeta = span.meta - } - - assertObjectContains(consumeSpanMeta, { - 'pathway.hash': expectedConsumerHash - }) - }) - - await sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsm }).promise() - await sqs.receiveMessage({ QueueUrl: QueueUrlDsm }).promise() - - return tracePromise - }) - } - - it('Should emit DSM stats to the agent when sending a message', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 1 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 1) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) - }).then(done, done) - - sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsm }, () => {}) - }) - - it('Should emit DSM stats to the agent when receiving a message', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 2 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 2) - assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash), true) - }, { timeoutMs: 5000 }).then(done, done) - - sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsm }, () => { - sqs.receiveMessage({ QueueUrl: QueueUrlDsm, MessageAttributeNames: ['.*'] }, () => {}) - }) - }) - - it('Should emit DSM stats when receiving a message when the producer was not instrumented', done => { - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 2 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.strictEqual(statsPointsReceived, 1) - assert.strictEqual(agent.dsmStatsExistWithParentHash(agent, '0'), true) - }).then(done, done) - - agent.reload('aws-sdk', { sqs: { dsmEnabled: false } }, { dsmEnabled: false }) - sqs.sendMessage({ MessageBody: 'test DSM', QueueUrl: QueueUrlDsmConsumerOnly }, () => { - agent.reload('aws-sdk', { sqs: { dsmEnabled: true } }, { dsmEnabled: true }) - sqs.receiveMessage({ QueueUrl: QueueUrlDsmConsumerOnly, MessageAttributeNames: ['.*'] }, () => {}) - }) - }) - - it('Should emit DSM stats to the agent when sending batch messages', done => { - // we need to stub Date.now() to ensure a new stats bucket is created for each call - // otherwise, all stats checkpoints will be combined into a single stats points - let now = Date.now() - nowStub = sinon.stub(Date, 'now') - nowStub.callsFake(() => { - now += 1000000 - return now - }) - - agent.expectPipelineStats(dsmStats => { - let statsPointsReceived = 0 - // we should have 3 dsm stats points - dsmStats.forEach((timeStatsBucket) => { - if (timeStatsBucket && timeStatsBucket.Stats) { - timeStatsBucket.Stats.forEach((statsBuckets) => { - statsPointsReceived += statsBuckets.Stats.length - }) - } - }) - assert.ok(statsPointsReceived >= 3) - assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash), true) - }).then(done, done) - - sqs.sendMessageBatch( - { - Entries: [ - { - Id: '1', - MessageBody: 'test DSM 1' - }, - { - Id: '2', - MessageBody: 'test DSM 2' - }, - { - Id: '3', - MessageBody: 'test DSM 3' - } - ], - QueueUrl: QueueUrlDsm - }, () => { - nowStub.restore() - }) - }) - }) }) }) }) diff --git a/packages/datadog-plugin-axios/test/integration-test/client.spec.js b/packages/datadog-plugin-axios/test/integration-test/client.spec.js index b23af4f0aa9..d8f13ac4688 100644 --- a/packages/datadog-plugin-axios/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-axios/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') describe('esm', () => { let agent @@ -33,7 +33,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'http.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-azure-event-hubs/src/index.js b/packages/datadog-plugin-azure-event-hubs/src/index.js index 6ce379cc2e3..3f78066693a 100644 --- a/packages/datadog-plugin-azure-event-hubs/src/index.js +++ b/packages/datadog-plugin-azure-event-hubs/src/index.js @@ -1,7 +1,7 @@ 'use strict' -const ProducerPlugin = require('./producer') const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const ProducerPlugin = require('./producer') class AzureEventHubsPlugin extends CompositePlugin { static get id () { return 'azure-event-hubs' } diff --git a/packages/datadog-plugin-azure-event-hubs/src/producer.js b/packages/datadog-plugin-azure-event-hubs/src/producer.js index 44e38aba99d..5867edc2e5a 100644 --- a/packages/datadog-plugin-azure-event-hubs/src/producer.js +++ b/packages/datadog-plugin-azure-event-hubs/src/producer.js @@ -3,6 +3,8 @@ const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const ProducerPlugin = require('../../dd-trace/src/plugins/producer') +const spanContexts = new WeakMap() + class AzureEventHubsProducerPlugin extends ProducerPlugin { static get id () { return 'azure-event-hubs' } static get operation () { return 'send' } @@ -36,7 +38,12 @@ class AzureEventHubsProducerPlugin extends ProducerPlugin { } if (batchLinksAreEnabled()) { - ctx.batch._spanContexts.push(span.context()) + const spanContext = spanContexts.get(ctx.batch) + if (spanContext) { + spanContext.push(span.context()) + } else { + spanContexts.set(ctx.batch, [span.context()]) + } injectTraceContext(this.tracer, span, ctx.eventData) } } @@ -53,9 +60,12 @@ class AzureEventHubsProducerPlugin extends ProducerPlugin { }) } else { if (batchLinksAreEnabled()) { - eventData._spanContexts.forEach(spanContext => { - span.addLink(spanContext) - }) + const contexts = spanContexts.get(eventData) + if (contexts) { + for (const spanContext of contexts) { + span.addLink(spanContext) + } + } } } } @@ -63,7 +73,11 @@ class AzureEventHubsProducerPlugin extends ProducerPlugin { } asyncEnd (ctx) { - super.finish() + super.finish(ctx) + } + + end (ctx) { + super.finish(ctx) } } diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/client.spec.js b/packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/client.spec.js new file mode 100644 index 00000000000..954f12474e8 --- /dev/null +++ b/packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/client.spec.js @@ -0,0 +1,47 @@ +'use strict' + +const assert = require('assert') +const { + FakeAgent, + sandboxCwd, + useSandbox, + spawnPluginIntegrationTestProcAndExpectExit +} = require('../../../../../integration-tests/helpers') +const { withVersions } = require('../../../../dd-trace/test/setup/mocha') + +describe('esm', () => { + let agent + let proc + let spawnEnv + + withVersions('azure-event-hubs', '@azure/event-hubs', version => { + useSandbox([`'@azure/event-hubs@${version}'`], false, [ + './packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/*']) + + beforeEach(async () => { + agent = await new FakeAgent().start() + process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' + spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000', NODE_OPTIONS: '--experimental-global-webcrypto' } + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + it('tryAdd does not set context in the Azure eventDataBatch._spanContext', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.ok(Array.isArray(payload)) + assert.strictEqual(payload.length, 3) + // Verify we got the expected spans from the test + assert.strictEqual(payload[0][0].name, 'azure.eventhubs.create') + assert.strictEqual(payload[1][0].name, 'azure.eventhubs.create') + assert.strictEqual(payload[2][0].name, 'azure.eventhubs.send') + }) + + // This test file will throw an error if tryAdd returns a Promise instead of a boolean + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) + await res + }).timeout(60000) + }) +}) diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/server.mjs b/packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/server.mjs new file mode 100644 index 00000000000..15b63e7a05f --- /dev/null +++ b/packages/datadog-plugin-azure-event-hubs/test/integration-test/batchSpanContextRegressionTest/server.mjs @@ -0,0 +1,29 @@ +import 'dd-trace/init.js' +import { EventHubProducerClient } from '@azure/event-hubs' + +const connectionString = 'Endpoint=sb://127.0.0.1:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;' +const eventHubName = 'eh1' + +const producer = new EventHubProducerClient(connectionString, eventHubName) + +const events = [ + { body: 'Test event 1' }, + { body: 'Test event 2' } +] + +// Test that tryAdd returns a boolean, not a Promise +const batch = await producer.createBatch() + +batch.tryAdd(events[0]) +batch.tryAdd(events[1]) + +if (batch._spanContexts.length !== 0) { + throw new Error( + "We should not be using Azure's eventDataBatchspan context. Please use the weak map instead." + ) +} + +// Send the batch to complete the operation +await producer.sendBatch(batch) + +await producer.close() diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/client.spec.js b/packages/datadog-plugin-azure-event-hubs/test/integration-test/client.spec.js deleted file mode 100644 index 732dfe93c66..00000000000 --- a/packages/datadog-plugin-azure-event-hubs/test/integration-test/client.spec.js +++ /dev/null @@ -1,104 +0,0 @@ -'use strict' - -const assert = require('node:assert/strict') - -const { afterEach, beforeEach, describe, it } = require('mocha') -const { - FakeAgent, - sandboxCwd, - useSandbox, - checkSpansForServiceName, - spawnPluginIntegrationTestProc -} = require('../../../../integration-tests/helpers') -const { withVersions } = require('../../../dd-trace/test/setup/mocha') - -const spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000' } - -// TODO: Fix this test / esm issue -describe.skip('esm', () => { - let agent - let proc - - withVersions('azure-event-hubs', '@azure/event-hubs', version => { - useSandbox([`'@azure/event-hubs@${version}'`], false, [ - './packages/datadog-plugin-azure-event-hubs/test/integration-test/*']) - - beforeEach(async () => { - agent = await new FakeAgent().start() - process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' - }) - - afterEach(async () => { - proc && proc.kill() - await agent.stop() - }) - - it('is instrumented', async () => { - const res = agent.assertMessageReceived(({ headers, payload }) => { - assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) - assert.ok(Array.isArray(payload)) - assert.strictEqual(checkSpansForServiceName(payload, 'azure.eventhubs.send'), true) - }) - - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) - await res - }).timeout(20000) - - it('injects context to the message properties', async () => { - const res = agent.assertMessageReceived(({ headers, payload }) => { - // list of EventData - assert.strictEqual(payload.length, 5) - assert.strictEqual(payload[0][0].name, 'azure.eventhubs.send') - assert.strictEqual(payload[0][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[0][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[0][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[0][0].meta['network.destination.name'], '127.0.0.1:5673') - assert.strictEqual(payload[0][0].metrics['messaging.batch.message_count'], 2) - // list of AMPQ messages - assert.strictEqual(payload[1][0].name, 'azure.eventhubs.send') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[1][0].meta['network.destination.name'], '127.0.0.1:5673') - assert.strictEqual(payload[1][0].metrics['messaging.batch.message_count'], 2) - // Batch -> EventDataBatchImpl - assert.strictEqual(payload[2][0].name, 'azure.eventhubs.create') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['network.destination.name'], '127.0.0.1:5673') - assert.strictEqual(payload[3][0].name, 'azure.eventhubs.create') - assert.strictEqual(payload[3][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[3][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[3][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[3][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[3][0].meta['network.destination.name'], '127.0.0.1:5673') - assert.strictEqual(payload[4][0].name, 'azure.eventhubs.send') - assert.strictEqual(payload[4][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[4][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[4][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[4][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[4][0].meta['network.destination.name'], '127.0.0.1:5673') - assert.strictEqual(payload[4][0].metrics['messaging.batch.message_count'], 4) - assert.strictEqual(parseLinks(payload[4][0]).length, 2) - }) - - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) - await res - }).timeout(60000) - - it('does not add span links when they are disabled', async () => { - const res = agent.assertMessageReceived(({ headers, payload }) => { - assert.ok(!('_dd.span_links' in payload[2][0])) - }) - const envVar = { DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: false, ...spawnEnv } - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, undefined, envVar) - await res - }).timeout(60000) - }) -}) - -function parseLinks (span) { - return JSON.parse(span.meta['_dd.span_links'] || '[]') -} diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/core-test/client.spec.js b/packages/datadog-plugin-azure-event-hubs/test/integration-test/core-test/client.spec.js new file mode 100644 index 00000000000..97bba1d26b1 --- /dev/null +++ b/packages/datadog-plugin-azure-event-hubs/test/integration-test/core-test/client.spec.js @@ -0,0 +1,127 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { afterEach, beforeEach, describe, it } = require('mocha') +const { + FakeAgent, + sandboxCwd, + useSandbox, + assertObjectContains, + checkSpansForServiceName, + spawnPluginIntegrationTestProcAndExpectExit +} = require('../../../../../integration-tests/helpers') +const { withVersions } = require('../../../../dd-trace/test/setup/mocha') + +describe('esm', () => { + let agent + let proc + let spawnEnv + + withVersions('azure-event-hubs', '@azure/event-hubs', version => { + useSandbox([`'@azure/event-hubs@${version}'`], false, [ + './packages/datadog-plugin-azure-event-hubs/test/integration-test/core-test/*']) + + beforeEach(async () => { + agent = await new FakeAgent().start() + process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' + spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000', NODE_OPTIONS: '--experimental-global-webcrypto' } + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) + assert.strictEqual(checkSpansForServiceName(payload, 'azure.eventhubs.send'), true) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) + await res + }).timeout(20000) + + it('injects context to the message properties', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + // list of EventData + assert.strictEqual(payload.length, 5) + assertObjectContains(payload[0][0], { + name: 'azure.eventhubs.send', + meta: { + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1:5673' + }, + metrics: { + 'messaging.batch.message_count': 2 + } + }) + // list of AMPQ messages + assertObjectContains(payload[1][0], { + name: 'azure.eventhubs.send', + meta: { + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1:5673' + }, + metrics: { + 'messaging.batch.message_count': 2 + } + }) + // Batch -> EventDataBatchImpl + assertObjectContains(payload[2][0], { + name: 'azure.eventhubs.create', + meta: { + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1:5673' + } + }) + assertObjectContains(payload[3][0], { + name: 'azure.eventhubs.create', + meta: { + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1:5673' + } + }) + assertObjectContains(payload[4][0], { + name: 'azure.eventhubs.send', + meta: { + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1:5673' + }, + metrics: { + 'messaging.batch.message_count': 4 + } + }) + assert.strictEqual(parseLinks(payload[4][0]).length, 2) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) + await res + }).timeout(60000) + + it('does not add span links when they are disabled', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.ok(!('_dd.span_links' in payload[2][0])) + }) + const envVar = { DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: 'false', ...spawnEnv } + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, envVar) + await res + }).timeout(60000) + }) +}) + +function parseLinks (span) { + return JSON.parse(span.meta['_dd.span_links'] || '[]') +} diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/server.mjs b/packages/datadog-plugin-azure-event-hubs/test/integration-test/core-test/server.mjs similarity index 100% rename from packages/datadog-plugin-azure-event-hubs/test/integration-test/server.mjs rename to packages/datadog-plugin-azure-event-hubs/test/integration-test/core-test/server.mjs diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/client.spec.js b/packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/client.spec.js new file mode 100644 index 00000000000..8709403f2be --- /dev/null +++ b/packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/client.spec.js @@ -0,0 +1,50 @@ +'use strict' + +const assert = require('assert') +const { + FakeAgent, + sandboxCwd, + useSandbox, + spawnPluginIntegrationTestProcAndExpectExit +} = require('../../../../../integration-tests/helpers') +const { withVersions } = require('../../../../dd-trace/test/setup/mocha') + +describe('esm', () => { + let agent + let proc + let spawnEnv + + withVersions('azure-event-hubs', '@azure/event-hubs', version => { + useSandbox([`'@azure/event-hubs@${version}'`], false, [ + './packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/*']) + + beforeEach(async () => { + agent = await new FakeAgent().start() + process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' + spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000', NODE_OPTIONS: '--experimental-global-webcrypto' } + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + it('tryAdd returns a boolean, not a Promise', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.ok(Array.isArray(payload)) + assert.strictEqual(payload.length, 3) + // Verify we got the expected spans from the test + assert.strictEqual(payload[0][0].name, 'azure.eventhubs.create') + assert.strictEqual(payload[1][0].name, 'azure.eventhubs.create') + assert.strictEqual(payload[2][0].name, 'azure.eventhubs.send') + }) + + // This test file will throw an error if tryAdd returns a Promise instead of a boolean + proc = await spawnPluginIntegrationTestProcAndExpectExit( + sandboxCwd(), 'server.mjs', agent.port, spawnEnv + ) + + await res + }).timeout(60000) + }) +}) diff --git a/packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/server.mjs b/packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/server.mjs new file mode 100644 index 00000000000..4c24f27bd3c --- /dev/null +++ b/packages/datadog-plugin-azure-event-hubs/test/integration-test/tryAddRegressionTest/server.mjs @@ -0,0 +1,49 @@ +import 'dd-trace/init.js' +import { EventHubProducerClient } from '@azure/event-hubs' + +const connectionString = 'Endpoint=sb://127.0.0.1:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;' +const eventHubName = 'eh1' + +const producer = new EventHubProducerClient(connectionString, eventHubName) + +const events = [ + { body: 'Test event 1' }, + { body: 'Test event 2' } +] + +// Test that tryAdd returns a boolean, not a Promise +const batch = await producer.createBatch() + +const result1 = batch.tryAdd(events[0]) +const result2 = batch.tryAdd(events[1]) + +// Verify the return types - throw error if not correct +if (typeof result1 !== 'boolean') { + throw new Error(`tryAdd should return a boolean, but returned ${typeof result1}`) +} + +if (result1 instanceof Promise) { + throw new Error('tryAdd should not return a Promise') +} + +if (typeof result2 !== 'boolean') { + throw new Error(`tryAdd should return a boolean, but returned ${typeof result2}`) +} + +if (result2 instanceof Promise) { + throw new Error('tryAdd should not return a Promise') +} + +// Verify the values are correct +if (result1 !== true) { + throw new Error(`Expected first tryAdd to return true, got ${result1}`) +} + +if (result2 !== true) { + throw new Error(`Expected second tryAdd to return true, got ${result2}`) +} + +// Send the batch to complete the operation +await producer.sendBatch(batch) + +await producer.close() diff --git a/packages/datadog-plugin-azure-functions/test/integration-test/eventhubs-test/eventhubs.spec.js b/packages/datadog-plugin-azure-functions/test/integration-test/eventhubs-test/eventhubs.spec.js index 87ab3eb19a6..c1762607a53 100644 --- a/packages/datadog-plugin-azure-functions/test/integration-test/eventhubs-test/eventhubs.spec.js +++ b/packages/datadog-plugin-azure-functions/test/integration-test/eventhubs-test/eventhubs.spec.js @@ -2,15 +2,16 @@ const assert = require('node:assert/strict') +const { spawn } = require('child_process') const { FakeAgent, + assertObjectContains, hookFile, sandboxCwd, useSandbox, curlAndAssertMessage, } = require('../../../../../integration-tests/helpers') const { withVersions } = require('../../../../dd-trace/test/setup/mocha') -const { spawn } = require('child_process') const { NODE_MAJOR } = require('../../../../../version') describe('esm', () => { @@ -46,19 +47,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh1-eventdata', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -71,19 +80,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh1-amqpmessages', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -95,19 +112,27 @@ describe('esm', () => { proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'func', ['start'], agent.port, undefined, envArgs) return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh1-batch', ({ headers, payload }) => { - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -120,12 +145,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh2-eventdata', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -138,12 +167,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh2-amqpmessages', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -156,12 +189,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh2-batch', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -174,12 +211,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh1-enqueueEvent', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) }) }).timeout(60000) @@ -192,19 +233,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh1-enqueueEvents', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -217,19 +266,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh1-enqueueAmqp', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'EventHubs eventHubTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'eh1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -242,12 +299,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh2-enqueueEvent', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) }) }).timeout(60000) @@ -264,12 +325,16 @@ describe('esm', () => { assert.strictEqual(payload[0][2].name, 'azure.eventhubs.create') assert.strictEqual(payload[0][3].name, 'azure.eventhubs.send') assert.strictEqual(parseLinks(payload[0][3]).length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -286,12 +351,16 @@ describe('esm', () => { assert.strictEqual(payload[0][2].name, 'azure.eventhubs.create') assert.strictEqual(payload[0][3].name, 'azure.eventhubs.send') assert.strictEqual(parseLinks(payload[0][3]).length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'EventHubs eventHubTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'eventhubs') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'eh2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'EventHubs eventHubTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'eventhubs', + 'messaging.destination.name': 'eh2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -299,7 +368,7 @@ describe('esm', () => { it('should add span links to non-batched messages when batch links are disabled', async () => { const envArgs = { PATH: `${sandboxCwd()}/node_modules/azure-functions-core-tools/bin:${process.env.PATH}`, - DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: false + DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: 'false' } proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'func', ['start'], agent.port, undefined, envArgs) return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh2-eventdata', ({ headers, payload }) => { @@ -310,7 +379,7 @@ describe('esm', () => { it('should not create a tryAdd span or add span links to batches when batch links are disabled', async () => { const envArgs = { PATH: `${sandboxCwd()}/node_modules/azure-functions-core-tools/bin:${process.env.PATH}`, - DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: false + DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: 'false' } proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'func', ['start'], agent.port, undefined, envArgs) return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/eh2-batch', ({ headers, payload }) => { diff --git a/packages/datadog-plugin-azure-functions/test/integration-test/http-test/client.spec.js b/packages/datadog-plugin-azure-functions/test/integration-test/http-test/client.spec.js index 8d7ee02899b..6be1e951d6c 100644 --- a/packages/datadog-plugin-azure-functions/test/integration-test/http-test/client.spec.js +++ b/packages/datadog-plugin-azure-functions/test/integration-test/http-test/client.spec.js @@ -2,6 +2,7 @@ const assert = require('node:assert/strict') +const { spawn } = require('child_process') const { FakeAgent, hookFile, @@ -10,7 +11,6 @@ const { curlAndAssertMessage } = require('../../../../../integration-tests/helpers') const { withVersions } = require('../../../../dd-trace/test/setup/mocha') -const { spawn } = require('child_process') const { NODE_MAJOR } = require('../../../../../version') describe('esm', () => { diff --git a/packages/datadog-plugin-azure-functions/test/integration-test/servicebus-test/servicebus.spec.js b/packages/datadog-plugin-azure-functions/test/integration-test/servicebus-test/servicebus.spec.js index 4122c847061..ca3f43b99fa 100644 --- a/packages/datadog-plugin-azure-functions/test/integration-test/servicebus-test/servicebus.spec.js +++ b/packages/datadog-plugin-azure-functions/test/integration-test/servicebus-test/servicebus.spec.js @@ -2,15 +2,16 @@ const assert = require('node:assert/strict') +const { spawn } = require('child_process') const { FakeAgent, + assertObjectContains, hookFile, sandboxCwd, useSandbox, curlAndAssertMessage } = require('../../../../../integration-tests/helpers') const { withVersions } = require('../../../../dd-trace/test/setup/mocha') -const { spawn } = require('child_process') const { NODE_MAJOR } = require('../../../../../version') describe('esm', () => { @@ -46,12 +47,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-message-1', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) }) }).timeout(60000) @@ -64,19 +69,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-messages-1', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -89,12 +102,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-amqp-message-1', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) }) }).timeout(60000) @@ -107,19 +124,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-amqp-messages-1', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -132,19 +157,27 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-message-batch-1', ({ headers, payload }) => { assert.strictEqual(payload.length, 3) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) - assert.strictEqual(payload[2][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[2][0].resource, 'ServiceBus queueTest1') - assert.strictEqual(payload[2][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[2][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[2][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[2][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[2][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest1', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[2][0]).length, 1) }) }).timeout(60000) @@ -157,12 +190,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-message-2', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) }) }).timeout(60000) @@ -175,12 +212,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-messages-2', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -193,12 +234,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-amqp-message-2', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 1) }) }).timeout(60000) @@ -211,12 +256,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-amqp-messages-2', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -229,12 +278,16 @@ describe('esm', () => { return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-message-batch-2', ({ headers, payload }) => { assert.strictEqual(payload.length, 2) - assert.strictEqual(payload[1][0].name, 'azure.functions.invoke') - assert.strictEqual(payload[1][0].resource, 'ServiceBus queueTest2') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'receive') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.destination.name'], 'queue.2') - assert.strictEqual(payload[1][0].meta['span.kind'], 'consumer') + assertObjectContains(payload[1][0], { + name: 'azure.functions.invoke', + resource: 'ServiceBus queueTest2', + meta: { + 'messaging.operation': 'receive', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.2', + 'span.kind': 'consumer' + } + }) assert.strictEqual(parseLinks(payload[1][0]).length, 2) }) }).timeout(60000) @@ -242,7 +295,7 @@ describe('esm', () => { it('should not create a tryAdd span or add span links to arrays when batch links are disabled', async () => { const envArgs = { PATH: `${sandboxCwd()}/node_modules/azure-functions-core-tools/bin:${process.env.PATH}`, - DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED: false + DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED: 'false' } proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'func', ['start'], agent.port, undefined, envArgs) return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-messages-2', ({ headers, payload }) => { @@ -255,7 +308,7 @@ describe('esm', () => { it('should not create a tryAdd span or add span links to batches when batch links are disabled', async () => { const envArgs = { PATH: `${sandboxCwd()}/node_modules/azure-functions-core-tools/bin:${process.env.PATH}`, - DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED: false + DD_TRACE_AZURE_SERVICEBUS_BATCH_LINKS_ENABLED: 'false' } proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'func', ['start'], agent.port, undefined, envArgs) return curlAndAssertMessage(agent, 'http://127.0.0.1:7071/api/send-message-batch-2', ({ headers, payload }) => { diff --git a/packages/datadog-plugin-azure-service-bus/src/index.js b/packages/datadog-plugin-azure-service-bus/src/index.js index f2ec918ba15..092153fb994 100644 --- a/packages/datadog-plugin-azure-service-bus/src/index.js +++ b/packages/datadog-plugin-azure-service-bus/src/index.js @@ -1,7 +1,7 @@ 'use strict' -const ProducerPlugin = require('./producer') const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const ProducerPlugin = require('./producer') class AzureServiceBusPlugin extends CompositePlugin { static get id () { return 'azure-service-bus' } diff --git a/packages/datadog-plugin-azure-service-bus/src/producer.js b/packages/datadog-plugin-azure-service-bus/src/producer.js index 8fdd67f4cc7..2fdec35f6b5 100644 --- a/packages/datadog-plugin-azure-service-bus/src/producer.js +++ b/packages/datadog-plugin-azure-service-bus/src/producer.js @@ -75,6 +75,10 @@ class AzureServiceBusProducerPlugin extends ProducerPlugin { asyncEnd (ctx) { super.finish(ctx) } + + end (ctx) { + super.finish(ctx) + } } function injectTraceContext (tracer, span, msg) { diff --git a/packages/datadog-plugin-azure-service-bus/test/integration-test/client.spec.js b/packages/datadog-plugin-azure-service-bus/test/integration-test/client.spec.js deleted file mode 100644 index b11f370c444..00000000000 --- a/packages/datadog-plugin-azure-service-bus/test/integration-test/client.spec.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict' - -const assert = require('node:assert/strict') - -const { - FakeAgent, - sandboxCwd, - useSandbox, - spawnPluginIntegrationTestProc -} = require('../../../../integration-tests/helpers') -const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000' } - -describe('esm', () => { - let agent - let proc - - withVersions('azure-service-bus', '@azure/service-bus', version => { - useSandbox([`'@azure/service-bus@${version}'`], false, [ - './packages/datadog-plugin-azure-service-bus/test/integration-test/*']) - - beforeEach(async () => { - agent = await new FakeAgent().start() - process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' - }) - - afterEach(async () => { - proc && proc.kill() - await agent.stop() - }) - - it('is instrumented', async () => { - const res = agent.assertMessageReceived(({ headers, payload }) => { - assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) - assert.ok(Array.isArray(payload)) - }) - - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) - - await res - }).timeout(20000) - - it('injects context to the message properties', async () => { - const res = agent.assertMessageReceived(({ headers, payload }) => { - assert.strictEqual(payload.length, 23) - // queue message - assert.strictEqual(payload[0][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[0][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[0][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[0][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[0][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[0][0].meta['network.destination.name'], '127.0.0.1') - // queue array of messages - assert.strictEqual(payload[1][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[1][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[1][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[1][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[1][0].meta['network.destination.name'], '127.0.0.1') - assert.strictEqual(payload[2][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[3][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[3][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[3][0].meta['messaging.destination.name'], 'queue.1') - // queue amqp messages - assert.strictEqual(payload[1][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[4][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[4][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[4][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[4][0].meta['network.destination.name'], '127.0.0.1') - assert.strictEqual(payload[5][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[6][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[6][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[6][0].meta['messaging.destination.name'], 'queue.1') - - // topic message - assert.strictEqual(payload[7][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[7][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[7][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[7][0].meta['messaging.destination.name'], 'topic.1') - assert.strictEqual(payload[7][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[7][0].meta['network.destination.name'], '127.0.0.1') - // topic array of messages - assert.strictEqual(payload[8][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[8][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[8][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[8][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[8][0].meta['network.destination.name'], '127.0.0.1') - assert.strictEqual(payload[9][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[10][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[10][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[10][0].meta['messaging.destination.name'], 'topic.1') - - // topic amqp messages - assert.strictEqual(payload[11][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[11][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[11][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[11][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[11][0].meta['network.destination.name'], '127.0.0.1') - assert.strictEqual(payload[12][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[13][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[13][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[13][0].meta['messaging.destination.name'], 'topic.1') - // scheduled message - assert.strictEqual(payload[14][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[14][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[14][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[14][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[14][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[14][0].meta['network.destination.name'], '127.0.0.1') - // scheduled array of messages - assert.strictEqual(payload[15][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[15][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[15][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[15][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[15][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[15][0].meta['network.destination.name'], '127.0.0.1') - // scheduled amqp messages - assert.strictEqual(payload[16][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[16][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[16][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[16][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(payload[16][0].meta['messaging.operation'], 'send') - assert.strictEqual(payload[16][0].meta['network.destination.name'], '127.0.0.1') - - // queue batch - assert.strictEqual(payload[17][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[17][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[17][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[17][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[17][0].meta['network.destination.name'], '127.0.0.1') - assert.strictEqual(payload[18][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[19][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[19][0].metrics['messaging.batch.message_count'], 2) - assert.strictEqual(payload[19][0].meta['messaging.destination.name'], 'queue.1') - assert.strictEqual(parseLinks(payload[19][0]).length, 2) - - // topic batch - assert.strictEqual(payload[20][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[20][0].meta['span.kind'], 'producer') - assert.strictEqual(payload[20][0].meta['messaging.system'], 'servicebus') - assert.strictEqual(payload[20][0].meta['messaging.operation'], 'create') - assert.strictEqual(payload[20][0].meta['network.destination.name'], '127.0.0.1') - assert.strictEqual(payload[21][0].name, 'azure.servicebus.create') - assert.strictEqual(payload[22][0].name, 'azure.servicebus.send') - assert.strictEqual(payload[22][0].metrics['messaging.batch.message_count'], 2) - assert.strictEqual(payload[22][0].meta['messaging.destination.name'], 'topic.1') - assert.strictEqual(parseLinks(payload[22][0]).length, 2) - }) - - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) - - await res - }).timeout(60000) - }) -}) - -function parseLinks (span) { - return JSON.parse(span.meta['_dd.span_links'] || '[]') -} diff --git a/packages/datadog-plugin-azure-service-bus/test/integration-test/core-test/client.spec.js b/packages/datadog-plugin-azure-service-bus/test/integration-test/core-test/client.spec.js new file mode 100644 index 00000000000..adcf44b39e8 --- /dev/null +++ b/packages/datadog-plugin-azure-service-bus/test/integration-test/core-test/client.spec.js @@ -0,0 +1,232 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { + FakeAgent, + assertObjectContains, + sandboxCwd, + useSandbox, + spawnPluginIntegrationTestProcAndExpectExit +} = require('../../../../../integration-tests/helpers') +const { withVersions } = require('../../../../dd-trace/test/setup/mocha') + +const spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000' } + +describe('esm', () => { + let agent + let proc + + withVersions('azure-service-bus', '@azure/service-bus', version => { + useSandbox([`'@azure/service-bus@${version}'`], false, [ + './packages/datadog-plugin-azure-service-bus/test/integration-test/core-test/*']) + + beforeEach(async () => { + agent = await new FakeAgent().start() + process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) + + await res + }).timeout(20000) + + it('injects context to the message properties', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(payload.length, 23) + // queue message + assertObjectContains(payload[0][0], { + name: 'azure.servicebus.send', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1' + } + }) + // queue array of messages + assertObjectContains(payload[1][0], { + name: 'azure.servicebus.create', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1' + } + }) + assert.strictEqual(payload[2][0].name, 'azure.servicebus.create') + assertObjectContains(payload[3][0], { + name: 'azure.servicebus.send', + meta: { + 'messaging.operation': 'send', + 'messaging.destination.name': 'queue.1' + } + }) + // queue amqp messages + assert.strictEqual(payload[1][0].name, 'azure.servicebus.create') + assertObjectContains(payload[4][0], { + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1' + } + }) + assert.strictEqual(payload[5][0].name, 'azure.servicebus.create') + assertObjectContains(payload[6][0], { + name: 'azure.servicebus.send', + meta: { + 'messaging.operation': 'send', + 'messaging.destination.name': 'queue.1' + } + }) + + // topic message + assertObjectContains(payload[7][0], { + name: 'azure.servicebus.send', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'topic.1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1' + } + }) + // topic array of messages + assertObjectContains(payload[8][0], { + name: 'azure.servicebus.create', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1' + } + }) + assert.strictEqual(payload[9][0].name, 'azure.servicebus.create') + assertObjectContains(payload[10][0], { + name: 'azure.servicebus.send', + meta: { + 'messaging.operation': 'send', + 'messaging.destination.name': 'topic.1' + } + }) + + // topic amqp messages + assertObjectContains(payload[11][0], { + name: 'azure.servicebus.create', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1' + } + }) + assert.strictEqual(payload[12][0].name, 'azure.servicebus.create') + assertObjectContains(payload[13][0], { + name: 'azure.servicebus.send', + meta: { + 'messaging.operation': 'send', + 'messaging.destination.name': 'topic.1' + } + }) + // scheduled message + assertObjectContains(payload[14][0], { + name: 'azure.servicebus.send', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1' + } + }) + // scheduled array of messages + assertObjectContains(payload[15][0], { + name: 'azure.servicebus.send', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1' + } + }) + // scheduled amqp messages + assertObjectContains(payload[16][0], { + name: 'azure.servicebus.send', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.destination.name': 'queue.1', + 'messaging.operation': 'send', + 'network.destination.name': '127.0.0.1' + } + }) + + // queue batch + assertObjectContains(payload[17][0], { + name: 'azure.servicebus.create', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1' + } + }) + assert.strictEqual(payload[18][0].name, 'azure.servicebus.create') + assertObjectContains(payload[19][0], { + name: 'azure.servicebus.send', + meta: { + 'messaging.destination.name': 'queue.1' + }, + metrics: { + 'messaging.batch.message_count': 2 + } + }) + assert.strictEqual(parseLinks(payload[19][0]).length, 2) + + // topic batch + assertObjectContains(payload[20][0], { + name: 'azure.servicebus.create', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'servicebus', + 'messaging.operation': 'create', + 'network.destination.name': '127.0.0.1' + } + }) + assert.strictEqual(payload[21][0].name, 'azure.servicebus.create') + assertObjectContains(payload[22][0], { + name: 'azure.servicebus.send', + meta: { + 'messaging.destination.name': 'topic.1' + }, + metrics: { + 'messaging.batch.message_count': 2 + } + }) + assert.strictEqual(parseLinks(payload[22][0]).length, 2) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) + + await res + }).timeout(60000) + }) +}) + +function parseLinks (span) { + return JSON.parse(span.meta['_dd.span_links'] || '[]') +} diff --git a/packages/datadog-plugin-azure-service-bus/test/integration-test/server.mjs b/packages/datadog-plugin-azure-service-bus/test/integration-test/core-test/server.mjs similarity index 100% rename from packages/datadog-plugin-azure-service-bus/test/integration-test/server.mjs rename to packages/datadog-plugin-azure-service-bus/test/integration-test/core-test/server.mjs diff --git a/packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/client.spec.js b/packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/client.spec.js new file mode 100644 index 00000000000..ce816b03dcc --- /dev/null +++ b/packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/client.spec.js @@ -0,0 +1,48 @@ +'use strict' + +const assert = require('assert') +const { + FakeAgent, + sandboxCwd, + useSandbox, + spawnPluginIntegrationTestProcAndExpectExit +} = require('../../../../../integration-tests/helpers') +const { withVersions } = require('../../../../dd-trace/test/setup/mocha') + +const spawnEnv = { DD_TRACE_FLUSH_INTERVAL: '2000' } + +describe('esm', () => { + let agent + let proc + + withVersions('azure-service-bus', '@azure/service-bus', version => { + useSandbox([`'@azure/service-bus@${version}'`], false, [ + './packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/*']) + + beforeEach(async () => { + agent = await new FakeAgent().start() + process.env.DD_TRACE_DISABLED_PLUGINS = 'amqplib,amqp10,rhea,net' + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + it('tryAddMessage returns a boolean, not a Promise', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.ok(Array.isArray(payload)) + assert.strictEqual(payload.length, 3) + // Verify we got the expected spans from the test + assert.strictEqual(payload[0][0].name, 'azure.servicebus.create') + assert.strictEqual(payload[1][0].name, 'azure.servicebus.create') + assert.strictEqual(payload[2][0].name, 'azure.servicebus.send') + }) + + // This test file will throw an error if tryAddMessage returns a Promise instead of a boolean + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, spawnEnv) + + await res + }).timeout(60000) + }) +}) diff --git a/packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/server.mjs b/packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/server.mjs new file mode 100644 index 00000000000..84e6a9414ec --- /dev/null +++ b/packages/datadog-plugin-azure-service-bus/test/integration-test/tryAddMessageRegressionTest/server.mjs @@ -0,0 +1,51 @@ +import 'dd-trace/init.js' +import { ServiceBusClient } from '@azure/service-bus' + +const connectionString = 'Endpoint=sb://127.0.0.1;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;' +const queueName = 'queue.1' + +const client = new ServiceBusClient(connectionString) +const sender = client.createSender(queueName) + +const messages = [ + { body: 'Test message 1' }, + { body: 'Test message 2' } +] + +// Test that tryAddMessage returns a boolean, not a Promise +const batch = await sender.createMessageBatch() + +const result1 = batch.tryAddMessage(messages[0]) +const result2 = batch.tryAddMessage(messages[1]) + +// Verify the return types - throw error if not correct +if (typeof result1 !== 'boolean') { + throw new Error(`tryAddMessage should return a boolean, but returned ${typeof result1}`) +} + +if (result1 instanceof Promise) { + throw new Error('tryAddMessage should not return a Promise') +} + +if (typeof result2 !== 'boolean') { + throw new Error(`tryAddMessage should return a boolean, but returned ${typeof result2}`) +} + +if (result2 instanceof Promise) { + throw new Error('tryAddMessage should not return a Promise') +} + +// Verify the values are correct +if (result1 !== true) { + throw new Error(`Expected first tryAddMessage to return true, got ${result1}`) +} + +if (result2 !== true) { + throw new Error(`Expected second tryAddMessage to return true, got ${result2}`) +} + +// Send the batch to complete the operation +await sender.sendMessages(batch) + +await sender.close() +await client.close() diff --git a/packages/datadog-plugin-bullmq/src/consumer.js b/packages/datadog-plugin-bullmq/src/consumer.js new file mode 100644 index 00000000000..1aa53eb8bf1 --- /dev/null +++ b/packages/datadog-plugin-bullmq/src/consumer.js @@ -0,0 +1,60 @@ +'use strict' + +const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') +const { getMessageSize } = require('../../dd-trace/src/datastreams') + +class BullmqConsumerPlugin extends ConsumerPlugin { + static id = 'bullmq' + static prefix = 'tracing:orchestrion:bullmq:Worker_callProcessJob' + + asyncEnd (ctx) { + ctx.currentStore?.span?.finish() + } + + bindStart (ctx) { + const job = ctx.arguments?.[0] + const queueName = job?.queueName || job?.queue?.name || 'bullmq' + + let childOf + const datadogContext = job?.data?._datadog + if (datadogContext) { + childOf = this.tracer.extract('text_map', datadogContext) + } + + const span = this.startSpan({ + childOf, + resource: queueName, + meta: { + component: 'bullmq', + 'span.kind': 'consumer', + 'messaging.system': 'bullmq', + 'messaging.destination.name': queueName, + 'messaging.operation': 'process' + } + }, ctx) + + if (this.config.dsmEnabled) { + this.setConsumerCheckpoint(span, ctx) + } + + return ctx.currentStore + } + + setConsumerCheckpoint (span, ctx) { + const job = ctx.arguments?.[0] + if (!job) return + + const queueName = job.queueName || job.queue?.name || 'bullmq' + const payloadSize = job.data ? getMessageSize(job.data) : 0 + + const datadogContext = job.data?._datadog + if (datadogContext) { + this.tracer.decodeDataStreamsContext(datadogContext) + } + + const edgeTags = ['direction:in', `topic:${queueName}`, 'type:bullmq'] + this.tracer.setCheckpoint(edgeTags, span, payloadSize) + } +} + +module.exports = BullmqConsumerPlugin diff --git a/packages/datadog-plugin-bullmq/src/index.js b/packages/datadog-plugin-bullmq/src/index.js new file mode 100644 index 00000000000..d1b11d681a2 --- /dev/null +++ b/packages/datadog-plugin-bullmq/src/index.js @@ -0,0 +1,18 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const BullmqProducerPlugins = require('./producer') +const BullmqConsumerPlugin = require('./consumer') + +class BullmqPlugin extends CompositePlugin { + static id = 'bullmq' + + static plugins = { + queueAdd: BullmqProducerPlugins[0], + queueAddBulk: BullmqProducerPlugins[1], + flowProducerAdd: BullmqProducerPlugins[2], + consumer: BullmqConsumerPlugin + } +} + +module.exports = BullmqPlugin diff --git a/packages/datadog-plugin-bullmq/src/producer.js b/packages/datadog-plugin-bullmq/src/producer.js new file mode 100644 index 00000000000..f30e5333851 --- /dev/null +++ b/packages/datadog-plugin-bullmq/src/producer.js @@ -0,0 +1,178 @@ +'use strict' + +const ProducerPlugin = require('../../dd-trace/src/plugins/producer') +const { DsmPathwayCodec, getMessageSize } = require('../../dd-trace/src/datastreams') + +class BaseBullmqProducerPlugin extends ProducerPlugin { + static id = 'bullmq' + + asyncEnd (ctx) { + ctx.currentStore?.span?.finish() + } + + bindStart (ctx) { + const { resource, meta } = this.getSpanData(ctx) + const span = this.startSpan({ + resource, + meta: { + component: 'bullmq', + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'messaging.operation': 'publish', + ...meta + } + }, ctx) + + this.injectTraceContext(span, ctx) + + if (this.config.dsmEnabled) { + this.setProducerCheckpoint(span, ctx) + } + + return ctx.currentStore + } + + getSpanData (ctx) { + throw new Error('getSpanData must be implemented by subclass') + } + + injectTraceContext (span, ctx) { + throw new Error('injectTraceContext must be implemented by subclass') + } + + setProducerCheckpoint (span, ctx) { + const { queueName, payloadSize, injectTarget } = this.getDsmData(ctx) + const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq'] + const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize) + if (injectTarget && typeof injectTarget === 'object') { + injectTarget._datadog = injectTarget._datadog || {} + DsmPathwayCodec.encode(dataStreamsContext, injectTarget._datadog) + } + } + + getDsmData (ctx) { + throw new Error('getDsmData must be implemented by subclass') + } +} + +class QueueAddPlugin extends BaseBullmqProducerPlugin { + static prefix = 'tracing:orchestrion:bullmq:Queue_add' + + getSpanData (ctx) { + const queueName = ctx.self?.name || 'bullmq' + return { + resource: queueName, + meta: { + 'messaging.destination.name': ctx.self?.name + } + } + } + + injectTraceContext (span, ctx) { + const data = ctx.arguments?.[1] + if (data?.constructor?.name === 'Object') { + data._datadog = data._datadog || {} + this.tracer.inject(span, 'text_map', data._datadog) + } + } + + getDsmData (ctx) { + const data = ctx.arguments?.[1] + return { + queueName: ctx.self?.name || 'bullmq', + payloadSize: data ? getMessageSize(data) : 0, + injectTarget: data + } + } +} + +class QueueAddBulkPlugin extends BaseBullmqProducerPlugin { + static prefix = 'tracing:orchestrion:bullmq:Queue_addBulk' + + operationName () { + return 'bullmq.addBulk' + } + + getSpanData (ctx) { + const queueName = ctx.self?.name || 'bullmq' + const jobs = ctx.arguments?.[0] + return { + resource: queueName, + meta: { + 'messaging.destination.name': ctx.self?.name, + 'messaging.batch.message_count': Array.isArray(jobs) ? jobs.length : undefined + } + } + } + + injectTraceContext (span, ctx) { + const jobs = ctx.arguments?.[0] + if (!Array.isArray(jobs)) return + for (const job of jobs) { + if (job?.data?.constructor?.name !== 'Object') continue + job.data._datadog = job.data._datadog || {} + this.tracer.inject(span, 'text_map', job.data._datadog) + } + } + + getDsmData (ctx) { + const jobs = ctx.arguments?.[0] || [] + const payloadSize = jobs.reduce((total, job) => { + return total + (job?.data ? getMessageSize(job.data) : 0) + }, 0) + return { + queueName: ctx.self?.name || 'bullmq', + payloadSize, + injectTarget: jobs[0]?.data + } + } + + setProducerCheckpoint (span, ctx) { + const jobs = ctx.arguments?.[0] || [] + const queueName = ctx.self?.name || 'bullmq' + const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq'] + + for (const job of jobs) { + if (job?.data && job.data !== null && job.data.constructor.name === 'Object') { + const payloadSize = getMessageSize(job.data) + const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize) + job.data._datadog = job.data._datadog || {} + DsmPathwayCodec.encode(dataStreamsContext, job.data._datadog) + } + } + } +} + +class FlowProducerAddPlugin extends BaseBullmqProducerPlugin { + static prefix = 'tracing:orchestrion:bullmq:FlowProducer_add' + + getSpanData (ctx) { + const flow = ctx.arguments?.[0] + const queueName = flow?.queueName || 'bullmq' + return { + resource: queueName, + meta: { + 'messaging.destination.name': flow?.queueName + } + } + } + + injectTraceContext (span, ctx) { + const flow = ctx.arguments?.[0] + if (flow?.data?.constructor?.name === 'Object') { + flow.data._datadog = flow.data._datadog || {} + this.tracer.inject(span, 'text_map', flow.data._datadog) + } + } + + getDsmData (ctx) { + const flow = ctx.arguments?.[0] + return { + queueName: flow?.queueName || 'bullmq', + payloadSize: flow?.data ? getMessageSize(flow.data) : 0, + injectTarget: flow?.data + } + } +} + +module.exports = [QueueAddPlugin, QueueAddBulkPlugin, FlowProducerAddPlugin] diff --git a/packages/datadog-plugin-bullmq/test/dsm.spec.js b/packages/datadog-plugin-bullmq/test/dsm.spec.js new file mode 100644 index 00000000000..2b7e155b3ea --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/dsm.spec.js @@ -0,0 +1,102 @@ +'use strict' + +// IMPORTANT: Set DD_DATA_STREAMS_ENABLED BEFORE any requires +process.env.DD_DATA_STREAMS_ENABLED = 'true' + +const assert = require('node:assert') +const sinon = require('sinon') +const { createIntegrationTestSuite } = require('../../dd-trace/test/setup/helpers/plugin-test-helpers') +const DataStreamsContext = require('../../dd-trace/src/datastreams/context') +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') +const TestSetup = require('./test-setup') + +const testSetup = new TestSetup() + +const getDsmPathwayHash = (queueName, isProducer, parentHash) => { + const edgeTags = [isProducer ? 'direction:out' : 'direction:in', `topic:${queueName}`, 'type:bullmq'] + edgeTags.sort() + return computePathwayHash('test', 'tester', edgeTags, parentHash) +} + +createIntegrationTestSuite('bullmq', 'bullmq', { + category: 'messaging' +}, (meta) => { + before(async () => { + await testSetup.setup(meta.mod) + }) + + after(async () => { + await testSetup.teardown() + }) + + describe('Data Streams Monitoring (DSM)', () => { + const queueName = 'test-queue' + let expectedProducerHash + let expectedConsumerHash + + beforeEach(() => { + expectedProducerHash = getDsmPathwayHash(queueName, true, ENTRY_PARENT_HASH) + expectedConsumerHash = getDsmPathwayHash(queueName, false, expectedProducerHash) + // Require tracer directly since the meta.tracer is null at test definition time + const tracerInstance = require('../../dd-trace') + tracerInstance.use('bullmq', { dsmEnabled: true }) + }) + + describe('checkpoints', () => { + let setDataStreamsContextSpy + + beforeEach(() => { + setDataStreamsContextSpy = sinon.spy(DataStreamsContext, 'setDataStreamsContext') + }) + + afterEach(() => { + setDataStreamsContextSpy.restore() + }) + + it('should set a checkpoint on produce (Queue.add)', async () => { + await testSetup.queueAdd() + assert.strictEqual(setDataStreamsContextSpy.called, true) + assert.deepStrictEqual(setDataStreamsContextSpy.args[0][0].hash, expectedProducerHash) + }) + + it('should set a checkpoint on produce (Queue.addBulk)', async () => { + await testSetup.queueAddBulk() + assert.strictEqual(setDataStreamsContextSpy.called, true) + // addBulk should set checkpoint for each message in the batch + assert.deepStrictEqual(setDataStreamsContextSpy.args[0][0].hash, expectedProducerHash) + }) + + it('should set a checkpoint on consume (Worker.processJob)', async function () { + this.timeout(10000) + await testSetup.workerProcessJob() + // After produce and consume, we should see consumer checkpoint + const consumerCall = setDataStreamsContextSpy.getCalls().find(call => { + return call.args[0]?.hash?.equals?.(expectedConsumerHash) + }) + assert.ok(consumerCall, 'Consumer checkpoint call not found') + }) + }) + + describe('payload size', () => { + let recordCheckpointSpy + + beforeEach(() => { + if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { + DataStreamsProcessor.prototype.recordCheckpoint.restore() + } + recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') + }) + + afterEach(() => { + recordCheckpointSpy.restore() + }) + + it('should set a message payload size when producing a message', async () => { + await testSetup.queueAdd() + assert.strictEqual(recordCheckpointSpy.called, true) + assert.ok(Object.hasOwn(recordCheckpointSpy.args[0][0], 'payloadSize')) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-bullmq/test/index.spec.js b/packages/datadog-plugin-bullmq/test/index.spec.js new file mode 100644 index 00000000000..5b17d5d9f2f --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/index.spec.js @@ -0,0 +1,271 @@ +'use strict' + +const assert = require('node:assert') +const { createIntegrationTestSuite } = require('../../dd-trace/test/setup/helpers/plugin-test-helpers') +const { ANY_STRING } = require('../../../integration-tests/helpers') +const TestSetup = require('./test-setup') + +const testSetup = new TestSetup() + +createIntegrationTestSuite('bullmq', 'bullmq', { + category: 'messaging' +}, (meta) => { + const { agent } = meta + + before(async () => { + await testSetup.setup(meta.mod) + }) + + after(async () => { + await testSetup.teardown() + }) + + describe('Queue.add() - bullmq.add', () => { + it('should generate span with correct tags (happy path)', async () => { + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.add', + resource: 'test-queue', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'messaging.destination.name': 'test-queue', + 'messaging.operation': 'publish', + component: 'bullmq' + } + }) + + await testSetup.queueAdd() + + return traceAssertion + }) + + it('should generate span with error tags (error path)', async () => { + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.add', + error: 1, + meta: { + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'error.type': 'Error', + 'error.message': 'Validation error, cannot resolve alias "inv"', + 'error.stack': ANY_STRING + } + }) + + try { + await testSetup.queueAddError() + } catch (err) { + // Expected error + } + + return traceAssertion + }) + }) + + describe('Queue.addBulk() - bullmq.addBulk', () => { + it('should generate span with correct tags (happy path)', async () => { + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.addBulk', + resource: 'test-queue', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'messaging.destination.name': 'test-queue', + 'messaging.operation': 'publish', + component: 'bullmq' + }, + metrics: { + 'messaging.batch.message_count': 3 + } + }) + + await testSetup.queueAddBulk() + + return traceAssertion + }) + + it('should generate span with error tags (error path)', async () => { + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.addBulk', + error: 1, + meta: { + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'error.type': 'ReplyError', + 'error.message': ANY_STRING, // this bullmq error message includes a hash that may change, use ANY_STRING + 'error.stack': ANY_STRING + } + }) + + try { + await testSetup.queueAddBulkError() + } catch (err) { + // Expected error + } + + return traceAssertion + }) + }) + + describe('Worker.processJob() - bullmq.processJob', () => { + it('should generate span with correct tags (happy path)', async function () { + this.timeout(10000) + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.processJob', + meta: { + 'span.kind': 'consumer', + 'messaging.system': 'bullmq', + 'messaging.operation': 'process', + component: 'bullmq' + } + }) + + await testSetup.workerProcessJob() + + return traceAssertion + }) + + it('should generate span with error tags (error path)', async function () { + this.timeout(10000) + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.processJob', + error: 1, + meta: { + 'span.kind': 'consumer', + 'messaging.system': 'bullmq', + 'error.type': 'Error', + 'error.message': 'Intentional job failure for testing', + 'error.stack': ANY_STRING + } + }) + + await testSetup.workerProcessJobError() + + return traceAssertion + }) + }) + + describe('FlowProducer.add() - bullmq.add', () => { + it('should generate span with correct tags (happy path)', async () => { + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.add', + meta: { + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'messaging.operation': 'publish', + component: 'bullmq' + } + }) + + await testSetup.flowProducerAdd() + + return traceAssertion + }) + + it('should generate span with error tags (error path)', async function () { + this.timeout(10000) + const traceAssertion = agent.assertFirstTraceSpan({ + name: 'bullmq.add', + error: 1, + meta: { + 'span.kind': 'producer', + 'messaging.system': 'bullmq', + 'error.type': 'TypeError', + 'error.message': 'Converting circular structure to JSON\n --> ' + + "starting at object with constructor 'Object'\n --- property 'self' closes the circle", + 'error.stack': ANY_STRING + } + }) + + try { + await testSetup.flowProducerAddError() + } catch (err) { + // Expected error + } + + return traceAssertion + }) + }) + + describe('Context Propagation', () => { + it('should link consumer span to producer span via distributed trace (Queue.add)', async function () { + this.timeout(15000) + let producerSpan + let consumerSpan + + const traceAssertion = agent.assertSomeTraces((traces) => { + const allSpans = traces.flat() + + for (const span of allSpans) { + if (span.name === 'bullmq.add') producerSpan = span + if (span.name === 'bullmq.processJob') consumerSpan = span + } + + if (!producerSpan) { + throw new Error('Producer span bullmq.add not found') + } + if (!consumerSpan) { + throw new Error('Consumer span bullmq.processJob not found') + } + + // CRITICAL: Verify distributed trace - same trace ID + assert.strictEqual( + consumerSpan.trace_id.toString(), + producerSpan.trace_id.toString(), + 'Consumer should have same trace_id as producer' + ) + + // CRITICAL: Consumer is child of producer + assert.strictEqual( + consumerSpan.parent_id.toString(), + producerSpan.span_id.toString(), + 'Consumer parent_id should equal producer span_id' + ) + }) + + await testSetup.workerProcessJob() + + return traceAssertion + }) + + it('should link consumer span to producer span via distributed trace (Queue.addBulk)', async function () { + this.timeout(15000) + let producerSpan + let consumerSpan + + const traceAssertion = agent.assertSomeTraces((traces) => { + const allSpans = traces.flat() + + for (const span of allSpans) { + if (span.name === 'bullmq.addBulk') producerSpan = span + if (span.name === 'bullmq.processJob') consumerSpan = span + } + + if (!producerSpan) { + throw new Error('Producer span bullmq.addBulk not found') + } + if (!consumerSpan) { + throw new Error('Consumer span bullmq.processJob not found') + } + + // CRITICAL: Verify distributed trace - same trace ID + assert.strictEqual( + consumerSpan.trace_id.toString(), + producerSpan.trace_id.toString(), + 'Consumer should have same trace_id as producer' + ) + + // CRITICAL: Consumer is child of producer + assert.strictEqual( + consumerSpan.parent_id.toString(), + producerSpan.span_id.toString(), + 'Consumer parent_id should equal producer span_id' + ) + }) + + await testSetup.workerProcessJobBulk() + + return traceAssertion + }) + }) +}) diff --git a/packages/datadog-plugin-bullmq/test/integration-test/client.spec.js b/packages/datadog-plugin-bullmq/test/integration-test/client.spec.js new file mode 100644 index 00000000000..c55e79c9f48 --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/integration-test/client.spec.js @@ -0,0 +1,97 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { + FakeAgent, + sandboxCwd, + useSandbox, + checkSpansForServiceName, + spawnPluginIntegrationTestProcAndExpectExit +} = require('../../../../integration-tests/helpers') +const { withVersions } = require('../../../dd-trace/test/setup/mocha') + +describe('esm', () => { + let agent + let proc + + withVersions('bullmq', 'bullmq', '>=5.66.0', version => { + useSandbox([`'bullmq@${version}'`], false, [ + './packages/datadog-plugin-bullmq/test/integration-test/*']) + + beforeEach(async () => { + agent = await new FakeAgent().start() + }) + + afterEach(async () => { + proc && proc.kill() + await agent.stop() + }) + + describe('Queue.add()', () => { + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) + assert.strictEqual(checkSpansForServiceName(payload, 'bullmq.add'), true) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server-queue-add.mjs', agent.port) + + await res + }).timeout(60000) + }) + + describe('Queue.addBulk()', () => { + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) + assert.strictEqual(checkSpansForServiceName(payload, 'bullmq.addBulk'), true) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server-queue-add-bulk.mjs', agent.port) + + await res + }).timeout(60000) + }) + + describe('FlowProducer.add()', () => { + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) + assert.strictEqual(checkSpansForServiceName(payload, 'bullmq.add'), true) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit( + sandboxCwd(), + 'server-flow-producer-add.mjs', + agent.port + ) + + await res + }).timeout(60000) + }) + + describe('Worker.callProcessJob()', () => { + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) + assert.strictEqual(checkSpansForServiceName(payload, 'bullmq.processJob'), true) + }) + + proc = await spawnPluginIntegrationTestProcAndExpectExit( + sandboxCwd(), + 'server-worker-process-job.mjs', + agent.port, + // Disable Redis/ioredis instrumentation to avoid hitting max active requests limit + { DD_TRACE_REDIS_ENABLED: 'false', DD_TRACE_IOREDIS_ENABLED: 'false' } + ) + + await res + }).timeout(60000) + }) + }) +}) diff --git a/packages/datadog-plugin-bullmq/test/integration-test/server-flow-producer-add.mjs b/packages/datadog-plugin-bullmq/test/integration-test/server-flow-producer-add.mjs new file mode 100644 index 00000000000..fbe0c970f5c --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/integration-test/server-flow-producer-add.mjs @@ -0,0 +1,27 @@ +import 'dd-trace/init.js' +import { FlowProducer } from 'bullmq' + +const connection = { + host: '127.0.0.1', + port: 6379 +} + +const queueName = 'esm-test-flow-producer' + +const flowProducer = new FlowProducer({ connection }) + +// Test FlowProducer.add() - tests FlowProducer_add channel +await flowProducer.add({ + name: 'parent-flow-job', + queueName, + data: { type: 'parent', message: 'Parent job' }, + children: [ + { + name: 'child-job-1', + queueName, + data: { type: 'child', message: 'Child job' } + } + ] +}) + +await flowProducer.close() diff --git a/packages/datadog-plugin-bullmq/test/integration-test/server-queue-add-bulk.mjs b/packages/datadog-plugin-bullmq/test/integration-test/server-queue-add-bulk.mjs new file mode 100644 index 00000000000..ae6874b4796 --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/integration-test/server-queue-add-bulk.mjs @@ -0,0 +1,20 @@ +import 'dd-trace/init.js' +import { Queue } from 'bullmq' + +const connection = { + host: '127.0.0.1', + port: 6379 +} + +const queueName = 'esm-test-queue-add-bulk' + +const queue = new Queue(queueName, { connection }) +await queue.waitUntilReady() + +// Test Queue.addBulk() - tests Queue_addBulk channel +await queue.addBulk([ + { name: 'bulk-job-1', data: { message: 'First bulk job' } }, + { name: 'bulk-job-2', data: { message: 'Second bulk job' } } +]) + +await queue.close() diff --git a/packages/datadog-plugin-bullmq/test/integration-test/server-queue-add.mjs b/packages/datadog-plugin-bullmq/test/integration-test/server-queue-add.mjs new file mode 100644 index 00000000000..87a0dfb4608 --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/integration-test/server-queue-add.mjs @@ -0,0 +1,17 @@ +import 'dd-trace/init.js' +import { Queue } from 'bullmq' + +const connection = { + host: '127.0.0.1', + port: 6379 +} + +const queueName = 'esm-test-queue-add' + +const queue = new Queue(queueName, { connection }) +await queue.waitUntilReady() + +// Test Queue.add() - tests Queue_add channel +await queue.add('test-job', { message: 'Testing Queue.add' }) + +await queue.close() diff --git a/packages/datadog-plugin-bullmq/test/integration-test/server-worker-process-job.mjs b/packages/datadog-plugin-bullmq/test/integration-test/server-worker-process-job.mjs new file mode 100644 index 00000000000..1b6eb735cd1 --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/integration-test/server-worker-process-job.mjs @@ -0,0 +1,31 @@ +import 'dd-trace/init.js' +import { Queue, Worker, QueueEvents } from 'bullmq' + +const connection = { + host: '127.0.0.1', + port: 6379 +} + +const queueName = 'esm-test-worker-process' + +// Create worker first and wait for it to be ready before creating the queue +// This ensures the worker is listening before any jobs are added +const worker = new Worker(queueName, async (job) => { + return { processed: true, jobId: job.id } +}, { connection }) + +await worker.waitUntilReady() + +const queue = new Queue(queueName, { connection }) +await queue.waitUntilReady() + +const queueEvents = new QueueEvents(queueName, { connection }) +await queueEvents.waitUntilReady() + +// Add job and wait for processing to complete +const job = await queue.add('process-test-job', { message: 'Test job for worker processing' }) +await job.waitUntilFinished(queueEvents) + +await worker.close() +await queue.close() +await queueEvents.close() diff --git a/packages/datadog-plugin-bullmq/test/test-setup.js b/packages/datadog-plugin-bullmq/test/test-setup.js new file mode 100644 index 00000000000..651bf731221 --- /dev/null +++ b/packages/datadog-plugin-bullmq/test/test-setup.js @@ -0,0 +1,125 @@ +'use strict' + +class BullmqTestSetup { + async setup (module) { + const connection = { + host: '127.0.0.1', + port: 6379 + } + + this.queue = new module.Queue('test-queue', { connection }) + this.flowProducer = new module.FlowProducer({ connection }) + this.queueEvents = new module.QueueEvents('test-queue', { connection }) + this.worker = new module.Worker('test-queue', async (job) => { + if (job.data.shouldFail) { + throw new Error('Intentional job failure for testing') + } + return { processed: true, jobId: job.id } + }, { connection }) + + await this.worker.waitUntilReady() + await this.queue.waitUntilReady() + await this.queueEvents.waitUntilReady() + } + + async teardown () { + if (this.worker) { + await this.worker.close() + } + if (this.queue) { + await this.queue.close() + } + if (this.flowProducer) { + await this.flowProducer.close() + } + if (this.queueEvents) { + await this.queueEvents.close() + } + } + + async queueAdd () { + const job = await this.queue.add('test-job', { + message: 'Hello from BullMQ', + timestamp: Date.now() + }) + return job + } + + async queueAddError () { + await this.queue.add('error-job', { data: 'test' }, { + repeat: { pattern: 'invalid-cron-pattern' } + }) + } + + async queueAddBulk () { + const jobs = await this.queue.addBulk([ + { name: 'bulk-job-1', data: { message: 'First bulk job' } }, + { name: 'bulk-job-2', data: { message: 'Second bulk job' } }, + { name: 'bulk-job-3', data: { message: 'Third bulk job' } } + ]) + return jobs + } + + async queueAddBulkError () { + await this.queue.addBulk([ + { name: 'valid-job', data: { ok: true } }, + { name: null, data: null, opts: { invalid: true } } + ]) + } + + async flowProducerAdd () { + const flow = await this.flowProducer.add({ + name: 'parent-flow-job', + queueName: 'test-queue', + data: { type: 'parent', message: 'I am the parent' }, + children: [ + { + name: 'child-job-1', + queueName: 'test-queue', + data: { type: 'child', message: 'I am child 1' } + }, + { + name: 'child-job-2', + queueName: 'test-queue', + data: { type: 'child', message: 'I am child 2' } + } + ] + }) + return flow + } + + async flowProducerAddError () { + // Pass circular reference to trigger JSON serialization error + const circularData = { test: true } + circularData.self = circularData + await this.flowProducer.add({ + name: 'invalid-flow', + queueName: 'test-queue', + data: circularData + }) + } + + async workerProcessJob () { + const job = await this.queue.add('process-test-job', { + message: 'Test job for worker processing' + }) + await job.waitUntilFinished(this.queueEvents) + } + + async workerProcessJobError () { + const job = await this.queue.add('error-test-job', { + shouldFail: true + }) + await job.waitUntilFinished(this.queueEvents).catch(() => {}) + } + + async workerProcessJobBulk () { + const jobs = await this.queue.addBulk([ + { name: 'bulk-process-job-1', data: { message: 'First bulk job for processing' } }, + { name: 'bulk-process-job-2', data: { message: 'Second bulk job for processing' } } + ]) + await jobs[0].waitUntilFinished(this.queueEvents) + } +} + +module.exports = BullmqTestSetup diff --git a/packages/datadog-plugin-bunyan/test/integration-test/client.spec.js b/packages/datadog-plugin-bunyan/test/integration-test/client.spec.js index b6e0a54dadd..4ed0d37a744 100644 --- a/packages/datadog-plugin-bunyan/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-bunyan/test/integration-test/client.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict') const { FakeAgent, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -33,10 +33,12 @@ describe('esm', () => { }) for (const variant of varySandbox.VARIANTS) { it(`is instrumented loaded with ${variant}`, async () => { - proc = await spawnPluginIntegrationTestProc( + proc = await spawnPluginIntegrationTestProcAndExpectExit( sandboxCwd(), variants[variant], agent.port, + undefined, + undefined, (data) => { const jsonObject = JSON.parse(data.toString()) assert.ok(Object.hasOwn(jsonObject, 'dd')) diff --git a/packages/datadog-plugin-cassandra-driver/test/integration-test/client.spec.js b/packages/datadog-plugin-cassandra-driver/test/integration-test/client.spec.js index c90a4715707..a04ea03a6cb 100644 --- a/packages/datadog-plugin-cassandra-driver/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-cassandra-driver/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -42,7 +42,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'cassandra.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-child_process/test/index.spec.js b/packages/datadog-plugin-child_process/test/index.spec.js index 13fca709a84..ec8e0b3c292 100644 --- a/packages/datadog-plugin-child_process/test/index.spec.js +++ b/packages/datadog-plugin-child_process/test/index.spec.js @@ -404,7 +404,7 @@ describe('Child process plugin', () => { const execFileAsync = util.promisify(childProcess.execFile) assert.strictEqual(global.Promise, Bluebird) - assert.ok(global.Promise.version != null) + assert.ok(global.Promise.version) const expectedPromise = expectSomeSpan(agent, { type: 'system', @@ -417,7 +417,7 @@ describe('Child process plugin', () => { }) const result = await execFileAsync('echo', ['bluebird-test']) - assert.ok(result != null) + assert.ok(result) assert.strictEqual(result.stdout, 'bluebird-test\n') return expectedPromise @@ -460,8 +460,8 @@ describe('Child process plugin', () => { await execFileAsync('node', ['-invalidFlag'], { stdio: 'pipe' }) throw new Error('Expected command to fail') } catch (error) { - assert.ok(error != null) - assert.ok(error.code != null) + assert.ok(error) + assert.ok(error.code) } return expectedPromise @@ -476,7 +476,7 @@ describe('Child process plugin', () => { const promise = execFileAsync('echo', ['util-promisify-test']) assert.strictEqual(promise.constructor, Bluebird) - assert.ok(promise.constructor.version != null) + assert.ok(promise.constructor.version) const result = await promise assert.strictEqual(result.stdout, 'util-promisify-test\n') diff --git a/packages/datadog-plugin-confluentinc-kafka-javascript/src/index.js b/packages/datadog-plugin-confluentinc-kafka-javascript/src/index.js index 4b28530e72a..d9edc852496 100644 --- a/packages/datadog-plugin-confluentinc-kafka-javascript/src/index.js +++ b/packages/datadog-plugin-confluentinc-kafka-javascript/src/index.js @@ -1,9 +1,9 @@ 'use strict' +const KafkajsPlugin = require('../../datadog-plugin-kafkajs/src/index') const ProducerPlugin = require('./producer') const ConsumerPlugin = require('./consumer') const BatchConsumerPlugin = require('./batch-consumer') -const KafkajsPlugin = require('../../datadog-plugin-kafkajs/src/index') class ConfluentKafkaJsPlugin extends KafkajsPlugin { /** diff --git a/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js b/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js new file mode 100644 index 00000000000..7dfff23c8c1 --- /dev/null +++ b/packages/datadog-plugin-confluentinc-kafka-javascript/test/dsm.spec.js @@ -0,0 +1,323 @@ +'use strict' + +const assert = require('node:assert/strict') +const { randomUUID } = require('node:crypto') +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') + +const agent = require('../../dd-trace/test/plugins/agent') +const { withVersions } = require('../../dd-trace/test/setup/mocha') + +const DataStreamsContext = require('../../dd-trace/src/datastreams/context') +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') + +const getDsmPathwayHash = (testTopic, isProducer, parentHash) => { + let edgeTags + if (isProducer) { + edgeTags = ['direction:out', 'topic:' + testTopic, 'type:kafka'] + } else { + edgeTags = ['direction:in', 'group:test-group-confluent', 'topic:' + testTopic, 'type:kafka'] + } + + edgeTags.sort() + return computePathwayHash('test', 'tester', edgeTags, parentHash) +} + +describe('Plugin', () => { + const module = '@confluentinc/kafka-javascript' + const groupId = 'test-group-confluent' + + describe('confluentinc-kafka-javascript', function () { + this.timeout(30000) + + afterEach(() => { + return agent.close({ ritmReset: false }) + }) + + withVersions('confluentinc-kafka-javascript', module, (version) => { + let kafka + let admin + let tracer + let Kafka + let ConfluentKafka + let messages + let nativeApi + let testTopic + + describe('data stream monitoring', () => { + let consumer + let expectedProducerHash + let expectedConsumerHash + + beforeEach(async () => { + messages = [{ key: 'key1', value: 'test2' }] + + process.env.DD_DATA_STREAMS_ENABLED = 'true' + tracer = require('../../dd-trace') + await agent.load('confluentinc-kafka-javascript') + const lib = require(`../../../versions/${module}@${version}`).get() + + // Store the module for later use + nativeApi = lib + + // Setup for the KafkaJS wrapper tests + ConfluentKafka = lib.KafkaJS + Kafka = ConfluentKafka.Kafka + kafka = new Kafka({ + kafkaJS: { + clientId: `kafkajs-test-${version}`, + brokers: ['127.0.0.1:9092'], + logLevel: ConfluentKafka.logLevel.WARN + } + }) + testTopic = `test-topic-${randomUUID()}` + admin = kafka.admin() + await admin.connect() + await admin.createTopics({ + topics: [{ + topic: testTopic, + numPartitions: 1, + replicationFactor: 1 + }] + }) + await admin.disconnect() + + consumer = kafka.consumer({ + kafkaJS: { groupId, fromBeginning: true } + }) + await consumer.connect() + await consumer.subscribe({ topic: testTopic }) + }) + + beforeEach(() => { + expectedProducerHash = getDsmPathwayHash(testTopic, true, ENTRY_PARENT_HASH) + expectedConsumerHash = getDsmPathwayHash(testTopic, false, expectedProducerHash) + }) + + afterEach(async () => { + await consumer.disconnect() + }) + + describe('checkpoints', () => { + let setDataStreamsContextSpy + + beforeEach(() => { + setDataStreamsContextSpy = sinon.spy(DataStreamsContext, 'setDataStreamsContext') + }) + + afterEach(async () => { + setDataStreamsContextSpy.restore() + }) + + it('Should set a checkpoint on produce', async () => { + const messages = [{ key: 'consumerDSM1', value: 'test2' }] + await sendMessages(kafka, testTopic, messages) + assert.strictEqual(setDataStreamsContextSpy.args[0][0].hash, expectedProducerHash) + }) + + it('Should set a checkpoint on consume (eachMessage)', async () => { + const runArgs = [] + let consumerReceiveMessagePromise + await consumer.run({ + eachMessage: async () => { + runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) + consumerReceiveMessagePromise = Promise.resolve() + } + }) + await sendMessages(kafka, testTopic, messages).then( + async () => await consumerReceiveMessagePromise + ) + + for (const runArg of runArgs) { + assert.strictEqual(runArg.hash, expectedConsumerHash) + } + }) + + it('Should set a checkpoint on consume (eachBatch)', async () => { + const runArgs = [] + let consumerReceiveMessagePromise + await consumer.run({ + eachBatch: async () => { + runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) + consumerReceiveMessagePromise = Promise.resolve() + } + }) + await sendMessages(kafka, testTopic, messages).then( + async () => await consumerReceiveMessagePromise + ) + for (const runArg of runArgs) { + assert.strictEqual(runArg.hash, expectedConsumerHash) + } + }) + + it('Should set a message payload size when producing a message', async () => { + const messages = [{ key: 'key1', value: 'test2' }] + if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { + DataStreamsProcessor.prototype.recordCheckpoint.restore() + } + const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') + await sendMessages(kafka, testTopic, messages) + assert.ok(recordCheckpointSpy.args[0][0].hasOwnProperty('payloadSize')) + recordCheckpointSpy.restore() + }) + + it('Should set a message payload size when consuming a message', async () => { + const messages = [{ key: 'key1', value: 'test2' }] + if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { + DataStreamsProcessor.prototype.recordCheckpoint.restore() + } + const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') + let consumerReceiveMessagePromise + await consumer.run({ + eachMessage: async () => { + assert.ok(recordCheckpointSpy.args[0][0].hasOwnProperty('payloadSize')) + recordCheckpointSpy.restore() + consumerReceiveMessagePromise = Promise.resolve() + } + }) + await sendMessages(kafka, testTopic, messages).then( + async () => await consumerReceiveMessagePromise + ) + }) + }) + + describe('backlogs', () => { + let setOffsetSpy + + beforeEach(() => { + setOffsetSpy = sinon.spy(tracer._tracer._dataStreamsProcessor, 'setOffset') + }) + + afterEach(() => { + setOffsetSpy.restore() + }) + + it('Should add backlog on consumer explicit commit', async () => { + // Send a message, consume it, and record the last consumed offset + let commitMeta + + let messageProcessedResolve + const messageProcessedPromise = new Promise(resolve => { + messageProcessedResolve = resolve + }) + + const consumerRunPromise = consumer.run({ + eachMessage: async payload => { + const { topic, partition, message } = payload + commitMeta = { + topic, + partition, + offset: Number(message.offset) + } + messageProcessedResolve() + } + }) + + await consumerRunPromise + + // wait for the message to be processed before continuing + await sendMessages(kafka, testTopic, messages) + await messageProcessedPromise + await consumer.disconnect() + + for (const call of setOffsetSpy.getCalls()) { + assert.notStrictEqual(call.args[0]?.type, 'kafka_commit') + } + + const newConsumer = kafka.consumer({ + kafkaJS: { groupId, fromBeginning: true, autoCommit: false } + }) + await newConsumer.connect() + await sendMessages(kafka, testTopic, [{ key: 'key1', value: 'test2' }]) + await newConsumer.run({ + eachMessage: async () => { + await newConsumer.disconnect() + } + }) + setOffsetSpy.resetHistory() + await newConsumer.commitOffsets() + + // Check our work + const runArg = setOffsetSpy.lastCall.args[0] + assert.strictEqual(runArg?.offset, commitMeta.offset) + assert.strictEqual(runArg?.partition, commitMeta.partition) + assert.strictEqual(runArg?.topic, commitMeta.topic) + assert.strictEqual(runArg?.type, 'kafka_commit') + assert.strictEqual(runArg?.consumer_group, groupId) + }) + + it('Should add backlog on producer response', async () => { + await sendMessages(kafka, testTopic, messages) + sinon.assert.calledOnce(setOffsetSpy) + const { topic } = setOffsetSpy.lastCall.args[0] + assert.strictEqual(topic, testTopic) + }) + }) + + describe('when using a kafka broker version that does not support message headers', () => { + class KafkaJSError extends Error { + constructor (message) { + super(message) + this.name = 'KafkaJSError' + this.type = 'ERR_UNKNOWN' + } + } + let error + let producer + let produceStub + + beforeEach(async () => { + // simulate a kafka error for the broker version not supporting message headers + error = new KafkaJSError() + error.message = 'Simulated KafkaJSError ERR_UNKNOWN from Producer.produce stub' + producer = kafka.producer() + await producer.connect() + + // Spy on the produce method from the native library before it gets wrapped + produceStub = sinon.stub(nativeApi.Producer.prototype, 'produce') + .callsFake((topic, partition, message, key) => { + throw error + }) + }) + + afterEach(async () => { + produceStub.restore() + await producer.disconnect() + }) + + 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) + } + // Verify headers were injected in the first attempt + assert.ok(Object.hasOwn(testMessages[0].headers[0], 'x-datadog-trace-id')) + + // restore the stub to allow the next send to succeed + produceStub.restore() + + const result = await producer.send({ topic: testTopic, messages: testMessages2 }) + assert.strictEqual(testMessages2[0].headers, null) + assert.notStrictEqual(result, undefined) + }) + }) + }) + }) + }) +}) + +async function sendMessages (kafka, topic, messages) { + const producer = kafka.producer() + await producer.connect() + await producer.send({ + topic, + messages + }) + await producer.disconnect() +} diff --git a/packages/datadog-plugin-confluentinc-kafka-javascript/test/index.spec.js b/packages/datadog-plugin-confluentinc-kafka-javascript/test/index.spec.js index 27cafbcdb2f..2c552beb20c 100644 --- a/packages/datadog-plugin-confluentinc-kafka-javascript/test/index.spec.js +++ b/packages/datadog-plugin-confluentinc-kafka-javascript/test/index.spec.js @@ -1,32 +1,16 @@ 'use strict' -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('mocha') -const sinon = require('sinon') +const assert = require('node:assert/strict') const { randomUUID } = require('node:crypto') +const { describe, it, beforeEach, afterEach } = require('mocha') 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 { expectedSchema } = require('./naming') const { withVersions } = require('../../dd-trace/test/setup/mocha') - -const DataStreamsContext = require('../../dd-trace/src/datastreams/context') -const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') -const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') - -const getDsmPathwayHash = (testTopic, isProducer, parentHash) => { - let edgeTags - if (isProducer) { - edgeTags = ['direction:out', 'topic:' + testTopic, 'type:kafka'] - } else { - edgeTags = ['direction:in', 'group:test-group-confluent', 'topic:' + testTopic, 'type:kafka'] - } - - edgeTags.sort() - return computePathwayHash('test', 'tester', edgeTags, parentHash) -} +const { assertObjectContains } = require('../../../integration-tests/helpers') +const { expectedSchema } = require('./naming') describe('Plugin', () => { const module = '@confluentinc/kafka-javascript' @@ -115,14 +99,14 @@ describe('Plugin', () => { const expectedSpanPromise = agent.assertSomeTraces(traces => { const span = traces[0][0] - expect(span).to.include({ + assertObjectContains(span, { name: expectedSchema.send.opName, service: expectedSchema.send.serviceName, resource: testTopic, error: 1 }) - expect(span.meta).to.include({ + assertObjectContains(span.meta, { [ERROR_TYPE]: error.name, [ERROR_MESSAGE]: error.message, [ERROR_STACK]: error.stack, @@ -189,8 +173,8 @@ describe('Plugin', () => { const currentSpan = tracer.scope().active() try { - expect(currentSpan).to.not.equal(firstSpan) - expect(currentSpan.context()._name).to.equal(expectedSchema.receive.opName) + assert.notStrictEqual(currentSpan, firstSpan) + assert.strictEqual(currentSpan.context()._name, expectedSchema.receive.opName) done() } catch (e) { done(e) @@ -209,13 +193,13 @@ describe('Plugin', () => { const expectedSpanPromise = agent.assertSomeTraces(traces => { const span = traces[0][0] - expect(span).to.include({ + assertObjectContains(span, { name: 'kafka.consume', service: 'test-kafka', resource: testTopic }) - expect(parseInt(span.parent_id.toString())).to.be.gt(0) + assert.ok(parseInt(span.parent_id.toString()) > 0) }, { timeoutMs: 10000 }) let consumerReceiveMessagePromise @@ -335,18 +319,18 @@ describe('Plugin', () => { const expectedSpanPromise = agent.assertSomeTraces(traces => { const span = traces[0][0] - expect(span).to.include({ + assertObjectContains(span, { name: expectedSchema.send.opName, service: expectedSchema.send.serviceName, error: 1 }) - expect(span.meta).to.include({ + assertObjectContains(span.meta, { component: 'confluentinc-kafka-javascript' }) - expect(span.meta[ERROR_TYPE]).to.exist - expect(span.meta[ERROR_MESSAGE]).to.exist + assert.ok(span.meta[ERROR_TYPE]) + assert.ok(span.meta[ERROR_MESSAGE]) }, { timeoutMs: 10000 }) try { @@ -456,13 +440,13 @@ describe('Plugin', () => { const expectedSpanPromise = agent.assertSomeTraces(traces => { const span = traces[0][0] - expect(span).to.include({ + assertObjectContains(span, { name: 'kafka.consume', service: 'test-kafka', resource: testTopic }) - expect(parseInt(span.parent_id.toString())).to.be.gt(0) + assert.ok(parseInt(span.parent_id.toString()) > 0) }, { timeoutMs: 10000 }) nativeConsumer.setDefaultConsumeTimeout(10) nativeConsumer.subscribe([testTopic]) @@ -503,241 +487,6 @@ describe('Plugin', () => { // }) }) }) - - describe('data stream monitoring', () => { - let consumer - let expectedProducerHash - let expectedConsumerHash - - beforeEach(async () => { - tracer.init() - tracer.use('confluentinc-kafka-javascript', { dsmEnabled: true }) - messages = [{ key: 'key1', value: 'test2' }] - consumer = kafka.consumer({ - kafkaJS: { groupId, fromBeginning: true } - }) - await consumer.connect() - await consumer.subscribe({ topic: testTopic }) - }) - - beforeEach(() => { - expectedProducerHash = getDsmPathwayHash(testTopic, true, ENTRY_PARENT_HASH) - expectedConsumerHash = getDsmPathwayHash(testTopic, false, expectedProducerHash) - }) - - afterEach(async () => { - await consumer.disconnect() - }) - - describe('checkpoints', () => { - let setDataStreamsContextSpy - - beforeEach(() => { - setDataStreamsContextSpy = sinon.spy(DataStreamsContext, 'setDataStreamsContext') - }) - - afterEach(async () => { - setDataStreamsContextSpy.restore() - }) - - it('Should set a checkpoint on produce', async () => { - const messages = [{ key: 'consumerDSM1', value: 'test2' }] - await sendMessages(kafka, testTopic, messages) - expect(setDataStreamsContextSpy.args[0][0].hash).to.equal(expectedProducerHash) - }) - - it('Should set a checkpoint on consume (eachMessage)', async () => { - const runArgs = [] - let consumerReceiveMessagePromise - await consumer.run({ - eachMessage: async () => { - runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) - consumerReceiveMessagePromise = Promise.resolve() - } - }) - await sendMessages(kafka, testTopic, messages).then( - async () => await consumerReceiveMessagePromise - ) - - for (const runArg of runArgs) { - expect(runArg.hash).to.equal(expectedConsumerHash) - } - }) - - it('Should set a checkpoint on consume (eachBatch)', async () => { - const runArgs = [] - let consumerReceiveMessagePromise - await consumer.run({ - eachBatch: async () => { - runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) - consumerReceiveMessagePromise = Promise.resolve() - } - }) - await sendMessages(kafka, testTopic, messages).then( - async () => await consumerReceiveMessagePromise - ) - for (const runArg of runArgs) { - expect(runArg.hash).to.equal(expectedConsumerHash) - } - }) - - it('Should set a message payload size when producing a message', async () => { - const messages = [{ key: 'key1', value: 'test2' }] - if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { - DataStreamsProcessor.prototype.recordCheckpoint.restore() - } - const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') - await sendMessages(kafka, testTopic, messages) - expect(recordCheckpointSpy.args[0][0].hasOwnProperty('payloadSize')) - recordCheckpointSpy.restore() - }) - - it('Should set a message payload size when consuming a message', async () => { - const messages = [{ key: 'key1', value: 'test2' }] - if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { - DataStreamsProcessor.prototype.recordCheckpoint.restore() - } - const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') - let consumerReceiveMessagePromise - await consumer.run({ - eachMessage: async () => { - expect(recordCheckpointSpy.args[0][0].hasOwnProperty('payloadSize')) - recordCheckpointSpy.restore() - consumerReceiveMessagePromise = Promise.resolve() - } - }) - await sendMessages(kafka, testTopic, messages).then( - async () => await consumerReceiveMessagePromise - ) - }) - }) - - describe('backlogs', () => { - let setOffsetSpy - - beforeEach(() => { - setOffsetSpy = sinon.spy(tracer._tracer._dataStreamsProcessor, 'setOffset') - }) - - afterEach(() => { - setOffsetSpy.restore() - }) - - it('Should add backlog on consumer explicit commit', async () => { - // Send a message, consume it, and record the last consumed offset - let commitMeta - - let messageProcessedResolve - const messageProcessedPromise = new Promise(resolve => { - messageProcessedResolve = resolve - }) - - const consumerRunPromise = consumer.run({ - eachMessage: async payload => { - const { topic, partition, message } = payload - commitMeta = { - topic, - partition, - offset: Number(message.offset) - } - messageProcessedResolve() - } - }) - - await consumerRunPromise - - // wait for the message to be processed before continuing - await sendMessages(kafka, testTopic, messages) - await messageProcessedPromise - await consumer.disconnect() - - for (const call of setOffsetSpy.getCalls()) { - expect(call.args[0]).to.not.have.property('type', 'kafka_commit') - } - - const newConsumer = kafka.consumer({ - kafkaJS: { groupId, fromBeginning: true, autoCommit: false } - }) - await newConsumer.connect() - await sendMessages(kafka, testTopic, [{ key: 'key1', value: 'test2' }]) - await newConsumer.run({ - eachMessage: async () => { - await newConsumer.disconnect() - } - }) - setOffsetSpy.resetHistory() - await newConsumer.commitOffsets() - - // Check our work - const runArg = setOffsetSpy.lastCall.args[0] - expect(runArg).to.have.property('offset', commitMeta.offset) - expect(runArg).to.have.property('partition', commitMeta.partition) - expect(runArg).to.have.property('topic', commitMeta.topic) - expect(runArg).to.have.property('type', 'kafka_commit') - expect(runArg).to.have.property('consumer_group', groupId) - }) - - it('Should add backlog on producer response', async () => { - await sendMessages(kafka, testTopic, messages) - expect(setOffsetSpy).to.be.calledOnce - const { topic } = setOffsetSpy.lastCall.args[0] - expect(topic).to.equal(testTopic) - }) - }) - - describe('when using a kafka broker version that does not support message headers', () => { - class KafkaJSError extends Error { - constructor (message) { - super(message) - this.name = 'KafkaJSError' - this.type = 'ERR_UNKNOWN' - } - } - let error - let producer - let produceStub - - beforeEach(async () => { - // simulate a kafka error for the broker version not supporting message headers - error = new KafkaJSError() - error.message = 'Simulated KafkaJSError ERR_UNKNOWN from Producer.produce stub' - producer = kafka.producer() - await producer.connect() - - // Spy on the produce method from the native library before it gets wrapped - produceStub = sinon.stub(nativeApi.Producer.prototype, 'produce') - .callsFake((topic, partition, message, key) => { - throw error - }) - }) - - afterEach(async () => { - produceStub.restore() - await producer.disconnect() - }) - - 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 }) - expect.fail('First producer.send() should have thrown an error') - } catch (e) { - expect(e).to.equal(error) - } - // Verify headers were injected in the first attempt - expect(testMessages[0].headers[0]).to.have.property('x-datadog-trace-id') - - // restore the stub to allow the next send to succeed - produceStub.restore() - - const result = await producer.send({ topic: testTopic, messages: testMessages2 }) - expect(testMessages2[0].headers).to.be.null - expect(result).to.not.be.undefined - }) - }) - }) }) }) }) diff --git a/packages/datadog-plugin-confluentinc-kafka-javascript/test/integration-test/client.spec.js b/packages/datadog-plugin-confluentinc-kafka-javascript/test/integration-test/client.spec.js index b42f02978cd..1dbaa11ed0c 100644 --- a/packages/datadog-plugin-confluentinc-kafka-javascript/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-confluentinc-kafka-javascript/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'kafka.produce'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(5000) diff --git a/packages/datadog-plugin-connect/test/index.spec.js b/packages/datadog-plugin-connect/test/index.spec.js index b9949d7bbb1..f53d551ab83 100644 --- a/packages/datadog-plugin-connect/test/index.spec.js +++ b/packages/datadog-plugin-connect/test/index.spec.js @@ -8,6 +8,7 @@ const axios = require('axios') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') +const { assertObjectContains } = require('../../../integration-tests/helpers') 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') @@ -56,15 +57,19 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'connect') - assert.strictEqual(spans[0].meta['_dd.integration'], 'connect') + assertObjectContains(spans[0], { + service: 'test', + type: 'web', + resource: 'GET /user', + meta: { + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'connect', + '_dd.integration': 'connect' + } + }) }) .then(done) .catch(done) @@ -382,10 +387,14 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + resource: 'GET /user', + meta: { + 'http.status_code': '500', + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -417,10 +426,14 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 0) - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['http.status_code'], '400') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 0, + resource: 'GET /user', + meta: { + 'http.status_code': '400', + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -446,12 +459,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + 'http.status_code': '500', + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -510,16 +527,24 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'connect') - assert.strictEqual(spans[1].error, 1) - assert.strictEqual(spans[1].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[1].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[1].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[1].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'connect' + } + }) + assertObjectContains(spans[1], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -645,14 +670,18 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].service, 'custom') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + service: 'custom', + type: 'web', + resource: 'GET /user', + meta: { + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -680,16 +709,24 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'connect') - assert.strictEqual(spans[1].error, 1) - assert.strictEqual(spans[1].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[1].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[1].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[1].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'connect' + } + }) + assertObjectContains(spans[1], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -715,12 +752,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + 'http.status_code': '500', + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -823,12 +864,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + 'http.status_code': '500', + component: 'connect' + } + }) }) .then(done) .catch(done) @@ -854,12 +899,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'connect') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + 'http.status_code': '500', + component: 'connect' + } + }) }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-cookie-parser/test/integration-test/client.spec.js b/packages/datadog-plugin-cookie-parser/test/integration-test/client.spec.js index e354bdeb7a3..0b8340ee0da 100644 --- a/packages/datadog-plugin-cookie-parser/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-cookie-parser/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('cookie-parser', 'cookie-parser', version => { diff --git a/packages/datadog-plugin-cookie/test/integration-test/client.spec.js b/packages/datadog-plugin-cookie/test/integration-test/client.spec.js index 56ae4d220aa..836669a513c 100644 --- a/packages/datadog-plugin-cookie/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-cookie/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('cookie', 'cookie', version => { diff --git a/packages/datadog-plugin-couchbase/test/integration-test/client.spec.js b/packages/datadog-plugin-couchbase/test/integration-test/client.spec.js index c1c1117b780..46426c5109a 100644 --- a/packages/datadog-plugin-couchbase/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-couchbase/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -35,7 +35,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'couchbase.upsert'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) }) diff --git a/packages/datadog-plugin-crypto/test/integration-test/client.spec.js b/packages/datadog-plugin-crypto/test/integration-test/client.spec.js index 935edc2736e..8164b403e68 100644 --- a/packages/datadog-plugin-crypto/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-crypto/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { useSandbox, sandboxCwd, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') describe('ESM', () => { let variants, proc, agent diff --git a/packages/datadog-plugin-crypto/test/integration-test/server.mjs b/packages/datadog-plugin-crypto/test/integration-test/server.mjs index 054fab696ca..113c789efde 100644 --- a/packages/datadog-plugin-crypto/test/integration-test/server.mjs +++ b/packages/datadog-plugin-crypto/test/integration-test/server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import express from 'express' import { createHash } from 'node:crypto' +import express from 'express' import dc from 'dc-polyfill' const cryptoHashCh = dc.channel('datadog:crypto:hashing:start') diff --git a/packages/datadog-plugin-cypress/src/plugin.js b/packages/datadog-plugin-cypress/src/plugin.js index a3c134e6503..6ff320f9735 100644 --- a/packages/datadog-plugin-cypress/src/plugin.js +++ b/packages/datadog-plugin-cypress/src/plugin.js @@ -1,9 +1,9 @@ 'use strict' const NoopTracer = require('../../dd-trace/src/noop/tracer') -const cypressPlugin = require('./cypress-plugin') const satisfies = require('../../../vendor/dist/semifies') const { DD_MAJOR } = require('../../../version') +const cypressPlugin = require('./cypress-plugin') const noopTask = { 'dd:testSuiteStart': () => { diff --git a/packages/datadog-plugin-dns/test/integration-test/client.spec.js b/packages/datadog-plugin-dns/test/integration-test/client.spec.js index d71c56c47aa..6b3cb9355f4 100644 --- a/packages/datadog-plugin-dns/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-dns/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -41,7 +41,7 @@ describe('esm', () => { assert.strictEqual(payload[0][0].resource, 'fakedomain.faketld') }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-elasticsearch/test/index.spec.js b/packages/datadog-plugin-elasticsearch/test/index.spec.js index 2a1c271a8d6..528904ff25f 100644 --- a/packages/datadog-plugin-elasticsearch/test/index.spec.js +++ b/packages/datadog-plugin-elasticsearch/test/index.spec.js @@ -8,6 +8,7 @@ const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/c const agent = require('../../dd-trace/test/plugins/agent') const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { expectedSchema, rawExpectedSchema } = require('./naming') describe('Plugin', () => { let elasticsearch @@ -80,19 +81,25 @@ describe('Plugin', () => { it('should set the correct tags', done => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) - assert.strictEqual(traces[0][0].meta.component, 'elasticsearch') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'elasticsearch') - assert.strictEqual(traces[0][0].meta['db.type'], 'elasticsearch') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['elasticsearch.method'], 'POST') - assert.strictEqual(traces[0][0].meta['elasticsearch.url'], '/docs/_search') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') + assertObjectContains(traces[0][0], { + name: expectedSchema.outbound.opName, + service: expectedSchema.outbound.serviceName, + meta: { + component: 'elasticsearch', + '_dd.integration': 'elasticsearch', + 'db.type': 'elasticsearch', + 'span.kind': 'client', + 'elasticsearch.method': 'POST', + 'elasticsearch.url': '/docs/_search', + 'out.host': 'localhost' + } + }) if (hasCallbackSupport) { - assert.strictEqual(traces[0][0].meta['elasticsearch.body'], '{"query":{"match_all":{}}}') - assert.strictEqual(traces[0][0].meta['elasticsearch.params'], '{"sort":"name","size":100}') + assertObjectContains(traces[0][0].meta, { + 'elasticsearch.body': '{"query":{"match_all":{}}}', + 'elasticsearch.params': '{"sort":"name","size":100}' + }) } else { assert.ok('elasticsearch.body' in traces[0][0].meta) assert.strictEqual( @@ -119,13 +126,17 @@ describe('Plugin', () => { it('should set the correct tags on msearch', done => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) - assert.strictEqual(traces[0][0].meta.component, 'elasticsearch') - assert.strictEqual(traces[0][0].meta['db.type'], 'elasticsearch') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['elasticsearch.method'], 'POST') - assert.strictEqual(traces[0][0].meta['elasticsearch.url'], '/_msearch') + assertObjectContains(traces[0][0], { + name: expectedSchema.outbound.opName, + service: expectedSchema.outbound.serviceName, + meta: { + component: 'elasticsearch', + 'db.type': 'elasticsearch', + 'span.kind': 'client', + 'elasticsearch.method': 'POST', + 'elasticsearch.url': '/_msearch' + } + }) assert.ok('elasticsearch.body' in traces[0][0].meta) assert.strictEqual( traces[0][0].meta['elasticsearch.body'], @@ -212,10 +223,12 @@ describe('Plugin', () => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'elasticsearch') + assertObjectContains(traces[0][0].meta, { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'elasticsearch' + }) }) .then(done) .catch(done) @@ -269,11 +282,16 @@ describe('Plugin', () => { it('should handle errors', done => { let error - agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'elasticsearch') + agent.assertFirstTraceSpan(span => { + assert.ok(error) + assertObjectContains(span, { + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'elasticsearch' + } + }) }) .then(done) .catch(done) @@ -360,14 +378,15 @@ describe('Plugin', () => { } }, hasCallbackSupport ? () => {} : undefined) - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, 'custom') - assert.strictEqual(traces[0][0].meta.component, 'elasticsearch') - assert.strictEqual(traces[0][0].meta['elasticsearch.params'], 'foo') - assert.strictEqual(traces[0][0].meta['elasticsearch.method'], 'POST') - }) + agent.assertFirstTraceSpan({ + name: expectedSchema.outbound.opName, + service: 'custom', + meta: { + component: 'elasticsearch', + 'elasticsearch.params': 'foo', + 'elasticsearch.method': 'POST' + } + }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-elasticsearch/test/integration-test/client.spec.js b/packages/datadog-plugin-elasticsearch/test/integration-test/client.spec.js index 9722e89a3c8..07c7760fdcc 100644 --- a/packages/datadog-plugin-elasticsearch/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-elasticsearch/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -41,7 +41,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'elasticsearch.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-express-mongo-sanitize/test/integration-test/client.spec.js b/packages/datadog-plugin-express-mongo-sanitize/test/integration-test/client.spec.js index cc38ab1ed5c..a99786cf8b0 100644 --- a/packages/datadog-plugin-express-mongo-sanitize/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-express-mongo-sanitize/test/integration-test/client.spec.js @@ -1,12 +1,12 @@ 'use strict' +const assert = require('node:assert/strict') +const axios = require('axios') const { sandboxCwd, useSandbox, varySandbox, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const axios = require('axios') withVersions('express-mongo-sanitize', 'express-mongo-sanitize', version => { describe('ESM', () => { diff --git a/packages/datadog-plugin-express-session/test/integration-test/client.spec.js b/packages/datadog-plugin-express-session/test/integration-test/client.spec.js index a80814cf808..47c783a22b1 100644 --- a/packages/datadog-plugin-express-session/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-express-session/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('express-session', 'express-session', version => { diff --git a/packages/datadog-plugin-express/src/index.js b/packages/datadog-plugin-express/src/index.js index 7fd77bdd718..2d01991aa89 100644 --- a/packages/datadog-plugin-express/src/index.js +++ b/packages/datadog-plugin-express/src/index.js @@ -1,8 +1,8 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const ExpressTracingPlugin = require('./tracing') const ExpressCodeOriginForSpansPlugin = require('./code_origin') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class ExpressPlugin extends CompositePlugin { static id = 'express' diff --git a/packages/datadog-plugin-express/test/index.spec.js b/packages/datadog-plugin-express/test/index.spec.js index 65305889bd8..16cbc29f722 100644 --- a/packages/datadog-plugin-express/test/index.spec.js +++ b/packages/datadog-plugin-express/test/index.spec.js @@ -8,6 +8,7 @@ const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const semver = require('semver') const sinon = require('sinon') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { NODE_MAJOR } = require('../../../version') const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') @@ -24,7 +25,10 @@ describe('Plugin', () => { withVersions('express', 'express', version => { // Express.js 4.10.5 and below have a Node.js incompatibility in the `fresh` package RE res._headers missing if (semver.intersects(version, '<=4.10.5') && NODE_MAJOR >= 24) { - describe.skip(`refusing to run tests as express@${version} is incompatible with Node.js ${NODE_MAJOR}`) + describe.skip( + `refusing to run tests as express@${version} is incompatible with Node.js ${NODE_MAJOR}`, + () => {} + ) return } @@ -118,15 +122,19 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta['http.route'], '/user') + assertObjectContains(spans[0], { + service: 'test', + type: 'web', + resource: 'GET /user', + meta: { + component: 'express', + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + 'http.route': '/user' + } + }) }) .then(done) .catch(done) @@ -154,15 +162,19 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /app/user/:id') - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[0].meta['_dd.integration'], 'express') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/app/user/1`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') + assertObjectContains(spans[0], { + service: 'test', + type: 'web', + resource: 'GET /app/user/:id', + meta: { + component: 'express', + '_dd.integration': 'express', + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/app/user/1`, + 'http.method': 'GET', + 'http.status_code': '200' + } + }) }) .then(done) .catch(done) @@ -192,14 +204,18 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /app/user/:id') - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/app/user/1`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') + assertObjectContains(spans[0], { + service: 'test', + type: 'web', + resource: 'GET /app/user/:id', + meta: { + component: 'express', + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/app/user/1`, + 'http.method': 'GET', + 'http.status_code': '200' + } + }) }) .then(done) .catch(done) @@ -232,34 +248,54 @@ describe('Plugin', () => { const whichMiddleware = isExpress4 ? 'express' : 'router' const rootSpan = spans[index++] - assert.strictEqual(rootSpan.resource, 'GET /app/user/:id') - assert.strictEqual(rootSpan.name, 'express.request') - assert.strictEqual(rootSpan.meta.component, 'express') + assertObjectContains(rootSpan, { + resource: 'GET /app/user/:id', + name: 'express.request', + meta: { + component: 'express' + } + }) if (isExpress4) { - assert.strictEqual(spans[index].resource, 'query') - assert.strictEqual(spans[index].name, 'express.middleware') + assertObjectContains(spans[index], { + resource: 'query', + name: 'express.middleware', + meta: { + component: 'express' + } + }) assert.strictEqual(spans[index].parent_id.toString(), rootSpan.span_id.toString()) - assert.strictEqual(spans[index].meta.component, 'express') index++ - assert.strictEqual(spans[index].resource, 'expressInit') - assert.strictEqual(spans[index].name, 'express.middleware') + assertObjectContains(spans[index], { + resource: 'expressInit', + name: 'express.middleware', + meta: { + component: 'express' + } + }) assert.strictEqual(spans[index].parent_id.toString(), rootSpan.span_id.toString()) - assert.strictEqual(spans[index].meta.component, 'express') index++ } - assert.strictEqual(spans[index].resource, 'named') - assert.strictEqual(spans[index].name, `${whichMiddleware}.middleware`) + assertObjectContains(spans[index], { + resource: 'named', + name: `${whichMiddleware}.middleware`, + meta: { + component: whichMiddleware + } + }) assert.strictEqual(spans[index].parent_id.toString(), rootSpan.span_id.toString()) - assert.strictEqual(spans[index].meta.component, whichMiddleware) index++ - assert.strictEqual(spans[index].resource, 'router') - assert.strictEqual(spans[index].name, `${whichMiddleware}.middleware`) + assertObjectContains(spans[index], { + resource: 'router', + name: `${whichMiddleware}.middleware`, + meta: { + component: whichMiddleware + } + }) assert.strictEqual(spans[index].parent_id.toString(), rootSpan.span_id.toString()) - assert.strictEqual(spans[index].meta.component, whichMiddleware) index++ if (isExpress4) { @@ -267,15 +303,23 @@ describe('Plugin', () => { } else { assert.strictEqual(spans[index].resource, 'handle') } - assert.strictEqual(spans[index].name, `${whichMiddleware}.middleware`) + assertObjectContains(spans[index], { + name: `${whichMiddleware}.middleware`, + meta: { + component: whichMiddleware + } + }) assert.strictEqual(spans[index].parent_id.toString(), spans[index - 1].span_id.toString()) - assert.strictEqual(spans[index].meta.component, whichMiddleware) index++ - assert.strictEqual(spans[index].resource, '') - assert.strictEqual(spans[index].name, `${whichMiddleware}.middleware`) + assertObjectContains(spans[index], { + resource: '', + name: `${whichMiddleware}.middleware`, + meta: { + component: whichMiddleware + } + }) assert.strictEqual(spans[index].parent_id.toString(), spans[index - 1].span_id.toString()) - assert.strictEqual(spans[index].meta.component, whichMiddleware) assert.strictEqual(index, spans.length - 1) }) @@ -318,12 +362,20 @@ describe('Plugin', () => { ? 'express' : 'router' - assert.strictEqual(spans[0].resource, 'GET /user/:id') - assert.strictEqual(spans[0].name, 'express.request') - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[breakingSpanIndex].resource, 'breaking') - assert.strictEqual(spans[breakingSpanIndex].name, `${whichMiddleware}.middleware`) - assert.strictEqual(spans[breakingSpanIndex].meta.component, whichMiddleware) + assertObjectContains(spans[0], { + resource: 'GET /user/:id', + name: 'express.request', + meta: { + component: 'express' + } + }) + assertObjectContains(spans[breakingSpanIndex], { + resource: 'breaking', + name: `${whichMiddleware}.middleware`, + meta: { + component: whichMiddleware + } + }) }) .then(done) .catch(done) @@ -366,11 +418,19 @@ describe('Plugin', () => { ? 'express' : 'router' - assert.strictEqual(spans[0].name, 'express.request') - assert.strictEqual(spans[errorSpanIndex].name, `${whichMiddleware}.middleware`) - assert.strictEqual(spans[errorSpanIndex].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[errorSpanIndex].meta.component, whichMiddleware) + assertObjectContains(spans[0], { + name: 'express.request', + meta: { + component: 'express' + } + }) + assertObjectContains(spans[errorSpanIndex], { + name: `${whichMiddleware}.middleware`, + meta: { + [ERROR_TYPE]: error.name, + component: whichMiddleware + } + }) }) .then(done) .catch(done) @@ -546,8 +606,12 @@ describe('Plugin', () => { const spans = sort(traces[0]) assert.strictEqual(spans.filter(span => span.name === 'express.request').length, 1) - assert.strictEqual(spans[0].resource, 'GET /parent/child') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + resource: 'GET /parent/child', + meta: { + component: 'express' + } + }) }) .then(done) .catch(done) @@ -750,7 +814,7 @@ describe('Plugin', () => { }) }) - it('long regex should not steal path', done => { + it('long regex should not steal path', function (done) { const app = express() try { @@ -760,7 +824,7 @@ describe('Plugin', () => { } catch (err) { // eslint-disable-next-line no-console console.log('This version of Express (>4.0 <4.6) has broken support for regex routing. Skipping this test.') - this.skip && this.skip() // mocha allows dynamic skipping, tap does not + this.skip() return done() } @@ -786,7 +850,7 @@ describe('Plugin', () => { }) }) - it('should work with regex having flags', done => { + it('should work with regex having flags', function (done) { const app = express() try { @@ -796,7 +860,7 @@ describe('Plugin', () => { } catch (err) { // eslint-disable-next-line no-console console.log('This version of Express (>4.0 <4.6) has broken support for regex routing. Skipping this test.') - this.skip && this.skip() // mocha allows dynamic skipping, tap does not + this.skip() return done() } @@ -822,7 +886,7 @@ describe('Plugin', () => { }) }) - it('long regex child of string router should not steal path', done => { + it('long regex child of string router should not steal path', function (done) { const app = express() const router = express.Router() @@ -834,7 +898,7 @@ describe('Plugin', () => { } catch (err) { // eslint-disable-next-line no-console console.log('This version of Express (>4.0 <4.6) has broken support for regex routing. Skipping this test.') - this.skip && this.skip() // mocha allows dynamic skipping, tap does not + this.skip() return done() } @@ -1091,10 +1155,14 @@ describe('Plugin', () => { agent.assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 1, + resource: 'GET /user', + meta: { + 'http.status_code': '500', + component: 'express' + } + }) done() }) @@ -1125,10 +1193,14 @@ describe('Plugin', () => { agent.assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 0) - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['http.status_code'], '400') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 0, + resource: 'GET /user', + meta: { + 'http.status_code': '400', + component: 'express' + } + }) done() }) @@ -1154,12 +1226,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + 'http.status_code': '500', + component: 'express' + } + }) }) .then(done) .catch(done) @@ -1190,16 +1266,24 @@ describe('Plugin', () => { ? 'express' : 'router' - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[secondErrorIndex].error, 1) - assert.strictEqual(spans[secondErrorIndex].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[secondErrorIndex].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[secondErrorIndex].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[secondErrorIndex].meta.component, whichMiddleware) + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'express' + } + }) + assertObjectContains(spans[secondErrorIndex], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: whichMiddleware + } + }) }) .then(done) .catch(done) @@ -1227,16 +1311,26 @@ describe('Plugin', () => { const spans = sort(traces[0]) const secondErrorIndex = spans.length - 2 - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'express') - assert.strictEqual(spans[secondErrorIndex].error, 1) - assert.strictEqual(spans[secondErrorIndex].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[secondErrorIndex].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[secondErrorIndex].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'express' + } + }) + assertObjectContains(spans[secondErrorIndex], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + } + }) + assertObjectContains(spans[0].meta, { + component: 'express' + }) }) .then(done) .catch(done) @@ -1249,9 +1343,9 @@ describe('Plugin', () => { }) }) - it('should support capturing groups in routes', done => { + it('should support capturing groups in routes', function (done) { if (semver.intersects(version, '>=5.0.0')) { - this.skip && this.skip() // mocha allows dynamic skipping, tap does not + this.skip() return done() } @@ -1268,8 +1362,12 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].resource, 'GET /:path(*)') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user`) + assertObjectContains(spans[0], { + resource: 'GET /:path(*)', + meta: { + 'http.url': `http://localhost:${port}/user` + } + }) }) .then(done) .catch(done) @@ -1294,8 +1392,12 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].resource, 'GET /*user') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user`) + assertObjectContains(spans[0], { + resource: 'GET /*user', + meta: { + 'http.url': `http://localhost:${port}/user` + } + }) }) .then(done) .catch(done) @@ -1372,10 +1474,14 @@ describe('Plugin', () => { agent.assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 0) - assert.strictEqual(spans[0].resource, 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '404') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 0, + resource: 'GET', + meta: { + 'http.status_code': '404', + component: 'express' + } + }) assert.ok(!('http.route' in spans[0].meta)) }).then(done).catch(done) @@ -1410,14 +1516,18 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /dd') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/dd`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + service: 'test', + type: 'web', + resource: 'GET /dd', + meta: { + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/dd`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'express' + } + }) }) .then(done) .catch(done) @@ -1678,10 +1788,14 @@ describe('Plugin', () => { agent.assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 1, + resource: 'GET /user', + meta: { + 'http.status_code': '500', + component: 'express' + } + }) done() }) @@ -1713,10 +1827,14 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 0) - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['http.status_code'], '400') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 0, + resource: 'GET /user', + meta: { + 'http.status_code': '400', + component: 'express' + } + }) }) .then(done) .catch(done) @@ -1743,11 +1861,15 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'express' + } + }) }) .then(done) .catch(done) @@ -1773,12 +1895,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = sort(traces[0]) - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'express') + assertObjectContains(spans[0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + 'http.status_code': '500', + component: 'express' + } + }) }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-express/test/integration-test/client.spec.js b/packages/datadog-plugin-express/test/integration-test/client.spec.js index 94b862c2e16..ccc0d4d692e 100644 --- a/packages/datadog-plugin-express/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-express/test/integration-test/client.spec.js @@ -2,6 +2,7 @@ const assert = require('node:assert/strict') +const semver = require('semver') const { FakeAgent, curlAndAssertMessage, @@ -11,7 +12,6 @@ const { varySandbox } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const semver = require('semver') describe('esm', () => { withVersions('express', 'express', version => { diff --git a/packages/datadog-plugin-fastify/src/index.js b/packages/datadog-plugin-fastify/src/index.js index 5871b15c4a5..0b91b2595df 100644 --- a/packages/datadog-plugin-fastify/src/index.js +++ b/packages/datadog-plugin-fastify/src/index.js @@ -1,8 +1,8 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const FastifyTracingPlugin = require('./tracing') const FastifyCodeOriginForSpansPlugin = require('./code_origin') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class FastifyPlugin extends CompositePlugin { static id = 'fastify' diff --git a/packages/datadog-plugin-fastify/test/integration-test/client.spec.js b/packages/datadog-plugin-fastify/test/integration-test/client.spec.js index 1fc95ca22d0..ddf0a7a3df3 100644 --- a/packages/datadog-plugin-fastify/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-fastify/test/integration-test/client.spec.js @@ -2,6 +2,7 @@ const assert = require('node:assert/strict') +const { join } = require('path') const { FakeAgent, curlAndAssertMessage, @@ -9,7 +10,6 @@ const { spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') const { withVersions, insertVersionDep } = require('../../../dd-trace/test/setup/mocha') -const { join } = require('path') describe('esm', () => { let agent diff --git a/packages/datadog-plugin-fastify/test/tracing.spec.js b/packages/datadog-plugin-fastify/test/tracing.spec.js index f8ef0df4981..4eacd8d87a5 100644 --- a/packages/datadog-plugin-fastify/test/tracing.spec.js +++ b/packages/datadog-plugin-fastify/test/tracing.spec.js @@ -10,6 +10,7 @@ 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 { withExports, withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') const host = 'localhost' describe('Plugin', () => { @@ -58,16 +59,20 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'fastify') - assert.strictEqual(spans[0].meta['_dd.integration'], 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + service: 'test', + type: 'web', + resource: 'GET /user', + meta: { + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'fastify', + '_dd.integration': 'fastify' + } + }) }) .then(done) .catch(done) @@ -94,15 +99,19 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /user/:id') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user/123`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + service: 'test', + type: 'web', + resource: 'GET /user/:id', + meta: { + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/user/123`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -128,15 +137,19 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].service, 'test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET /user/:id') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://localhost:${port}/user/123`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + service: 'test', + type: 'web', + resource: 'GET /user/:id', + meta: { + 'span.kind': 'server', + 'http.url': `http://localhost:${port}/user/123`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -275,12 +288,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + resource: 'GET /user', + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -354,13 +371,17 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + resource: 'GET /user', + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -387,10 +408,14 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].error, 0) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + resource: 'GET /user', + error: 0, + meta: { + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -418,12 +443,16 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + resource: 'GET /user', + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -452,13 +481,17 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + resource: 'GET /user', + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'fastify' + } + }) }) .then(done) .catch(done) @@ -491,7 +524,9 @@ describe('Plugin', () => { assert.ok(!(ERROR_TYPE in spans[0].meta)) assert.ok(!(ERROR_MESSAGE in spans[0].meta)) assert.ok(!(ERROR_STACK in spans[0].meta)) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0].meta, { + component: 'fastify' + }) }) .then(done) .catch(done) @@ -524,13 +559,17 @@ describe('Plugin', () => { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[0].name, 'fastify.request') - assert.strictEqual(spans[0].resource, 'GET /user') - assert.strictEqual(spans[0].error, 1) - assert.strictEqual(spans[0].meta[ERROR_TYPE], error.name) - assert.strictEqual(spans[0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(spans[0].meta[ERROR_STACK], error.stack) - assert.strictEqual(spans[0].meta.component, 'fastify') + assertObjectContains(spans[0], { + name: 'fastify.request', + resource: 'GET /user', + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'fastify' + } + }) }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-fetch/test/index.spec.js b/packages/datadog-plugin-fetch/test/index.spec.js index de67cebf267..8d77bccfe17 100644 --- a/packages/datadog-plugin-fetch/test/index.spec.js +++ b/packages/datadog-plugin-fetch/test/index.spec.js @@ -9,6 +9,7 @@ const { storage } = require('../../datadog-core') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { rawExpectedSchema } = require('./naming') const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS @@ -69,19 +70,20 @@ describe('Plugin', function () { res.status(200).send() }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'GET') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'GET') - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta.component, 'fetch') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'fetch') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'GET', + meta: { + 'span.kind': 'client', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'fetch', + '_dd.integration': 'fetch', + 'out.host': 'localhost' + } + }) .then(done) .catch(done) @@ -95,18 +97,19 @@ describe('Plugin', function () { res.status(200).send() }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'POST') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'POST') - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta.component, 'fetch') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'POST', + meta: { + 'span.kind': 'client', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'POST', + 'http.status_code': '200', + component: 'fetch', + 'out.host': 'localhost' + } + }) .then(done) .catch(done) @@ -120,18 +123,19 @@ describe('Plugin', function () { res.status(200).send() }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'GET') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'GET') - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta.component, 'fetch') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'GET', + meta: { + 'span.kind': 'client', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'fetch', + 'out.host': 'localhost' + } + }) .then(done) .catch(done) @@ -162,11 +166,12 @@ describe('Plugin', function () { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/user`) - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200', + 'http.url': `http://localhost:${port}/user` + } + }) .then(done) .catch(done) @@ -185,10 +190,11 @@ describe('Plugin', function () { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200' + } + }) .then(done) .catch(done) @@ -208,10 +214,11 @@ describe('Plugin', function () { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200' + } + }) .then(done) .catch(done) @@ -224,10 +231,12 @@ describe('Plugin', function () { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message || error.code) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'fetch') + assertObjectContains(traces[0][0].meta, { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message || error.code, + [ERROR_STACK]: error.stack, + component: 'fetch' + }) }) .then(done) .catch(done) @@ -651,18 +660,19 @@ describe('Plugin', function () { res.status(200).send() }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'GET') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'GET') - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta.component, 'fetch') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'GET', + meta: { + 'span.kind': 'client', + 'http.url': `http://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'fetch', + 'out.host': 'localhost' + } + }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-fetch/test/integration-test/client.spec.js b/packages/datadog-plugin-fetch/test/integration-test/client.spec.js index ea32c88004b..20f938da31a 100644 --- a/packages/datadog-plugin-fetch/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-fetch/test/integration-test/client.spec.js @@ -6,7 +6,7 @@ const { FakeAgent, sandboxCwd, useSandbox, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const describe = globalThis.fetch ? globalThis.describe : globalThis.describe.skip @@ -35,7 +35,7 @@ describe('esm', () => { assert.strictEqual(isFetch, true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(50000) diff --git a/packages/datadog-plugin-fs/test/index.spec.js b/packages/datadog-plugin-fs/test/index.spec.js index 1469988cdc3..6fe14a0f0e9 100644 --- a/packages/datadog-plugin-fs/test/index.spec.js +++ b/packages/datadog-plugin-fs/test/index.spec.js @@ -5,7 +5,6 @@ const os = require('node:os') const path = require('node:path') const util = require('node:util') -const { expect } = require('chai') const { channel } = require('dc-polyfill') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const realFS = Object.assign({}, require('node:fs')) @@ -44,7 +43,7 @@ describe('Plugin', () => { data.forEach((arr) => { arr.forEach((trace) => { if (trace.name === 'fs.operation') { - expect.fail('should not have been any fs traces') + assert.fail('should not have been any fs traces') } }) }) @@ -98,7 +97,7 @@ describe('Plugin', () => { describe('open', () => { it('should not be instrumented', (done) => { agent.assertSomeTraces(() => { - expect.fail('should not have been any traces') + assert.fail('should not have been any traces') }).catch(done) setTimeout(done, 1500) // allow enough time to ensure no traces happened diff --git a/packages/datadog-plugin-generic-pool/test/integration-test/client.spec.js b/packages/datadog-plugin-generic-pool/test/integration-test/client.spec.js index 95337476575..2e892456b02 100644 --- a/packages/datadog-plugin-generic-pool/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-generic-pool/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('generic-pool', 'generic-pool', '<3', version => { diff --git a/packages/datadog-plugin-generic-pool/test/integration-test/server.mjs b/packages/datadog-plugin-generic-pool/test/integration-test/server.mjs index 1e9e62fcc83..c2554911e76 100644 --- a/packages/datadog-plugin-generic-pool/test/integration-test/server.mjs +++ b/packages/datadog-plugin-generic-pool/test/integration-test/server.mjs @@ -1,7 +1,7 @@ import 'dd-trace/init.js' +import { AsyncLocalStorage } from 'async_hooks' import express from 'express' import genericPool from 'generic-pool' -import { AsyncLocalStorage } from 'async_hooks' const asyncStore = new AsyncLocalStorage() const app = express() diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/client.js b/packages/datadog-plugin-google-cloud-pubsub/src/client.js index 162031b999a..bf4f2fb0bd6 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/client.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/client.js @@ -8,11 +8,11 @@ class GoogleCloudPubsubClientPlugin extends ClientPlugin { static operation = 'request' start (ctx) { - const { request, api, projectId } = ctx + const { request, api, projectId, storedContext } = ctx if (api === 'publish') return - this.startSpan(this.operationName(), { + const spanOptions = { service: this.config.service || this.serviceName(), resource: [api, request.name].filter(Boolean).join(' '), kind: this.constructor.kind, @@ -20,7 +20,17 @@ class GoogleCloudPubsubClientPlugin extends ClientPlugin { 'pubsub.method': api, 'gcloud.project_id': projectId } - }, ctx) + } + + /** + * Use stored context from consumer plugin to link acknowledge span to message processing span. + * Without this, the acknowledge span would be orphaned (no async context available). + */ + if (storedContext?.span) { + spanOptions.childOf = storedContext.span.context() + } + + this.startSpan(this.operationName(), spanOptions, ctx) return ctx.currentStore } diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js b/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js index af46088a16a..c785399a5a5 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js @@ -2,30 +2,188 @@ const { getMessageSize } = require('../../dd-trace/src/datastreams') const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') +const SpanContext = require('../../dd-trace/src/opentracing/span_context') +const id = require('../../dd-trace/src/id') +const { storage } = require('../../datadog-core') + +/** + * PULL SUBSCRIPTION: Service explicitly pulls messages from Pub/Sub and processes them. + * Async context storage for linking acknowledge() API calls back to original message spans. + * + * Problem: message.ack() is often called asynchronously outside the original handler, + * losing async context. The acknowledge() API only receives ackIds (strings), not Messages. + * + * Solution: Store context by Message (WeakMap), lookup by ackId (Map>). + * WeakRef allows Messages to be GC'd even if never acknowledged (network failures, crashes). + */ +const messageToContext = new WeakMap() // Message -> context (auto-cleanup on GC) +const ackIdToMessage = new Map() // ackId -> WeakRef (needs cleanup) + +const ackMapCleanup = new FinalizationRegistry((ackId) => { + ackIdToMessage.delete(ackId) // Remove orphaned ackId when Message is GC'd +}) class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin { static id = 'google-cloud-pubsub' static operation = 'receive' + constructor (...args) { + super(...args) + + /** + * Channel: message:ack-store + * When message.ack() is called, capture and store the current async context. + * Use WeakRef to allow Message GC even if never acknowledged. + */ + this.addSub('apm:google-cloud-pubsub:message:ack-store', (ctx) => { + const { message, ackId } = ctx + const currentStore = storage('legacy').getStore() + + if (currentStore) { + messageToContext.set(message, currentStore) + ackIdToMessage.set(ackId, new WeakRef(message)) + ackMapCleanup.register(message, ackId, message) + } + }) + + /** + * Channel: message:ack-retrieve + * When acknowledge() API is called, retrieve stored context for the ackIds. + * Map: ackId -> WeakRef -> stored context. + * Clean up immediately after acknowledge (happy path), or via FinalizationRegistry (GC). + */ + this.addSub('apm:google-cloud-pubsub:message:ack-retrieve', ({ ackIds, api, ctx }) => { + for (const ackId of ackIds) { + const weakRef = ackIdToMessage.get(ackId) + if (weakRef) { + const message = weakRef.deref() + if (message) { + const storedContext = messageToContext.get(message) + if (storedContext) { + ctx.storedContext = storedContext + break + } + } + } + } + + if (api === 'acknowledge') { + ackIds.forEach(ackId => ackIdToMessage.delete(ackId)) + } + }) + } + + #reconstructPubSubRequestContext (attrs) { + const traceIdLower = attrs['_dd.pubsub_request.trace_id'] + const spanId = attrs['_dd.pubsub_request.span_id'] + const traceIdUpper = attrs['_dd.pubsub_request.p.tid'] + + if (!traceIdLower || !spanId) return null + + const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0') + const traceId = id(traceId128, 16) + const parentId = id(spanId, 16) + + const tags = {} + if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper + + return new SpanContext({ + traceId, + spanId: parentId, + tags + }) + } + bindStart (ctx) { const { message } = ctx const subscription = message._subscriber._subscription - const topic = subscription.metadata && subscription.metadata.topic - const childOf = this.tracer.extract('text_map', message.attributes) || null + const topic = subscription?.metadata?.topic || message.attributes?.['pubsub.topic'] + + const batchRequestTraceId = message.attributes?.['_dd.pubsub_request.trace_id'] + const batchRequestSpanId = message.attributes?.['_dd.pubsub_request.span_id'] + + // Parse batch metadata once upfront for reuse + const batchSizeStr = message.attributes?.['_dd.batch.size'] + const batchIndexStr = message.attributes?.['_dd.batch.index'] + const batchSize = batchSizeStr ? Number(batchSizeStr) : undefined + const batchIndex = batchIndexStr ? Number(batchIndexStr) : undefined + + let childOf = this.tracer.extract('text_map', message.attributes) + + const isFirstMessage = batchIndex === 0 + if (isFirstMessage && batchRequestSpanId) { + const pubsubRequestContext = this.#reconstructPubSubRequestContext(message.attributes) + if (pubsubRequestContext) { + childOf = pubsubRequestContext + } + } + + const topicName = topic?.slice(topic.lastIndexOf('/') + 1) ?? + subscription.name.slice(subscription.name.lastIndexOf('/') + 1) + const baseService = this.tracer._service || 'unknown' + const serviceName = this.config.service || `${baseService}-pubsub` + const meta = { + 'gcloud.project_id': subscription.pubsub.projectId, + 'pubsub.topic': topic, + 'span.kind': 'consumer', + 'pubsub.subscription_type': 'pull', + 'pubsub.span_type': 'message_processing', + 'messaging.operation': 'receive', + base_service: baseService, + service_override_type: 'custom' + } + + if (batchRequestTraceId && batchRequestSpanId) { + meta['pubsub.batch.request_trace_id'] = batchRequestTraceId + // Use JSON format like producer for proper span link parsing + meta['_dd.span_links'] = JSON.stringify([{ + trace_id: batchRequestTraceId, + span_id: batchRequestSpanId, + flags: 0 + }]) + } + + const metrics = { + 'pubsub.ack': 0 + } + + if (batchSize) { + metrics['pubsub.batch.message_count'] = batchSize + metrics['pubsub.batch.size'] = batchSize + } + if (batchIndex !== undefined) { + metrics['pubsub.batch.message_index'] = batchIndex + metrics['pubsub.batch.index'] = batchIndex + } + + if (batchSize && batchIndex !== undefined) { + meta['pubsub.batch.description'] = `Message ${batchIndex + 1} of ${batchSize}` + } const span = this.startSpan({ childOf, - resource: topic, + resource: `Message from ${topicName}`, type: 'worker', - meta: { - 'gcloud.project_id': subscription.pubsub.projectId, - 'pubsub.topic': topic - }, - metrics: { - 'pubsub.ack': 0 - } + service: serviceName, + meta, + metrics }, ctx) + if (message.id) { + span.setTag('pubsub.message_id', message.id) + } + if (message.publishTime) { + span.setTag('pubsub.publish_time', message.publishTime.toISOString()) + } + + if (message.attributes) { + const publishStartTime = message.attributes['x-dd-publish-start-time'] + if (publishStartTime) { + const deliveryDuration = Date.now() - Number(publishStartTime) + span.setTag('pubsub.delivery_duration_ms', deliveryDuration) + } + } + if (this.config.dsmEnabled && message?.attributes) { const payloadSize = getMessageSize(message) this.tracer.decodeDataStreamsContext(message.attributes) @@ -38,14 +196,15 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin { bindFinish (ctx) { const { message } = ctx - const span = ctx.currentStore.span + const span = ctx.currentStore?.span + + if (!span) return ctx.parentStore if (message?._handled) { span.setTag('pubsub.ack', 1) } super.finish() - return ctx.parentStore } } diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/index.js b/packages/datadog-plugin-google-cloud-pubsub/src/index.js index 8b8f5f4fe7f..ef08e4c9f83 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/index.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/index.js @@ -1,11 +1,10 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const ProducerPlugin = require('./producer') const ConsumerPlugin = require('./consumer') const ClientPlugin = require('./client') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') -// TODO: Consider splitting channels for publish/receive in the instrumentation. class GoogleCloudPubsubPlugin extends CompositePlugin { static id = 'google-cloud-pubsub' static get plugins () { diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/producer.js b/packages/datadog-plugin-google-cloud-pubsub/src/producer.js index 94644e4e063..d9f7fbd870d 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +++ b/packages/datadog-plugin-google-cloud-pubsub/src/producer.js @@ -2,42 +2,189 @@ const ProducerPlugin = require('../../dd-trace/src/plugins/producer') const { DsmPathwayCodec, getHeadersSize } = require('../../dd-trace/src/datastreams') +const id = require('../../dd-trace/src/id') class GoogleCloudPubsubProducerPlugin extends ProducerPlugin { static id = 'google-cloud-pubsub' static operation = 'request' + constructor (...args) { + super(...args) + + /** + * Inject trace context into individual messages published via Topic.publish() or + * Topic.publishMessage(). This happens before the batch publish API call, allowing + * each message to propagate trace context to consumers. + */ + this.addSub('apm:google-cloud-pubsub:message:publish', this.handleMessagePublish.bind(this)) + } + + handleMessagePublish ({ attributes, pubsub, topicName }) { + // Skip if message already has trace context from upstream + if (attributes['x-datadog-trace-id'] || attributes.traceparent) return + + const activeSpan = this.tracer.scope().active() + if (!activeSpan) return + + // Inject current span's trace context into message attributes + this.tracer.inject(activeSpan, 'text_map', attributes) + + const traceIdUpperBits = activeSpan.context()._trace.tags['_dd.p.tid'] + if (traceIdUpperBits) attributes['_dd.p.tid'] = traceIdUpperBits + + if (pubsub) attributes['gcloud.project_id'] = pubsub.projectId + if (topicName) attributes['pubsub.topic'] = topicName + } + bindStart (ctx) { const { request, api, projectId } = ctx - if (api !== 'publish') return const messages = request.messages || [] const topic = request.topic - const span = this.startSpan({ // TODO: rename - resource: `${api} ${topic}`, + const messageCount = messages.length + const hasTraceContext = messages[0]?.attributes?.['x-datadog-trace-id'] + + /** + * Batch Publishing Strategy: + * - Create one "batch span" representing the entire publish operation + * - If messages already have trace context (from upstream), use the first message's + * context as the parent, and create span links for messages 2-N + * - Otherwise, use the current active span as parent + * - 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 firstAttrs = messages[0]?.attributes + const parentData = firstAttrs?.['x-datadog-trace-id'] && firstAttrs['x-datadog-parent-id'] + ? { + traceId: firstAttrs['x-datadog-trace-id'], + spanId: firstAttrs['x-datadog-parent-id'], + traceIdUpper: firstAttrs['_dd.p.tid'], + samplingPriority: firstAttrs['x-datadog-sampling-priority'] + } + : null + + const lastSlash = topic.lastIndexOf('/') + const topicName = lastSlash === -1 ? topic : topic.slice(lastSlash + 1) + const batchSpan = this.startSpan({ + childOf: parentData ? this.#extractParentContext(parentData) : undefined, + resource: `${api} to Topic ${topicName}`, meta: { 'gcloud.project_id': projectId, - 'pubsub.method': api, // TODO: remove - 'pubsub.topic': topic + 'pubsub.method': api, + 'pubsub.topic': topic, + 'span.kind': 'producer', + '_dd.base_service': this.tracer._service, + '_dd.serviceoverride.type': 'integration', + 'pubsub.linked_message_count': spanLinkData.length || undefined, + operation: messageCount > 1 ? 'batched.pubsub.request' : 'pubsub.request' + }, + metrics: { + 'pubsub.batch.message_count': messageCount, + 'pubsub.batch': messageCount > 1 ? true : undefined } }, ctx) - for (const msg of messages) { - if (!msg.attributes) { - msg.attributes = {} + const spanCtx = batchSpan.context() + // Get 128-bit trace ID and span ID as hex strings + const fullTraceIdHex = spanCtx.toTraceId(true) + const batchSpanIdHex = spanCtx.toSpanId(true) + // Extract lower 64 bits (last 16 hex chars) for trace ID + const batchTraceIdHex = fullTraceIdHex.slice(-16) + const batchTraceIdUpper = spanCtx._trace.tags['_dd.p.tid'] + + if (spanLinkData.length) { + batchSpan.setTag('_dd.span_links', JSON.stringify( + spanLinkData.map(link => ({ + trace_id: link.traceId, + span_id: link.spanId, + flags: link.samplingPriority || 0 + })) + )) + } + + messages.forEach((msg, i) => { + msg.attributes ??= {} + + if (!hasTraceContext) { + this.tracer.inject(batchSpan, 'text_map', msg.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)) + }) + + if (batchTraceIdUpper) { + msg.attributes['_dd.pubsub_request.p.tid'] = batchTraceIdUpper } - this.tracer.inject(span, 'text_map', msg.attributes) + if (this.config.dsmEnabled) { - const payloadSize = getHeadersSize(msg) - const dataStreamsContext = this.tracer - .setCheckpoint(['direction:out', `topic:${topic}`, 'type:google-pubsub'], span, payloadSize) + const dataStreamsContext = this.tracer.setCheckpoint( + ['direction:out', `topic:${topic}`, 'type:google-pubsub'], + batchSpan, + getHeadersSize(msg) + ) DsmPathwayCodec.encode(dataStreamsContext, msg.attributes) } - } + }) + ctx.batchSpan = batchSpan return ctx.currentStore } + + bindFinish (ctx) { + if (ctx.batchSpan && !ctx.batchSpan._duration) ctx.batchSpan.finish() + return super.bindFinish(ctx) + } + + bindError (ctx) { + if (ctx.error && ctx.batchSpan) { + ctx.batchSpan.setTag('error', ctx.error) + } + return ctx.parentStore + } + + #extractSpanLink (attrs) { + if (!attrs?.['x-datadog-trace-id'] || !attrs['x-datadog-parent-id']) return null + + // Convert to hex strings + const lowerHex = id(attrs['x-datadog-trace-id']).toString(16) + const spanIdHex = id(attrs['x-datadog-parent-id']).toString(16) + + // Build full 128-bit trace ID + const traceIdHex = attrs['_dd.p.tid'] + ? attrs['_dd.p.tid'] + lowerHex + : lowerHex.padStart(32, '0') + + return { + traceId: traceIdHex, + spanId: spanIdHex, + samplingPriority: attrs['x-datadog-sampling-priority'] + ? Number.parseInt(attrs['x-datadog-sampling-priority'], 10) + : undefined + } + } + + #extractParentContext (data) { + const carrier = { + 'x-datadog-trace-id': data.traceId, + 'x-datadog-parent-id': data.spanId + } + if (data.traceIdUpper) carrier['_dd.p.tid'] = data.traceIdUpper + if (data.samplingPriority) carrier['x-datadog-sampling-priority'] = String(data.samplingPriority) + + return this.tracer.extract('text_map', carrier) + } } module.exports = GoogleCloudPubsubProducerPlugin diff --git a/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js b/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js new file mode 100644 index 00000000000..ee61d803d7c --- /dev/null +++ b/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js @@ -0,0 +1,217 @@ +'use strict' + +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const SpanContext = require('../../dd-trace/src/opentracing/span_context') +const id = require('../../dd-trace/src/id') +const log = require('../../dd-trace/src/log') + +// WeakMap to track push receive spans by request +const pushReceiveSpans = new WeakMap() + +class GoogleCloudPubsubPushSubscriptionPlugin extends TracingPlugin { + static get id () { return 'google-cloud-pubsub-push-subscription' } + + constructor (...args) { + super(...args) + + /** + * PUSH SUBSCRIPTION: GCP sends HTTP POST requests to our service with message data in headers. + * We intercept these requests to create a pubsub.push.receive span that wraps the HTTP request. + * + * Flow: Detect push request -> Extract trace context -> Create receive span -> Activate it + * Hierarchy: pubsub.push.receive (parent) -> http.request (child) -> express.middleware... + * + * Plugin load order (http/index.js) ensures we subscribe before HttpServerPlugin. + */ + this.addSub('apm:http:server:request:start', (ctx) => { + this.#handlePubSubRequest(ctx) + }) + + this.addSub('apm:http:server:request:finish', ({ req }) => { + this.#finishPushReceiveSpan(req) + }) + } + + #finishPushReceiveSpan (req) { + const pushReceiveSpan = pushReceiveSpans.get(req) + if (pushReceiveSpan && !pushReceiveSpan._duration) { + pushReceiveSpan.finish() + pushReceiveSpans.delete(req) + } + } + + #handlePubSubRequest (ctx) { + const { req } = ctx + const userAgent = req.headers['user-agent'] || '' + if (req.method !== 'POST' || !userAgent.includes('APIs-Google')) { + return false + } + + if (req.headers['x-goog-pubsub-message-id']) { + this.#createPushReceiveSpanAndActivate(ctx) + return true + } + + log.warn( + '[PubSub] No x-goog-pubsub-* headers detected. pubsub.push.receive spans will not be created. ' + + 'Add --push-no-wrapper-write-metadata to your subscription.' + ) + return false + } + + #createPushReceiveSpanAndActivate (ctx) { + const { req, res } = ctx + const messageData = this.#parseMessage(req) + if (!messageData) { + return + } + + const originalContext = this.#extractContext(messageData) + const pubsubRequestContext = this.#reconstructPubSubContext(messageData.attrs) || originalContext + const isSameTrace = pubsubRequestContext && + originalContext?.toTraceId() === pubsubRequestContext.toTraceId() + + /** + * Create receive span, choosing parent context: + * - Same trace: use batch context (message is part of the batch trace) + * - Different trace: use message context as parent, link to batch for observability + * + * this.enter() activates the span so the HTTP request span becomes its child. + */ + const pushReceiveSpan = this.#createPushReceiveSpan( + messageData, + isSameTrace ? pubsubRequestContext : originalContext, + isSameTrace ? null : pubsubRequestContext + ) + + if (!pushReceiveSpan) { + return + } + + this.enter(pushReceiveSpan, { req, res }) + pushReceiveSpans.set(req, pushReceiveSpan) + } + + #parseMessage (req) { + const subscription = req.headers['x-goog-pubsub-subscription-name'] + const message = { + messageId: req.headers['x-goog-pubsub-message-id'], + publishTime: req.headers['x-goog-pubsub-publish-time'] + } + + const topicName = req.headers['pubsub.topic'] || 'push-subscription-topic' + return { message, subscription, attrs: req.headers, topicName } + } + + #extractContext (messageData) { + return this.tracer.extract('text_map', messageData.attrs) + } + + #reconstructPubSubContext (attrs) { + /** + * Reconstruct the batch publish span context from message attributes. + * + * When a batch is published, the producer injects: + * - _dd.pubsub_request.trace_id: lower 64 bits of the batch span's trace ID (hex) + * - _dd.pubsub_request.span_id: the batch span's span ID (hex) + * - _dd.pubsub_request.p.tid: upper 64 bits of trace ID (hex, optional for 128-bit traces) + * + * This context represents the "pubsub.request" span on the producer side. + * We use it to create span links, connecting each pubsub.push.receive span back to the original batch. + */ + const traceIdLower = attrs['_dd.pubsub_request.trace_id'] + const spanId = attrs['_dd.pubsub_request.span_id'] + const traceIdUpper = attrs['_dd.pubsub_request.p.tid'] + + if (!traceIdLower || !spanId) return null + + // Reconstruct full 128-bit trace ID (or pad 64-bit to 128-bit) + const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0') + const traceId = id(traceId128, 16) + const parentId = id(spanId, 16) + + const tags = {} + if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper + + return new SpanContext({ + traceId, + spanId: parentId, + tags + }) + } + + #createPushReceiveSpan (messageData, parentContext, linkContext) { + const { message, subscription, topicName, attrs } = messageData + const subscriptionName = subscription?.slice(subscription.lastIndexOf('/') + 1) ?? subscription + const publishStartTime = attrs['x-dd-publish-start-time'] + const startTime = publishStartTime ? Number.parseInt(publishStartTime, 10) : undefined + + // Get the base service name and construct the pubsub service override + const baseService = this.tracer._service + const serviceOverride = this.config.service ?? `${baseService}-pubsub` + + // Use this.startSpan() which automatically activates the span + const span = this.startSpan('pubsub.push.receive', { + childOf: parentContext, + startTime, + kind: 'consumer', + service: serviceOverride, + meta: { + component: 'google-cloud-pubsub', + 'pubsub.method': 'receive', + 'pubsub.subscription': subscription, + 'pubsub.message_id': message.messageId, + 'pubsub.subscription_type': 'push', + 'pubsub.topic': topicName, + '_dd.base_service': baseService, + '_dd.serviceoverride.type': 'integration', + 'resource.name': `Push Subscription ${subscriptionName}` + } + }) + + if (!span) { + return null + } + + span._integrationName = 'google-cloud-pubsub' + // Calculate delivery latency (queue time from publish to delivery) + if (publishStartTime) { + const deliveryDuration = Date.now() - Number(publishStartTime) + span.setTag('pubsub.delivery_duration_ms', deliveryDuration) + } + + this.#addBatchMetadata(span, attrs) + + if (linkContext) { + if (span.addLink) { + span.addLink(linkContext, {}) + } else { + span._links ??= [] + span._links.push({ context: linkContext, attributes: {} }) + } + } + + return span + } + + #addBatchMetadata (span, attrs) { + const batchSizeStr = attrs['_dd.batch.size'] + const batchIndexStr = attrs['_dd.batch.index'] + + if (!batchSizeStr || batchIndexStr === undefined) return + + const size = Number(batchSizeStr) + const index = Number(batchIndexStr) + + span.setTag('pubsub.batch.message_count', size) + span.setTag('pubsub.batch.message_index', index) + span.setTag('pubsub.batch.description', `Message ${index + 1} of ${size}`) + + const requestTraceId = attrs['_dd.pubsub_request.trace_id'] + if (requestTraceId) { + span.setTag('pubsub.batch.request_trace_id', requestTraceId) + } + } +} + +module.exports = GoogleCloudPubsubPushSubscriptionPlugin diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/dsm.spec.js b/packages/datadog-plugin-google-cloud-pubsub/test/dsm.spec.js new file mode 100644 index 00000000000..05f5a3e9f20 --- /dev/null +++ b/packages/datadog-plugin-google-cloud-pubsub/test/dsm.spec.js @@ -0,0 +1,166 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { after, before, beforeEach, describe, it } = require('mocha') +const sinon = require('sinon') + +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') +const id = require('../../dd-trace/src/id') +const agent = require('../../dd-trace/test/plugins/agent') +const { withVersions } = require('../../dd-trace/test/setup/mocha') + +const TIMEOUT = 30000 +const dsmTopicName = 'dsm-topic' + +describe('Plugin', () => { + let tracer + + describe('google-cloud-pubsub', function () { + this.timeout(TIMEOUT) + + before(() => { + process.env.PUBSUB_EMULATOR_HOST = 'localhost:8081' + process.env.DD_DATA_STREAMS_ENABLED = 'true' + }) + + after(() => { + delete process.env.PUBSUB_EMULATOR_HOST + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + withVersions('google-cloud-pubsub', '@google-cloud/pubsub', version => { + let pubsub + let project + let expectedProducerHash + let expectedConsumerHash + + describe('data stream monitoring', () => { + let dsmTopic + let sub + let consume + + before(async () => { + tracer = require('../../dd-trace') + await agent.load('google-cloud-pubsub', { + dsmEnabled: true + }) + tracer.use('google-cloud-pubsub', { dsmEnabled: true }) + + const { PubSub } = require(`../../../versions/@google-cloud/pubsub@${version}`).get() + project = getProjectId() + pubsub = new PubSub({ projectId: project }) + + dsmTopic = await pubsub.createTopic(dsmTopicName) + dsmTopic = dsmTopic[0] + sub = await dsmTopic.createSubscription('DSM') + sub = sub[0] + consume = function (cb) { + sub.on('message', cb) + } + + const dsmFullTopic = `projects/${project}/topics/${dsmTopicName}` + + expectedProducerHash = computePathwayHash( + 'test', + 'tester', + ['direction:out', 'topic:' + dsmFullTopic, 'type:google-pubsub'], + ENTRY_PARENT_HASH + ) + expectedConsumerHash = computePathwayHash( + 'test', + 'tester', + ['direction:in', 'topic:' + dsmFullTopic, 'type:google-pubsub'], + expectedProducerHash + ) + }) + + beforeEach(() => { + return agent.load('google-cloud-pubsub', { + dsmEnabled: true + }) + }) + + describe('should set a DSM checkpoint', () => { + it('on produce', async () => { + await publish(dsmTopic, { data: Buffer.from('DSM produce checkpoint') }) + + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 1 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 1) + assert.strictEqual(agent.dsmStatsExist(agent, expectedProducerHash.readBigUInt64BE(0).toString()), true) + }, { timeoutMs: TIMEOUT }) + }) + + it('on consume', async () => { + await publish(dsmTopic, { data: Buffer.from('DSM consume checkpoint') }) + await consume(async () => { + agent.expectPipelineStats(dsmStats => { + let statsPointsReceived = 0 + // we should have 2 dsm stats points + dsmStats.forEach((timeStatsBucket) => { + if (timeStatsBucket && timeStatsBucket.Stats) { + timeStatsBucket.Stats.forEach((statsBuckets) => { + statsPointsReceived += statsBuckets.Stats.length + }) + } + }) + assert.ok(statsPointsReceived >= 2) + assert.strictEqual(agent.dsmStatsExist(agent, expectedConsumerHash.readBigUInt64BE(0).toString()), true) + }, { timeoutMs: TIMEOUT }) + }) + }) + }) + + describe('it should set a message payload size', () => { + let recordCheckpointSpy + + beforeEach(() => { + recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') + }) + + afterEach(() => { + DataStreamsProcessor.prototype.recordCheckpoint.restore() + }) + + it('when producing a message', async () => { + await publish(dsmTopic, { data: Buffer.from('DSM produce payload size') }) + assert.ok(recordCheckpointSpy.args[0][0].hasOwnProperty('payloadSize')) + }) + + it('when consuming a message', async () => { + await publish(dsmTopic, { data: Buffer.from('DSM consume payload size') }) + + await consume(async () => { + assert.ok(recordCheckpointSpy.args[0][0].hasOwnProperty('payloadSize')) + }) + }) + }) + }) + }) + }) +}) + +function getProjectId () { + return `test-project-dsm-${id()}` +} + +function publish (topic, options) { + if (topic.publishMessage) { + return topic.publishMessage(options) + } else { + return topic.publish(options.data) + } +} diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js b/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js index 47d064c5b91..ea704165bb8 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js +++ b/packages/datadog-plugin-google-cloud-pubsub/test/index.spec.js @@ -1,18 +1,22 @@ 'use strict' const assert = require('node:assert/strict') +const sinon = require('sinon') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') -const sinon = require('sinon') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') -const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') -const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') const id = require('../../dd-trace/src/id') +const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const { expectSomeSpan, withDefaults } = require('../../dd-trace/test/plugins/helpers') -const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') + +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { DataStreamsProcessor, ENTRY_PARENT_HASH } = require('../../dd-trace/src/datastreams/processor') const { expectedSchema, rawExpectedSchema } = require('./naming') +const gc = global.gc ?? (() => {}) + +// The roundtrip to the pubsub emulator takes time. Sometimes a *long* time. const TIMEOUT = 30000 const dsmTopicName = 'dsm-topic' @@ -25,10 +29,13 @@ describe('Plugin', () => { before(() => { process.env.PUBSUB_EMULATOR_HOST = 'localhost:8081' process.env.DD_DATA_STREAMS_ENABLED = 'true' + process.env.K_SERVICE = 'test-service' }) after(() => { delete process.env.PUBSUB_EMULATOR_HOST + delete process.env.DD_DATA_STREAMS_ENABLED + // Don't delete K_SERVICE - pubsub-push-subscription.spec.js needs it }) afterEach(() => { @@ -41,12 +48,10 @@ describe('Plugin', () => { let resource let v1 let gax - let expectedProducerHash - let expectedConsumerHash describe('without configuration', () => { beforeEach(() => { - return agent.load('google-cloud-pubsub', { dsmEnabled: false }) + return agent.load('google-cloud-pubsub', { dsmEnabled: false }, { flushMinSpans: 1 }) }) beforeEach(() => { @@ -115,10 +120,18 @@ describe('Plugin', () => { component: 'google-cloud-pubsub' } }) - const publisher = new v1.PublisherClient({ projectId: project }) + const publisher = new v1.PublisherClient({ + projectId: project, + grpc: gax.grpc, + servicePath: 'localhost', + port: 8081, + sslCreds: gax.grpc.credentials.createInsecure() + }, gax) const name = `projects/${project}/topics/${topicName}` try { + // This should fail because the topic already exists or similar error await publisher.createTopic({ name }) + await publisher.createTopic({ name }) // Try to create twice to force error } catch (e) { // this is just to prevent mocha from crashing } @@ -197,24 +210,24 @@ describe('Plugin', () => { const expectedSpanPromise = expectSpanWithDefaults({ name: expectedSchema.receive.opName, service: expectedSchema.receive.serviceName, - meta: { 'span.kind': 'consumer' } + type: 'worker', + meta: { + component: 'google-cloud-pubsub', + 'span.kind': 'consumer', + 'pubsub.topic': resource + } }) const [topic] = await pubsub.createTopic(topicName) const [sub] = await topic.createSubscription('foo') - const rxPromise = new Promise((resolve, reject) => { - sub.on('message', msg => { - const receiverSpanContext = tracer.scope().active()._spanContext - try { - assert.ok(typeof receiverSpanContext._parentId === 'object' && receiverSpanContext._parentId !== null) - resolve() - msg.ack() - } catch (e) { - reject(e) - } - }) + sub.on('message', msg => { + const activeSpan = tracer.scope().active() + if (activeSpan) { + const receiverSpanContext = activeSpan._spanContext + assert.ok(typeof receiverSpanContext._parentId === 'object' && receiverSpanContext._parentId !== null) + } + msg.ack() }) await publish(topic, { data: Buffer.from('hello') }) - await rxPromise return expectedSpanPromise }) @@ -223,12 +236,15 @@ describe('Plugin', () => { const expectedSpanPromise = expectSpanWithDefaults({ name: expectedSchema.receive.opName, service: expectedSchema.receive.serviceName, + type: 'worker', error: 1, meta: { [ERROR_MESSAGE]: error.message, [ERROR_TYPE]: error.name, [ERROR_STACK]: error.stack, - component: 'google-cloud-pubsub' + component: 'google-cloud-pubsub', + 'span.kind': 'consumer', + 'pubsub.topic': resource } }) const [topic] = await pubsub.createTopic(topicName) @@ -259,13 +275,25 @@ describe('Plugin', () => { }) withNamingSchema( - async () => { + async (config) => { const [topic] = await pubsub.createTopic(topicName) const [sub] = await topic.createSubscription('foo') sub.on('message', msg => msg.ack()) await publish(topic, { data: Buffer.from('hello') }) }, - rawExpectedSchema.receive + rawExpectedSchema.receive, + { + selectSpan: (traces) => { + for (const trace of traces) { + for (const span of trace) { + if (span.type === 'worker') { + return span + } + } + } + return undefined + } + } ) }) @@ -336,6 +364,8 @@ describe('Plugin', () => { let dsmTopic let sub let consume + let expectedProducerHash + let expectedConsumerHash beforeEach(() => { return agent.load('google-cloud-pubsub', { @@ -398,7 +428,6 @@ describe('Plugin', () => { await consume(async () => { agent.expectPipelineStats(dsmStats => { let statsPointsReceived = 0 - // we should have 2 dsm stats points dsmStats.forEach((timeStatsBucket) => { if (timeStatsBucket && timeStatsBucket.Stats) { timeStatsBucket.Stats.forEach((statsBuckets) => { @@ -437,13 +466,149 @@ describe('Plugin', () => { }) }) }) + + describe('garbage collection and memory leaks', function () { + // GC tests need --expose-gc flag + if (typeof global.gc !== 'function') { + return it.skip('requires --expose-gc flag') + } + + it('should clean up WeakMap entries when messages are garbage collected', async function () { + this.timeout(10000) + + // Create a weak reference to track if message is GC'd + let messageWasCollected = false + const finalizationRegistry = new FinalizationRegistry(() => { + messageWasCollected = true + }) + + // Use unique topic name for GC test + const gcTopicName = `gc-test-${Date.now()}` + + // Publish and consume without acknowledging + await (async () => { + const [topic] = await pubsub.createTopic(gcTopicName) + const [subscription] = await topic.createSubscription('gc-test-sub') + + let messageReceived = false + subscription.on('message', (message) => { + // Register the message for GC tracking + finalizationRegistry.register(message, 'test-message') + messageReceived = true + // DON'T call message.ack() - this tests the GC cleanup path + }) + + await publish(topic, { data: Buffer.from('gc test message') }) + + // Wait for message to be received + await new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (messageReceived) { + clearInterval(checkInterval) + resolve() + } + }, 100) + }) + + // Close subscription to release references + subscription.close() + })() + + // Force garbage collection multiple times + gc() + await new Promise(resolve => setTimeout(resolve, 100)) + gc() + await new Promise(resolve => setTimeout(resolve, 100)) + gc() + + // Wait a bit for FinalizationRegistry callback + await new Promise(resolve => setTimeout(resolve, 500)) + + // Verify the message was garbage collected + // This proves WeakMap doesn't prevent GC + assert.ok(messageWasCollected, 'Message should be garbage collected even without ack()') + }) + + it('should not leak memory with many messages without ack', async function () { + this.timeout(20000) // Increase timeout for older Pub/Sub versions + + const initialMemory = process.memoryUsage().heapUsed + + // Use unique topic name for leak test + const leakTopicName = `leak-test-${Date.now()}` + const [topic] = await pubsub.createTopic(leakTopicName) + const [subscription] = await topic.createSubscription('leak-test-sub') + + let messagesReceived = 0 + const targetMessages = 50 // Reduce from 100 to 50 for faster test + + subscription.on('message', (message) => { + messagesReceived++ + // DON'T acknowledge - test that WeakMap doesn't leak + }) + + // Send many messages + for (let i = 0; i < targetMessages; i++) { + await publish(topic, { data: Buffer.from(`leak test ${i}`) }) + } + + // Wait for all messages + await new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (messagesReceived >= targetMessages) { + clearInterval(checkInterval) + resolve() + } + }, 100) + }) + + subscription.close() + + // Force GC + gc() + await new Promise(resolve => setTimeout(resolve, 100)) + gc() + + const afterMemory = process.memoryUsage().heapUsed + const memoryIncrease = afterMemory - initialMemory + + // Memory should not increase significantly (less than 10MB for 50 messages) + // If WeakMap is leaking, this would be much higher + assert.ok( + memoryIncrease < 10 * 1024 * 1024, + `Memory increase should be minimal but was ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB` + ) + }) + }) }) function expectSpanWithDefaults (expected) { - const prefixedResource = [expected.meta['pubsub.method'], resource].filter(x => x).join(' ') - const service = expected.meta['pubsub.method'] ? 'test-pubsub' : 'test' + let prefixedResource + const method = expected.meta?.['pubsub.method'] + const spanKind = expected.meta?.['span.kind'] + + if (method === 'publish') { + prefixedResource = `${method} to Topic ${topicName}` + } else if (spanKind === 'consumer') { + prefixedResource = `Message from ${topicName}` + } else if (method) { + prefixedResource = `${method} ${resource}` + } else { + prefixedResource = resource + } + + let defaultOpName = 'pubsub.receive' + if (spanKind === 'consumer') { + defaultOpName = expectedSchema.receive.opName + } else if (spanKind === 'producer') { + defaultOpName = expectedSchema.send.opName + } else { + defaultOpName = expectedSchema.controlPlane.opName + } + + const service = method ? 'test-pubsub' : 'test' expected = withDefaults({ - name: 'pubsub.request', + name: defaultOpName, resource: prefixedResource, service, error: 0, @@ -452,7 +617,8 @@ describe('Plugin', () => { 'gcloud.project_id': project } }, expected) - return expectSomeSpan(agent, expected, { timeoutMs: TIMEOUT }) + + return expectSomeSpan(agent, expected, TIMEOUT) } }) }) diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/client.spec.js b/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/client.spec.js index e14e690f1a9..4736f66d998 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,8 +34,9 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'pubsub.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, undefined, - { PUBSUB_EMULATOR_HOST: 'localhost:8081' }) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, { + PUBSUB_EMULATOR_HOST: 'localhost:8081' + }) await res }).timeout(20000) diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/naming.js b/packages/datadog-plugin-google-cloud-pubsub/test/naming.js index b03e300f346..d10f801b8d1 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/test/naming.js +++ b/packages/datadog-plugin-google-cloud-pubsub/test/naming.js @@ -16,11 +16,11 @@ const rawExpectedSchema = { receive: { v0: { opName: 'pubsub.receive', - serviceName: 'test' + serviceName: 'test-pubsub' }, v1: { opName: 'gcp.pubsub.process', - serviceName: 'test' + serviceName: 'test-pubsub' } }, controlPlane: { diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/pubsub-push-subscription.spec.js b/packages/datadog-plugin-google-cloud-pubsub/test/pubsub-push-subscription.spec.js new file mode 100644 index 00000000000..a180f0c8d76 --- /dev/null +++ b/packages/datadog-plugin-google-cloud-pubsub/test/pubsub-push-subscription.spec.js @@ -0,0 +1,335 @@ +'use strict' + +// Set K_SERVICE before any modules load to enable push subscription plugin +process.env.K_SERVICE = 'test-service' + +const assert = require('node:assert/strict') +const { setTimeout: wait } = require('node:timers/promises') + +const axios = require('axios') +const { describe, it, beforeEach, afterEach, before, after } = require('mocha') +const agent = require('../../dd-trace/test/plugins/agent') +const { assertObjectContains } = require('../../../integration-tests/helpers') +const gc = global.gc ?? (() => {}) + +describe('Push Subscription Plugin', () => { + let appListener + let http + + before(() => { + return agent.load(['http'], { client: false }) + }) + + after(() => { + return agent.close({ ritmReset: false }) + }) + + beforeEach(() => { + http = require('http') + }) + + afterEach(() => { + if (appListener) { + appListener.close() + appListener = null + } + }) + + // Helper to create a simple HTTP server that responds OK to /push-endpoint + function createServer () { + return http.createServer((req, res) => { + if (req.method === 'POST' && req.url.startsWith('/push-endpoint')) { + req.on('data', () => {}) + req.on('end', () => { + res.writeHead(200) + res.end('OK') + }) + } else { + res.writeHead(404) + res.end() + } + }) + } + + // Helper to send a push subscription request + async function sendPushRequest (port, headers = {}) { + const defaultHeaders = { + 'Content-Type': 'application/json', + 'User-Agent': 'APIs-Google; (+https://developers.google.com/webmasters/APIs-Google.html)', + 'x-goog-pubsub-message-id': 'test-message-id', + 'x-goog-pubsub-subscription-name': 'projects/test-project/subscriptions/test-sub', + 'x-goog-pubsub-publish-time': new Date().toISOString() + } + + return axios.post(`http://localhost:${port}/push-endpoint`, { + message: { data: 'dGVzdA==', messageId: 'test-message-id' } + }, { + headers: { ...defaultHeaders, ...headers } + }) + } + + // Helper to find pubsub.push.receive span + function findPubSubSpan (traces) { + const trace = traces.find(t => t.some(s => s.name === 'pubsub.push.receive')) + if (!trace) throw new Error('Could not find trace with pubsub.push.receive span') + return trace.find(s => s.name === 'pubsub.push.receive') + } + + describe('Push subscription with raw HTTP server', () => { + it('should create pubsub.push.receive span with delivery duration', (done) => { + const messageId = 'http-test-789' + const publishStartTime = Date.now().toString() + + appListener = createServer().listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .assertSomeTraces(traces => { + const pubsubSpan = findPubSubSpan(traces) + + assertObjectContains(pubsubSpan.meta, { + 'span.kind': 'consumer', + component: 'google-cloud-pubsub', + 'pubsub.message_id': messageId, + 'pubsub.subscription_type': 'push' + }) + + // Verify delivery_duration_ms + assert.ok(pubsubSpan.metrics['pubsub.delivery_duration_ms'] !== undefined) + assert.ok(typeof pubsubSpan.metrics['pubsub.delivery_duration_ms'] === 'number') + assert.ok(pubsubSpan.metrics['pubsub.delivery_duration_ms'] >= 0) + }) + .then(done) + .catch(done) + + sendPushRequest(port, { + 'x-goog-pubsub-message-id': messageId, + 'x-dd-publish-start-time': publishStartTime, + 'pubsub.topic': 'projects/test-project/topics/test-topic' + }).catch(done) + }) + }) + + it('should propagate distributed trace context from producer to push receive', (done) => { + const producerTraceId = '1234567890abcdef' + const producerSpanId = 'fedcba0987654321' + + appListener = createServer().listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .assertSomeTraces(traces => { + const pubsubSpan = findPubSubSpan(traces) + + if (pubsubSpan.meta['_dd.span_links']) { + const spanLinks = JSON.parse(pubsubSpan.meta['_dd.span_links']) + assert.ok(Array.isArray(spanLinks)) + const hasProducerLink = spanLinks.some(link => link.trace_id && link.span_id) + assert.strictEqual(hasProducerLink, true) + } + }) + .then(done) + .catch(done) + + sendPushRequest(port, { + 'x-datadog-trace-id': producerTraceId, + 'x-datadog-parent-id': producerSpanId, + 'x-datadog-sampling-priority': '1' + }).catch(done) + }) + }) + + it('should add batch metadata to receive span', (done) => { + const batchTraceId = 'abc123def456' + const batchSpanId = '789012345678' + + appListener = createServer().listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .assertSomeTraces(traces => { + const pubsubSpan = findPubSubSpan(traces) + + assertObjectContains(pubsubSpan.meta, { + 'pubsub.batch.description': 'Message 1 of 3', + 'pubsub.batch.request_trace_id': batchTraceId + }) + assertObjectContains(pubsubSpan.metrics, { + 'pubsub.batch.message_count': 3, + 'pubsub.batch.message_index': 0 + }) + }) + .then(done) + .catch(done) + + sendPushRequest(port, { + '_dd.batch.size': '3', + '_dd.batch.index': '0', + '_dd.pubsub_request.trace_id': batchTraceId, + '_dd.pubsub_request.span_id': batchSpanId + }).catch(done) + }) + }) + + it('should set service name with -pubsub suffix', (done) => { + appListener = createServer().listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .assertSomeTraces(traces => { + const pubsubSpan = findPubSubSpan(traces) + + assert.strictEqual(pubsubSpan.service, 'test-pubsub') + assertObjectContains(pubsubSpan.meta, { + '_dd.base_service': 'test', + '_dd.serviceoverride.type': 'integration' + }) + }) + .then(done) + .catch(done) + + sendPushRequest(port).catch(done) + }) + }) + + it('should NOT create pubsub span for non-push-subscription requests', (done) => { + appListener = createServer().listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .assertSomeTraces(traces => { + const trace = traces.find(t => t.some(s => s.name === 'web.request')) + if (!trace) return + + const pubsubSpan = trace.find(s => s.name === 'pubsub.push.receive') + assert.ok(!pubsubSpan, 'pubsub.push.receive span should NOT exist') + }) + .then(done) + .catch(done) + + axios.post(`http://localhost:${port}/push-endpoint`, { data: 'regular' }, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0' // Not Google user agent + } + }).catch(done) + }) + }) + + it('should NOT create pubsub span when missing required headers', (done) => { + appListener = createServer().listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .assertSomeTraces(traces => { + const trace = traces.find(t => t.some(s => s.name === 'web.request')) + if (!trace) return + + const pubsubSpan = trace.find(s => s.name === 'pubsub.push.receive') + assert.ok(!pubsubSpan, 'pubsub.push.receive span should NOT exist') + }) + .then(done) + .catch(done) + + axios.post(`http://localhost:${port}/push-endpoint`, { message: { data: 'dGVzdA==' } }, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'APIs-Google; (+https://developers.google.com/webmasters/APIs-Google.html)' + // Missing x-goog-pubsub-message-id + } + }).catch(done) + }) + }) + + describe('garbage collection and memory leaks', function () { + if (typeof global.gc !== 'function') { + return it.skip('requires --expose-gc flag') + } + + it('should clean up receiveSpans WeakMap when request is garbage collected', function (done) { + this.timeout(10000) + + let requestWasCollected = false + const finalizationRegistry = new FinalizationRegistry(() => { + requestWasCollected = true + }) + + const server = http.createServer((req, res) => { + if (req.method === 'POST') { + finalizationRegistry.register(req, 'test-request') + req.on('data', () => {}) + req.on('end', () => { + res.writeHead(200) + res.end('OK') + }) + } + }) + + appListener = server.listen(0, 'localhost', async () => { + const port = server.address().port + + try { + await sendPushRequest(port) + await wait(100) + + // Force garbage collection multiple times + gc() + await wait(100) + gc() + await wait(100) + gc() + await wait(500) + + assert.strictEqual(requestWasCollected, true) + done() + } catch (err) { + done(err) + } + }) + }) + + it('should not leak memory with many push requests', function (done) { + this.timeout(15000) + + appListener = createServer().listen(0, 'localhost', async () => { + const port = appListener.address().port + + try { + const initialMemory = process.memoryUsage().heapUsed + + // Send 100 requests + const promises = [] + for (let i = 0; i < 100; i++) { + promises.push( + sendPushRequest(port, { + 'x-goog-pubsub-message-id': `msg-${i}` + }).catch(() => {}) + ) + } + + await Promise.all(promises) + await wait(500) + + // Force GC + gc() + await wait(100) + gc() + + const afterMemory = process.memoryUsage().heapUsed + const memoryIncrease = afterMemory - initialMemory + + // Memory should not increase significantly (less than 10MB for 100 requests) + assert.ok( + memoryIncrease < (10 * 1024 * 1024), + `Memory increase should be minimal but was ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB` + ) + + done() + } catch (err) { + done(err) + } + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-google-cloud-vertexai/src/index.js b/packages/datadog-plugin-google-cloud-vertexai/src/index.js index ca91aa3e0eb..6b859a0ea88 100644 --- a/packages/datadog-plugin-google-cloud-vertexai/src/index.js +++ b/packages/datadog-plugin-google-cloud-vertexai/src/index.js @@ -1,8 +1,8 @@ 'use strict' const CompositePlugin = require('../../dd-trace/src/plugins/composite') -const GoogleVertexAITracingPlugin = require('./tracing') const VertexAILLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/vertexai') +const GoogleVertexAITracingPlugin = require('./tracing') class GoogleCloudVertexAIPlugin extends CompositePlugin { static id = 'google-cloud-vertexai' diff --git a/packages/datadog-plugin-google-cloud-vertexai/test/integration-test/client.spec.js b/packages/datadog-plugin-google-cloud-vertexai/test/integration-test/client.spec.js index da6ff54d906..cb6eb976496 100644 --- a/packages/datadog-plugin-google-cloud-vertexai/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-google-cloud-vertexai/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -38,7 +38,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'vertexai.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-google-genai/src/index.js b/packages/datadog-plugin-google-genai/src/index.js index 9d6a957098f..1cd17e42dac 100644 --- a/packages/datadog-plugin-google-genai/src/index.js +++ b/packages/datadog-plugin-google-genai/src/index.js @@ -1,8 +1,8 @@ 'use strict' const CompositePlugin = require('../../dd-trace/src/plugins/composite') -const GenAiTracingPlugin = require('./tracing') const GenAiLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/genai') +const GenAiTracingPlugin = require('./tracing') class GenAiPlugin extends CompositePlugin { static id = 'google-genai' diff --git a/packages/datadog-plugin-google-genai/test/index.spec.js b/packages/datadog-plugin-google-genai/test/index.spec.js index 8e1c0433d15..98c2922acce 100644 --- a/packages/datadog-plugin-google-genai/test/index.spec.js +++ b/packages/datadog-plugin-google-genai/test/index.spec.js @@ -1,9 +1,9 @@ 'use strict' +const assert = require('node:assert') const { describe, before, after, it } = require('mocha') const { withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') -const assert = require('node:assert') describe('Plugin', () => { withVersions('google-genai', '@google/genai', (version) => { diff --git a/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js b/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js index 9a7764ad55c..dac4cd476dd 100644 --- a/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js @@ -1,15 +1,15 @@ 'use strict' +const assert = require('node:assert/strict') +const { describe, it, beforeEach, afterEach } = require('mocha') const { FakeAgent, sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const { assert } = require('chai') -const { describe, it, beforeEach, afterEach } = require('mocha') describe('esm', () => { let agent @@ -33,12 +33,12 @@ describe('esm', () => { it('is instrumented', async () => { const res = agent.assertMessageReceived(({ headers, payload }) => { - assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) - assert.isArray(payload) + assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`) + assert.ok(Array.isArray(payload)) assert.strictEqual(checkSpansForServiceName(payload, 'google_genai.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, null, { + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, { NODE_OPTIONS: '--import dd-trace/initialize.mjs', GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || '' }) diff --git a/packages/datadog-plugin-graphql/src/resolve.js b/packages/datadog-plugin-graphql/src/resolve.js index 67f07a88658..98fe71d0587 100644 --- a/packages/datadog-plugin-graphql/src/resolve.js +++ b/packages/datadog-plugin-graphql/src/resolve.js @@ -1,7 +1,7 @@ 'use strict' -const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const dc = require('dc-polyfill') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const collapsedPathSym = Symbol('collapsedPaths') diff --git a/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-server.mjs b/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-server.mjs index 07e16cd2327..de09fc5f825 100644 --- a/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-server.mjs +++ b/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import graphql from 'graphql' import { createServer } from 'node:http' +import graphql from 'graphql' const schema = new graphql.GraphQLSchema({ query: new graphql.GraphQLObjectType({ diff --git a/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-yoga-server.mjs b/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-yoga-server.mjs index 24493c359c4..269a6cbea18 100644 --- a/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-yoga-server.mjs +++ b/packages/datadog-plugin-graphql/test/esm-test/esm-graphql-yoga-server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import { createSchema, createYoga } from 'graphql-yoga' import { createServer } from 'node:http' +import { createSchema, createYoga } from 'graphql-yoga' const typeDefs = /* GraphQL */ ` type Query { diff --git a/packages/datadog-plugin-graphql/test/esm-test/esm.spec.js b/packages/datadog-plugin-graphql/test/esm-test/esm.spec.js index dc3dafcc8de..b7bf2815fec 100644 --- a/packages/datadog-plugin-graphql/test/esm-test/esm.spec.js +++ b/packages/datadog-plugin-graphql/test/esm-test/esm.spec.js @@ -2,6 +2,8 @@ const assert = require('node:assert/strict') +const axios = require('axios') +const semver = require('semver') const { FakeAgent, sandboxCwd, @@ -10,8 +12,6 @@ const { spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const axios = require('axios') -const semver = require('semver') describe('Plugin (ESM)', () => { describe('graphql (ESM)', () => { @@ -42,7 +42,6 @@ describe('Plugin (ESM)', () => { sandboxCwd(), 'esm-graphql-server.mjs', agent.port, - undefined, { NODE_OPTIONS: '--no-warnings --loader=dd-trace/loader-hook.mjs' } ) @@ -81,7 +80,6 @@ describe('Plugin (ESM)', () => { sandboxCwd(), 'esm-graphql-yoga-server.mjs', agent.port, - undefined, { NODE_OPTIONS: '--no-warnings --loader=dd-trace/loader-hook.mjs' } ) diff --git a/packages/datadog-plugin-graphql/test/integration-test/client.spec.js b/packages/datadog-plugin-graphql/test/integration-test/client.spec.js index 9a6ffcabf18..43f6b592ede 100644 --- a/packages/datadog-plugin-graphql/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-graphql/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'graphql.parse'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(50000) diff --git a/packages/datadog-plugin-grpc/src/index.js b/packages/datadog-plugin-grpc/src/index.js index 9df196b1b6a..b68b70430fa 100644 --- a/packages/datadog-plugin-grpc/src/index.js +++ b/packages/datadog-plugin-grpc/src/index.js @@ -1,8 +1,8 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const GrpcServerPlugin = require('./server') const GrpcClientPlugin = require('./client') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class GrpcPlugin extends CompositePlugin { static id = 'grpc' diff --git a/packages/datadog-plugin-grpc/test/client.spec.js b/packages/datadog-plugin-grpc/test/client.spec.js index dc57c4b4df6..1e072009dd0 100644 --- a/packages/datadog-plugin-grpc/test/client.spec.js +++ b/packages/datadog-plugin-grpc/test/client.spec.js @@ -2,19 +2,18 @@ const assert = require('node:assert/strict') const path = require('node:path') +const Readable = require('node:stream').Readable const { after, afterEach, before, describe, it } = require('mocha') const semver = require('semver') const { assertObjectContains } = require('../../../integration-tests/helpers') -const Readable = require('node:stream').Readable - -const getService = require('./service') const loader = require('../../../versions/@grpc/proto-loader').get() const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, GRPC_CLIENT_ERROR_STATUSES } = require('../../dd-trace/src/constants') const { NODE_MAJOR } = require('../../../version') +const getService = require('./service') const pkgs = NODE_MAJOR > 14 ? ['@grpc/grpc-js'] : ['grpc', '@grpc/grpc-js'] @@ -135,16 +134,15 @@ describe('Plugin', () => { }) client.getUnary({ first: 'foobar' }, () => {}) - return agent - .assertSomeTraces(traces => { - assertObjectContains(traces[0][0].meta, { - 'network.destination.ip': '127.0.0.1', - 'network.destination.port': port.toString(), - 'rpc.service': 'test.TestService', - 'span.kind': 'client', - component: 'grpc' - }) - }) + return agent.assertFirstTraceSpan({ + meta: { + 'network.destination.ip': '127.0.0.1', + 'network.destination.port': port.toString(), + 'rpc.service': 'test.TestService', + 'span.kind': 'client', + component: 'grpc' + } + }) }) } @@ -336,19 +334,22 @@ describe('Plugin', () => { return agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].error, 1) - assertObjectContains(traces[0][0].meta, { - [ERROR_MESSAGE]: '2 UNKNOWN: foobar', - [ERROR_TYPE]: 'Error', - 'grpc.method.name': 'getUnary', - 'grpc.method.service': 'TestService', - 'grpc.method.package': 'test', - 'grpc.method.path': '/test.TestService/getUnary', - 'grpc.method.kind': 'unary', - 'rpc.service': 'test.TestService', - 'span.kind': 'client', - component: 'grpc' + assertObjectContains(traces[0][0], { + error: 1, + meta: { + [ERROR_MESSAGE]: '2 UNKNOWN: foobar', + [ERROR_TYPE]: 'Error', + 'grpc.method.name': 'getUnary', + 'grpc.method.service': 'TestService', + 'grpc.method.package': 'test', + 'grpc.method.path': '/test.TestService/getUnary', + 'grpc.method.kind': 'unary', + 'rpc.service': 'test.TestService', + 'span.kind': 'client', + component: 'grpc' + } }) + assert.ok(Object.hasOwn(traces[0][0].meta, ERROR_STACK)) assert.strictEqual(traces[0][0].metrics['grpc.status.code'], 2) }) @@ -382,18 +383,21 @@ describe('Plugin', () => { return agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].error, 1) - assertObjectContains(traces[0][0].meta, { - [ERROR_TYPE]: 'Error', - 'grpc.method.name': 'getUnary', - 'grpc.method.service': 'TestService', - 'grpc.method.package': 'test', - 'grpc.method.path': '/test.TestService/getUnary', - 'grpc.method.kind': 'unary', - 'rpc.service': 'test.TestService', - 'span.kind': 'client', - component: 'grpc' + assertObjectContains(traces[0][0], { + error: 1, + meta: { + [ERROR_TYPE]: 'Error', + 'grpc.method.name': 'getUnary', + 'grpc.method.service': 'TestService', + 'grpc.method.package': 'test', + 'grpc.method.path': '/test.TestService/getUnary', + 'grpc.method.kind': 'unary', + 'rpc.service': 'test.TestService', + 'span.kind': 'client', + component: 'grpc' + } }) + assert.ok(Object.hasOwn(traces[0][0].meta, ERROR_STACK)) assert.match(traces[0][0].meta[ERROR_MESSAGE], /^13 INTERNAL:.+$/m) assert.strictEqual(traces[0][0].metrics['grpc.status.code'], 13) @@ -560,12 +564,9 @@ describe('Plugin', () => { client.getUnary({ first: 'foobar' }, () => {}) - return agent - .assertSomeTraces(traces => { - assertObjectContains(traces[0][0], { - service: 'custom' - }) - }) + return agent.assertFirstTraceSpan({ + service: 'custom' + }) }) }) @@ -656,18 +657,17 @@ describe('Plugin', () => { client.getUnary({ first: 'foobar' }, metadata, () => {}) - return agent - .assertSomeTraces(traces => { - assertObjectContains(traces[0][0].meta, { - 'grpc.method.name': 'getUnary', - 'grpc.method.service': 'TestService', - 'grpc.method.path': '/test.TestService/getUnary', - 'grpc.method.kind': 'unary', - 'grpc.request.metadata.foo': 'bar', - 'rpc.service': 'test.TestService', - 'span.kind': 'client' - }) - }) + return agent.assertFirstTraceSpan({ + meta: { + 'grpc.method.name': 'getUnary', + 'grpc.method.service': 'TestService', + 'grpc.method.path': '/test.TestService/getUnary', + 'grpc.method.kind': 'unary', + 'grpc.request.metadata.foo': 'bar', + 'rpc.service': 'test.TestService', + 'span.kind': 'client' + } + }) }) it('should handle response metadata', async () => { diff --git a/packages/datadog-plugin-grpc/test/integration-test/server.mjs b/packages/datadog-plugin-grpc/test/integration-test/server.mjs index a1bda1d2acf..947d4726b20 100644 --- a/packages/datadog-plugin-grpc/test/integration-test/server.mjs +++ b/packages/datadog-plugin-grpc/test/integration-test/server.mjs @@ -1,7 +1,7 @@ import 'dd-trace/init.js' +import path from 'path' import * as grpc from '@grpc/grpc-js' import * as protoLoader from '@grpc/proto-loader' -import path from 'path' const currentDirectoryPath = path.dirname(new URL(import.meta.url).pathname) const parentDirectoryPath = path.resolve(currentDirectoryPath, '..') diff --git a/packages/datadog-plugin-grpc/test/server.spec.js b/packages/datadog-plugin-grpc/test/server.spec.js index 49f8bf6fb70..890cc34270d 100644 --- a/packages/datadog-plugin-grpc/test/server.spec.js +++ b/packages/datadog-plugin-grpc/test/server.spec.js @@ -1,19 +1,17 @@ 'use strict' const assert = require('node:assert/strict') +const Readable = require('node:stream').Readable +const path = require('node:path') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const { assertObjectContains } = require('../../../integration-tests/helpers') -const Readable = require('node:stream').Readable -const path = require('node:path') - const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') - const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, GRPC_SERVER_ERROR_STATUSES } = require('../../dd-trace/src/constants') - const { NODE_MAJOR } = require('../../../version') + const pkgs = NODE_MAJOR > 14 ? ['@grpc/grpc-js'] : ['grpc', '@grpc/grpc-js'] describe('Plugin', () => { @@ -420,12 +418,9 @@ describe('Plugin', () => { client.getUnary({ first: 'foobar' }, () => {}) - return agent - .assertSomeTraces(traces => { - assertObjectContains(traces[0][0], { - service: 'custom' - }) - }) + return agent.assertFirstTraceSpan({ + service: 'custom' + }) }) }) diff --git a/packages/datadog-plugin-handlebars/test/integration-test/client.spec.js b/packages/datadog-plugin-handlebars/test/integration-test/client.spec.js index 5b4ffc45084..844526affd6 100644 --- a/packages/datadog-plugin-handlebars/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-handlebars/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('handlebars', 'handlebars', '>=4.0.0', version => { diff --git a/packages/datadog-plugin-hono/test/integration-test/client.spec.js b/packages/datadog-plugin-hono/test/integration-test/client.spec.js index c30d733870e..1431d72bd42 100644 --- a/packages/datadog-plugin-hono/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-hono/test/integration-test/client.spec.js @@ -28,9 +28,7 @@ describe('esm integration test', () => { }) it('is instrumented', async () => { - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, undefined, { - VERSION: version - }) + proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, { VERSION: version }) proc.url += '/hello' return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { @@ -40,9 +38,7 @@ describe('esm integration test', () => { }).timeout(50000) it('receives missing route trace', async () => { - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, undefined, { - VERSION: version - }) + proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, { VERSION: version }) proc.url += '/missing' return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { diff --git a/packages/datadog-plugin-hono/test/integration-test/server.mjs b/packages/datadog-plugin-hono/test/integration-test/server.mjs index b8ec620436e..80ebcb49e40 100644 --- a/packages/datadog-plugin-hono/test/integration-test/server.mjs +++ b/packages/datadog-plugin-hono/test/integration-test/server.mjs @@ -1,9 +1,8 @@ import 'dd-trace/init.js' +import process from 'node:process' import { Hono } from 'hono' import { serve } from '@hono/node-server' -import process from 'node:process' - const app = new Hono() const version = process.env.VERSION.split('.').map(Number) diff --git a/packages/datadog-plugin-http/src/client.js b/packages/datadog-plugin-http/src/client.js index 12e7b5020da..4e270df8a14 100644 --- a/packages/datadog-plugin-http/src/client.js +++ b/packages/datadog-plugin-http/src/client.js @@ -1,5 +1,7 @@ 'use strict' +const { URL } = require('url') + const ClientPlugin = require('../../dd-trace/src/plugins/client') const { storage } = require('../../datadog-core') const tags = require('../../../ext/tags') @@ -9,7 +11,6 @@ const HTTP_HEADERS = formats.HTTP_HEADERS const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') const log = require('../../dd-trace/src/log') const { CLIENT_PORT_KEY, COMPONENT, ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') -const { URL } = require('url') const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS diff --git a/packages/datadog-plugin-http/src/index.js b/packages/datadog-plugin-http/src/index.js index ba6c4fc41a9..8b4eb2b5840 100644 --- a/packages/datadog-plugin-http/src/index.js +++ b/packages/datadog-plugin-http/src/index.js @@ -1,16 +1,36 @@ 'use strict' -const HttpServerPlugin = require('./server') -const HttpClientPlugin = require('./client') +const PushSubscriptionPlugin = require('../../datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription') const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const { enableGCPPubSubPushSubscription } = require('../../dd-trace/src/serverless') +const log = require('../../dd-trace/src/log') +const HttpClientPlugin = require('./client') +const HttpServerPlugin = require('./server') +/** + * HTTP Plugin loads server/client plugins with optional GCP Pub/Sub Push support. + * Plugin order is critical: PushSubscriptionPlugin must load FIRST to intercept + * Pub/Sub push requests and activate delivery spans before HTTP spans are created. + */ class HttpPlugin extends CompositePlugin { static id = 'http' static get plugins () { - return { - server: HttpServerPlugin, - client: HttpClientPlugin + const plugins = {} + + // Load push subscription plugin first (if enabled) for GCP Cloud Run + if (enableGCPPubSubPushSubscription()) { + try { + plugins['pubsub-push-subscription'] = PushSubscriptionPlugin + log.debug('Loaded GCP Pub/Sub Push Subscription plugin for HTTP requests') + } catch (e) { + log.debug(`Failed to load GCP Pub/Sub Push Subscription plugin: ${e.message}`) + } } + + plugins.server = HttpServerPlugin + plugins.client = HttpClientPlugin + + return plugins } } diff --git a/packages/datadog-plugin-http/test/client.spec.js b/packages/datadog-plugin-http/test/client.spec.js index e4b3e8216a4..7936922c767 100644 --- a/packages/datadog-plugin-http/test/client.spec.js +++ b/packages/datadog-plugin-http/test/client.spec.js @@ -4,6 +4,7 @@ const fs = require('fs') const assert = require('node:assert/strict') const path = require('path') +const { satisfies } = require('semver') const tags = require('../../../ext/tags') const { storage } = require('../../datadog-core') const agent = require('../../dd-trace/test/plugins/agent') @@ -11,8 +12,8 @@ const { withNamingSchema, withPeerService } = require('../../dd-trace/test/setup const key = fs.readFileSync(path.join(__dirname, './ssl/test.key')) const cert = fs.readFileSync(path.join(__dirname, './ssl/test.crt')) const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { rawExpectedSchema } = require('./naming') -const { satisfies } = require('semver') const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS @@ -104,19 +105,20 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'GET') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'GET') - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta.component, 'http') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'http') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'GET', + meta: { + 'span.kind': 'client', + 'http.url': `${protocol}://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '200', + component: 'http', + '_dd.integration': 'http', + 'out.host': 'localhost' + } + }) .then(done) .catch(done) @@ -159,18 +161,19 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'CONNECT') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'CONNECT') - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta.component, 'http') - assert.strictEqual(traces[0][0].meta['out.host'], 'localhost') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'CONNECT', + meta: { + 'span.kind': 'client', + 'http.url': `${protocol}://localhost:${port}/user`, + 'http.method': 'CONNECT', + 'http.status_code': '200', + component: 'http', + 'out.host': 'localhost' + } + }) .then(done) .catch(done) @@ -203,17 +206,18 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, SERVICE_NAME) - assert.strictEqual(traces[0][0].type, 'http') - assert.strictEqual(traces[0][0].resource, 'GET') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - assert.strictEqual(traces[0][0].meta['http.method'], 'GET') - assert.strictEqual(traces[0][0].meta['http.status_code'], '101') - assert.strictEqual(traces[0][0].meta.component, 'http') - }) + agent.assertFirstTraceSpan({ + service: SERVICE_NAME, + type: 'http', + resource: 'GET', + meta: { + 'span.kind': 'client', + 'http.url': `${protocol}://localhost:${port}/user`, + 'http.method': 'GET', + 'http.status_code': '101', + component: 'http' + } + }) .then(done) .catch(done) @@ -276,11 +280,12 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200', + 'http.url': `${protocol}://localhost:${port}/user` + } + }) .then(done) .catch(done) @@ -302,11 +307,12 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200', + 'http.url': `${protocol}://localhost:${port}/user` + } + }) .then(done) .catch(done) @@ -325,11 +331,12 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200', + 'http.url': `${protocol}://localhost:${port}/user` + } + }) .then(done) .catch(done) @@ -353,9 +360,11 @@ describe('Plugin', () => { app.get('/user', (req, res) => res.status(200).send()) - agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200', + 'http.url': `${protocol}://localhost:${port}/user` + } }).then(done, done) const req = http.request(url) @@ -551,10 +560,15 @@ describe('Plugin', () => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message || error.code) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'http') + assertObjectContains(traces[0][0], { + error: 1, + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message || error.code, + [ERROR_STACK]: error.stack, + component: 'http' + } + }) }) .then(done) .catch(done) @@ -623,12 +637,17 @@ describe('Plugin', () => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].error, 1) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) + assertObjectContains(traces[0][0], { + error: 1, + meta: { + [ERROR_MESSAGE]: error.message, + [ERROR_TYPE]: error.name, + [ERROR_STACK]: error.stack, + component: 'http' + } + }) + assert.ok(!('http.status_code' in traces[0][0].meta)) - assert.strictEqual(traces[0][0].meta.component, 'http') }) .then(done) .catch(done) @@ -860,11 +879,12 @@ describe('Plugin', () => { }) appListener = server(app, port => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['http.status_code'], '200') - assert.strictEqual(traces[0][0].meta['http.url'], `${protocol}://localhost:${port}/user`) - }) + agent.assertFirstTraceSpan({ + meta: { + 'http.status_code': '200', + 'http.url': `${protocol}://localhost:${port}/user` + } + }) .then(done) .catch(done) @@ -890,9 +910,13 @@ describe('Plugin', () => { appListener = server(app, port => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][1].error, 0) - assert.strictEqual(traces[0][1].meta['http.status_code'], '200') - assert.strictEqual(traces[0][1].meta['http.url'], `${protocol}://localhost:${port}/user`) + assertObjectContains(traces[0][1], { + error: 0, + meta: { + 'http.status_code': '200', + 'http.url': `${protocol}://localhost:${port}/user` + } + }) }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-http/test/http_endpoint.spec.js b/packages/datadog-plugin-http/test/http_endpoint.spec.js index 93f746cbd9d..089829935ee 100644 --- a/packages/datadog-plugin-http/test/http_endpoint.spec.js +++ b/packages/datadog-plugin-http/test/http_endpoint.spec.js @@ -1,7 +1,8 @@ 'use strict' +const assert = require('node:assert/strict') const axios = require('axios') -const { expect } = require('chai') + const { describe, it, beforeEach, afterEach, before } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') @@ -56,10 +57,10 @@ describe('Plugin', () => { it('should set http.endpoint with int when no route is available', done => { agent .assertSomeTraces(traces => { - expect(traces[0][0]).to.have.property('name', 'web.request') - expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/users/123`) - expect(traces[0][0].meta).to.not.have.property('http.route') - expect(traces[0][0].meta).to.have.property('http.endpoint', '/users/{param:int}') + assert.strictEqual(traces[0][0].name, 'web.request') + assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/users/123`) + assert.ok(!('http.route' in traces[0][0].meta)) + assert.strictEqual(traces[0][0].meta['http.endpoint'], '/users/{param:int}') }) .then(done) .catch(done) @@ -70,9 +71,9 @@ describe('Plugin', () => { it('should set http.endpoint with int_id when no route is available', done => { agent .assertSomeTraces(traces => { - expect(traces[0][0]).to.have.property('name', 'web.request') - expect(traces[0][0].meta).to.not.have.property('http.route') - expect(traces[0][0].meta).to.have.property('http.endpoint', '/resources/{param:int_id}') + assert.strictEqual(traces[0][0].name, 'web.request') + assert.ok(!('http.route' in traces[0][0].meta)) + assert.strictEqual(traces[0][0].meta['http.endpoint'], '/resources/{param:int_id}') }) .then(done) .catch(done) @@ -83,10 +84,10 @@ describe('Plugin', () => { it('should set http.endpoint with hex when no route is available', done => { agent .assertSomeTraces(traces => { - expect(traces[0][0]).to.have.property('name', 'web.request') - expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/orders/abc123`) - expect(traces[0][0].meta).to.not.have.property('http.route') - expect(traces[0][0].meta).to.have.property('http.endpoint', '/orders/{param:hex}') + assert.strictEqual(traces[0][0].name, 'web.request') + assert.strictEqual(traces[0][0].meta['http.url'], `http://localhost:${port}/orders/abc123`) + assert.ok(!('http.route' in traces[0][0].meta)) + assert.strictEqual(traces[0][0].meta['http.endpoint'], '/orders/{param:hex}') }) .then(done) .catch(done) @@ -97,9 +98,9 @@ describe('Plugin', () => { it('should set http.endpoint with hex_id when no route is available', done => { agent .assertSomeTraces(traces => { - expect(traces[0][0]).to.have.property('name', 'web.request') - expect(traces[0][0].meta).to.not.have.property('http.route') - expect(traces[0][0].meta).to.have.property('http.endpoint', '/resources/{param:hex_id}') + assert.strictEqual(traces[0][0].name, 'web.request') + assert.ok(!('http.route' in traces[0][0].meta)) + assert.strictEqual(traces[0][0].meta['http.endpoint'], '/resources/{param:hex_id}') }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-http2/src/client.js b/packages/datadog-plugin-http2/src/client.js index cbc8bf9e1c7..3dff79b5497 100644 --- a/packages/datadog-plugin-http2/src/client.js +++ b/packages/datadog-plugin-http2/src/client.js @@ -1,9 +1,9 @@ 'use strict' +const URL = require('url').URL + const { storage } = require('../../datadog-core') const ClientPlugin = require('../../dd-trace/src/plugins/client') - -const URL = require('url').URL const log = require('../../dd-trace/src/log') const tags = require('../../../ext/tags') const kinds = require('../../../ext/kinds') diff --git a/packages/datadog-plugin-http2/src/index.js b/packages/datadog-plugin-http2/src/index.js index 683362ed008..ef10db64631 100644 --- a/packages/datadog-plugin-http2/src/index.js +++ b/packages/datadog-plugin-http2/src/index.js @@ -1,8 +1,8 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const Http2ServerPlugin = require('./server') const Http2ClientPlugin = require('./client') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class Http2Plugin extends CompositePlugin { static id = 'http2' diff --git a/packages/datadog-plugin-http2/test/integration-test/client.spec.js b/packages/datadog-plugin-http2/test/integration-test/client.spec.js index 0b41aef9303..6a8802185b7 100644 --- a/packages/datadog-plugin-http2/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-http2/test/integration-test/client.spec.js @@ -2,6 +2,7 @@ const assert = require('node:assert/strict') +const http2 = require('http2') const { FakeAgent, spawnPluginIntegrationTestProc, @@ -9,7 +10,6 @@ const { useSandbox, varySandbox } = require('../../../../integration-tests/helpers') -const http2 = require('http2') describe('esm', () => { let agent diff --git a/packages/datadog-plugin-ioredis/test/index.spec.js b/packages/datadog-plugin-ioredis/test/index.spec.js index 472a7d525cc..396d3f6c109 100644 --- a/packages/datadog-plugin-ioredis/test/index.spec.js +++ b/packages/datadog-plugin-ioredis/test/index.spec.js @@ -9,6 +9,7 @@ const agent = require('../../dd-trace/test/plugins/agent') const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers') const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') const { expectedSchema, rawExpectedSchema } = require('./naming') + describe('Plugin', () => { let Redis let redis diff --git a/packages/datadog-plugin-ioredis/test/integration-test/client.spec.js b/packages/datadog-plugin-ioredis/test/integration-test/client.spec.js index 22d61933548..ba88a615918 100644 --- a/packages/datadog-plugin-ioredis/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-ioredis/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -40,7 +40,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'redis.command'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-iovalkey/test/integration-test/client.spec.js b/packages/datadog-plugin-iovalkey/test/integration-test/client.spec.js index 2d3d690b634..32d64127003 100644 --- a/packages/datadog-plugin-iovalkey/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-iovalkey/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'valkey.command'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-kafkajs/src/index.js b/packages/datadog-plugin-kafkajs/src/index.js index 71d09698d33..4805bca49e4 100644 --- a/packages/datadog-plugin-kafkajs/src/index.js +++ b/packages/datadog-plugin-kafkajs/src/index.js @@ -1,9 +1,9 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const ProducerPlugin = require('./producer') const ConsumerPlugin = require('./consumer') const BatchConsumerPlugin = require('./batch-consumer') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class KafkajsPlugin extends CompositePlugin { static id = 'kafkajs' diff --git a/packages/datadog-plugin-kafkajs/test/dsm.spec.js b/packages/datadog-plugin-kafkajs/test/dsm.spec.js new file mode 100644 index 00000000000..f2bd23f95fb --- /dev/null +++ b/packages/datadog-plugin-kafkajs/test/dsm.spec.js @@ -0,0 +1,249 @@ +'use strict' + +const assert = require('node:assert/strict') +const { randomUUID } = require('crypto') +const { describe, it, beforeEach, afterEach } = require('mocha') +const semver = require('semver') +const sinon = require('sinon') + +const { withVersions } = require('../../dd-trace/test/setup/mocha') +const agent = require('../../dd-trace/test/plugins/agent') + +const DataStreamsContext = require('../../dd-trace/src/datastreams/context') +const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') +const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') +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}`) + } + edgeTags.sort() + return computePathwayHash('test', 'tester', edgeTags, parentHash) +} + +describe('Plugin', () => { + describe('kafkajs', function () { + this.timeout(10000) + + afterEach(() => { + return agent.close({ ritmReset: false }) + }) + + withVersions('kafkajs', 'kafkajs', (version) => { + let kafka + let admin + let tracer + let Kafka + let clusterIdAvailable + let expectedProducerHash + let expectedConsumerHash + let testTopic + + describe('data stream monitoring', () => { + const messages = [{ key: 'key1', value: 'test2' }] + + beforeEach(async () => { + process.env.DD_DATA_STREAMS_ENABLED = 'true' + tracer = require('../../dd-trace') + await agent.load('kafkajs') + const lib = require(`../../../versions/kafkajs@${version}`).get() + Kafka = lib.Kafka + kafka = new Kafka({ + clientId: `kafkajs-test-${version}`, + brokers: ['127.0.0.1:9092'], + logLevel: lib.logLevel.WARN + }) + testTopic = `test-topic-${randomUUID()}` + admin = kafka.admin() + await admin.createTopics({ + topics: [{ + topic: testTopic, + numPartitions: 1, + replicationFactor: 1 + }] + }) + clusterIdAvailable = semver.intersects(version, '>=1.13') + expectedProducerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, true, ENTRY_PARENT_HASH) + expectedConsumerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, false, expectedProducerHash) + }) + + describe('checkpoints', () => { + let consumer + let setDataStreamsContextSpy + + beforeEach(async () => { + tracer.init() + tracer.use('kafkajs', { dsmEnabled: true }) + consumer = kafka.consumer({ groupId: 'test-group' }) + await consumer.connect() + await consumer.subscribe({ topic: testTopic }) + setDataStreamsContextSpy = sinon.spy(DataStreamsContext, 'setDataStreamsContext') + }) + + afterEach(async () => { + setDataStreamsContextSpy.restore() + await consumer.disconnect() + }) + + it('Should set a checkpoint on produce', async () => { + const messages = [{ key: 'consumerDSM1', value: 'test2' }] + await sendMessages(kafka, testTopic, messages) + assert.strictEqual(setDataStreamsContextSpy.args[0][0].hash, expectedProducerHash) + }) + + it('Should set a checkpoint on consume (eachMessage)', async () => { + const runArgs = [] + await consumer.run({ + eachMessage: async () => { + runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) + } + }) + await sendMessages(kafka, testTopic, messages) + await consumer.disconnect() + for (const runArg of runArgs) { + assert.strictEqual(runArg.hash, expectedConsumerHash) + } + }) + + it('Should set a checkpoint on consume (eachBatch)', async () => { + const runArgs = [] + await consumer.run({ + eachBatch: async () => { + runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) + } + }) + await sendMessages(kafka, testTopic, messages) + await consumer.disconnect() + for (const runArg of runArgs) { + assert.strictEqual(runArg.hash, expectedConsumerHash) + } + }) + + it('Should set a message payload size when producing a message', async () => { + const messages = [{ key: 'key1', value: 'test2' }] + if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { + DataStreamsProcessor.prototype.recordCheckpoint.restore() + } + const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') + await sendMessages(kafka, testTopic, messages) + assert.ok(Object.hasOwn(recordCheckpointSpy.args[0][0], 'payloadSize')) + recordCheckpointSpy.restore() + }) + + it('Should set a message payload size when consuming a message', async () => { + const messages = [{ key: 'key1', value: 'test2' }] + if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { + DataStreamsProcessor.prototype.recordCheckpoint.restore() + } + const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') + await sendMessages(kafka, testTopic, messages) + await consumer.run({ + eachMessage: async () => { + assert.ok(Object.hasOwn(recordCheckpointSpy.args[0][0], 'payloadSize')) + recordCheckpointSpy.restore() + } + }) + }) + }) + + describe('backlogs', () => { + let consumer + let setOffsetSpy + + beforeEach(async () => { + tracer.init() + tracer.use('kafkajs', { dsmEnabled: true }) + consumer = kafka.consumer({ groupId: 'test-group' }) + await consumer.connect() + await consumer.subscribe({ topic: testTopic }) + setOffsetSpy = sinon.spy(tracer._tracer._dataStreamsProcessor, 'setOffset') + }) + + afterEach(async () => { + setOffsetSpy.restore() + await consumer.disconnect() + }) + + if (semver.intersects(version, '>=1.10')) { + it('Should add backlog on consumer explicit commit', async () => { + // Send a message, consume it, and record the last consumed offset + let commitMeta + const deferred = {} + deferred.promise = new Promise((resolve, reject) => { + deferred.resolve = resolve + deferred.reject = reject + }) + await consumer.run({ + eachMessage: async payload => { + const { topic, partition, message } = payload + commitMeta = { + topic, + partition, + offset: Number(message.offset) + } + deferred.resolve() + }, + autoCommit: false + }) + await sendMessages(kafka, testTopic, messages) + await deferred.promise + await consumer.disconnect() // Flush ongoing `eachMessage` calls + for (const call of setOffsetSpy.getCalls()) { + assert.notStrictEqual(call.args[0]?.type, 'kafka_commit') + } + + /** + * No choice but to reinitialize everything, because the only way to flush eachMessage + * calls is to disconnect. + */ + consumer.connect() + await sendMessages(kafka, testTopic, messages) + await consumer.run({ eachMessage: async () => {}, autoCommit: false }) + setOffsetSpy.resetHistory() + await consumer.commitOffsets([commitMeta]) + await consumer.disconnect() + + // Check our work + const runArg = setOffsetSpy.lastCall.args[0] + sinon.assert.calledOnce(setOffsetSpy) + assert.strictEqual(runArg?.offset, commitMeta.offset) + assert.strictEqual(runArg?.partition, commitMeta.partition) + assert.strictEqual(runArg?.topic, commitMeta.topic) + assertObjectContains(runArg, { + type: 'kafka_commit', + consumer_group: 'test-group' + }) + }) + } + + it('Should add backlog on producer response', async () => { + await sendMessages(kafka, testTopic, messages) + sinon.assert.calledOnce(setOffsetSpy) + const { topic } = setOffsetSpy.lastCall.args[0] + assert.strictEqual(topic, testTopic) + }) + }) + }) + }) + }) +}) + +async function sendMessages (kafka, topic, messages) { + const producer = kafka.producer() + await producer.connect() + await producer.send({ + topic, + messages + }) + await producer.disconnect() +} diff --git a/packages/datadog-plugin-kafkajs/test/index.spec.js b/packages/datadog-plugin-kafkajs/test/index.spec.js index a6b02edeb52..160eeadf46b 100644 --- a/packages/datadog-plugin-kafkajs/test/index.spec.js +++ b/packages/datadog-plugin-kafkajs/test/index.spec.js @@ -3,9 +3,8 @@ const assert = require('node:assert/strict') const { randomUUID } = require('node:crypto') -const { expect } = require('chai') const dc = require('dc-polyfill') -const { describe, it, beforeEach, afterEach, before } = require('mocha') +const { describe, it, beforeEach, afterEach } = require('mocha') const semver = require('semver') const sinon = require('sinon') @@ -13,30 +12,12 @@ 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 { expectedSchema, rawExpectedSchema } = require('./naming') -const DataStreamsContext = require('../../dd-trace/src/datastreams/context') -const { computePathwayHash } = require('../../dd-trace/src/datastreams/pathway') -const { ENTRY_PARENT_HASH, DataStreamsProcessor } = require('../../dd-trace/src/datastreams/processor') -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}`) - } - edgeTags.sort() - return computePathwayHash('test', 'tester', edgeTags, parentHash) -} - describe('Plugin', () => { describe('kafkajs', function () { // TODO: remove when new internal trace has landed @@ -52,8 +33,6 @@ describe('Plugin', () => { let Kafka let Broker let clusterIdAvailable - let expectedProducerHash - let expectedConsumerHash let testTopic describe('without configuration', () => { @@ -61,7 +40,6 @@ describe('Plugin', () => { const messages2 = [{ key: 'key2', value: 'test3' }] beforeEach(async () => { - process.env.DD_DATA_STREAMS_ENABLED = 'true' tracer = require('../../dd-trace') await agent.load('kafkajs') const lib = require(`../../../versions/kafkajs@${version}`).get() @@ -82,8 +60,6 @@ describe('Plugin', () => { }] }) clusterIdAvailable = semver.intersects(version, '>=1.13') - expectedProducerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, true, ENTRY_PARENT_HASH) - expectedConsumerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, false, expectedProducerHash) }) describe('producer', () => { @@ -91,7 +67,6 @@ describe('Plugin', () => { const meta = { 'span.kind': 'producer', component: 'kafkajs', - 'pathway.hash': expectedProducerHash.readBigUInt64LE(0).toString(), 'messaging.destination.name': testTopic, 'messaging.kafka.bootstrap.servers': '127.0.0.1:9092' } @@ -130,7 +105,6 @@ describe('Plugin', () => { const expectedSpanPromise = agent.assertSomeTraces(traces => { const span = traces[0][0] - assertObjectContains(span, { name: resourceName, service: expectedSchema.send.serviceName, @@ -162,7 +136,7 @@ describe('Plugin', () => { it('should not extract bootstrap servers when initialized with a function', async () => { const expectedSpanPromise = agent.assertSomeTraces(traces => { const span = traces[0][0] - expect(span.meta).to.not.have.any.keys(['messaging.kafka.bootstrap.servers']) + assert.ok(!((['messaging.kafka.bootstrap.servers']).some(k => Object.hasOwn((span.meta), k)))) }) kafka = new Kafka({ @@ -217,7 +191,7 @@ 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) - expect(messages[0].headers).to.have.property('x-datadog-trace-id') + assert.ok(Object.hasOwn(messages[0].headers, 'x-datadog-trace-id')) // restore the stub to allow the next send to succeed sendRequestStub.restore() @@ -254,7 +228,6 @@ describe('Plugin', () => { meta: { 'span.kind': 'consumer', component: 'kafkajs', - 'pathway.hash': expectedConsumerHash.readBigUInt64LE(0).toString(), 'messaging.destination.name': testTopic }, resource: testTopic, @@ -408,7 +381,7 @@ describe('Plugin', () => { let eachMessage = async ({ topic, partition, message }) => { setImmediate(() => { try { - expect(spy).to.have.been.calledOnceWith(undefined, beforeFinish.name) + sinon.assert.calledOnceWithExactly(spy, undefined, beforeFinish.name) done() } catch (e) { @@ -431,169 +404,6 @@ describe('Plugin', () => { rawExpectedSchema.receive ) }) - - describe('data stream monitoring', () => { - let consumer - - beforeEach(async () => { - tracer.init() - tracer.use('kafkajs', { dsmEnabled: true }) - consumer = kafka.consumer({ groupId: 'test-group' }) - await consumer.connect() - await consumer.subscribe({ topic: testTopic }) - }) - - before(() => { - clusterIdAvailable = semver.intersects(version, '>=1.13') - expectedProducerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, true, ENTRY_PARENT_HASH) - expectedConsumerHash = getDsmPathwayHash(testTopic, clusterIdAvailable, false, expectedProducerHash) - }) - - afterEach(async () => { - await consumer.disconnect() - }) - - describe('checkpoints', () => { - let setDataStreamsContextSpy - - beforeEach(() => { - setDataStreamsContextSpy = sinon.spy(DataStreamsContext, 'setDataStreamsContext') - }) - - afterEach(() => { - setDataStreamsContextSpy.restore() - }) - - it('Should set a checkpoint on produce', async () => { - const messages = [{ key: 'consumerDSM1', value: 'test2' }] - await sendMessages(kafka, testTopic, messages) - assert.strictEqual(setDataStreamsContextSpy.args[0][0].hash, expectedProducerHash) - }) - - it('Should set a checkpoint on consume (eachMessage)', async () => { - const runArgs = [] - await consumer.run({ - eachMessage: async () => { - runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) - } - }) - await sendMessages(kafka, testTopic, messages) - await consumer.disconnect() - for (const runArg of runArgs) { - assert.strictEqual(runArg.hash, expectedConsumerHash) - } - }) - - it('Should set a checkpoint on consume (eachBatch)', async () => { - const runArgs = [] - await consumer.run({ - eachBatch: async () => { - runArgs.push(setDataStreamsContextSpy.lastCall.args[0]) - } - }) - await sendMessages(kafka, testTopic, messages) - await consumer.disconnect() - for (const runArg of runArgs) { - assert.strictEqual(runArg.hash, expectedConsumerHash) - } - }) - - it('Should set a message payload size when producing a message', async () => { - const messages = [{ key: 'key1', value: 'test2' }] - if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { - DataStreamsProcessor.prototype.recordCheckpoint.restore() - } - const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') - await sendMessages(kafka, testTopic, messages) - assert.ok(Object.hasOwn(recordCheckpointSpy.args[0][0], 'payloadSize')) - recordCheckpointSpy.restore() - }) - - it('Should set a message payload size when consuming a message', async () => { - const messages = [{ key: 'key1', value: 'test2' }] - if (DataStreamsProcessor.prototype.recordCheckpoint.isSinonProxy) { - DataStreamsProcessor.prototype.recordCheckpoint.restore() - } - const recordCheckpointSpy = sinon.spy(DataStreamsProcessor.prototype, 'recordCheckpoint') - await sendMessages(kafka, testTopic, messages) - await consumer.run({ - eachMessage: async () => { - assert.ok(Object.hasOwn(recordCheckpointSpy.args[0][0], 'payloadSize')) - recordCheckpointSpy.restore() - } - }) - }) - }) - - describe('backlogs', () => { - let setOffsetSpy - - beforeEach(() => { - setOffsetSpy = sinon.spy(tracer._tracer._dataStreamsProcessor, 'setOffset') - }) - - afterEach(() => { - setOffsetSpy.restore() - }) - - if (semver.intersects(version, '>=1.10')) { - it('Should add backlog on consumer explicit commit', async () => { - // Send a message, consume it, and record the last consumed offset - let commitMeta - const deferred = {} - deferred.promise = new Promise((resolve, reject) => { - deferred.resolve = resolve - deferred.reject = reject - }) - await consumer.run({ - eachMessage: async payload => { - const { topic, partition, message } = payload - commitMeta = { - topic, - partition, - offset: Number(message.offset) - } - deferred.resolve() - }, - autoCommit: false - }) - await sendMessages(kafka, testTopic, messages) - await deferred.promise - await consumer.disconnect() // Flush ongoing `eachMessage` calls - for (const call of setOffsetSpy.getCalls()) { - expect(call.args[0]).to.not.have.property('type', 'kafka_commit') - } - - /** - * No choice but to reinitialize everything, because the only way to flush eachMessage - * calls is to disconnect. - */ - consumer.connect() - await sendMessages(kafka, testTopic, messages) - await consumer.run({ eachMessage: async () => {}, autoCommit: false }) - setOffsetSpy.resetHistory() - await consumer.commitOffsets([commitMeta]) - await consumer.disconnect() - - // Check our work - const runArg = setOffsetSpy.lastCall.args[0] - expect(setOffsetSpy).to.be.calledOnce - expect(runArg).to.have.property('offset', commitMeta.offset) - expect(runArg).to.have.property('partition', commitMeta.partition) - expect(runArg).to.have.property('topic', commitMeta.topic) - expect(runArg).to.have.property('type', 'kafka_commit') - expect(runArg).to.have.property('consumer_group', 'test-group') - }) - } - - it('Should add backlog on producer response', async () => { - await sendMessages(kafka, testTopic, messages) - expect(setOffsetSpy).to.be.calledOnce - const { topic } = setOffsetSpy.lastCall.args[0] - assert.strictEqual(topic, testTopic) - }) - }) - }) }) }) }) diff --git a/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js b/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js index 414af65f963..3262a7116a4 100644 --- a/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-kafkajs/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'kafka.produce'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-knex/test/integration-test/client.spec.js b/packages/datadog-plugin-knex/test/integration-test/client.spec.js index 0fc86741c6b..b7039bf0d61 100644 --- a/packages/datadog-plugin-knex/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-knex/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('knex', 'knex', version => { diff --git a/packages/datadog-plugin-knex/test/integration-test/server.mjs b/packages/datadog-plugin-knex/test/integration-test/server.mjs index 5f447626c1d..b68dd91c560 100644 --- a/packages/datadog-plugin-knex/test/integration-test/server.mjs +++ b/packages/datadog-plugin-knex/test/integration-test/server.mjs @@ -1,7 +1,7 @@ import 'dd-trace/init.js' +import { AsyncLocalStorage } from 'async_hooks' import express from 'express' import knex from 'knex' -import { AsyncLocalStorage } from 'async_hooks' let counter = 0 const asyncStore = new AsyncLocalStorage() diff --git a/packages/datadog-plugin-koa/test/index.spec.js b/packages/datadog-plugin-koa/test/index.spec.js index 02d8f2572a2..fec6e2a85e4 100644 --- a/packages/datadog-plugin-koa/test/index.spec.js +++ b/packages/datadog-plugin-koa/test/index.spec.js @@ -4,7 +4,7 @@ const assert = require('node:assert/strict') const { AsyncLocalStorage } = require('node:async_hooks') const axios = require('axios') -const { expect } = require('chai') + const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const semver = require('semver') const sinon = require('sinon') @@ -200,7 +200,7 @@ describe('Plugin', () => { try { sinon.assert.called(childSpan.finish) sinon.assert.called(parentSpan.finish) - expect(parentSpan.finish).to.have.been.calledAfter(childSpan.finish) + assert.strictEqual(parentSpan.finish.calledAfter(childSpan.finish), true) assert.strictEqual(childSpan.context()._parentId.toString(10), parentSpan.context().toSpanId()) assert.notStrictEqual(parentSpan.context()._parentId, null) done() diff --git a/packages/datadog-plugin-langchain/src/index.js b/packages/datadog-plugin-langchain/src/index.js index 5d900ac20f9..786bbf884be 100644 --- a/packages/datadog-plugin-langchain/src/index.js +++ b/packages/datadog-plugin-langchain/src/index.js @@ -1,8 +1,8 @@ 'use strict' -const langChainTracingPlugins = require('./tracing') const langChainLLMObsPlugins = require('../../dd-trace/src/llmobs/plugins/langchain') const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const langChainTracingPlugins = require('./tracing') const plugins = {} diff --git a/packages/datadog-plugin-langchain/test/index.spec.js b/packages/datadog-plugin-langchain/test/index.spec.js index 7f68d23cf3d..94b3adea64d 100644 --- a/packages/datadog-plugin-langchain/test/index.spec.js +++ b/packages/datadog-plugin-langchain/test/index.spec.js @@ -2,16 +2,15 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { after, before, beforeEach, describe, it } = require('mocha') +const semifies = require('semifies') -const { useEnv } = require('../../../integration-tests/helpers') +const { assertObjectContains, useEnv } = require('../../../integration-tests/helpers') const iastFilter = require('../../dd-trace/src/appsec/iast/taint-tracking/filter') const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') -const isDdTrace = iastFilter.isDdTrace -const semifies = require('semifies') +const isDdTrace = iastFilter.isDdTrace describe('Plugin', () => { let langchainOpenai @@ -163,17 +162,20 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.name, 'langchain.request') - assert.strictEqual(span.resource, 'langchain.llms.openai.OpenAI') - - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-3.5-turbo-instruct') - assert.strictEqual(span.meta['langchain.request.type'], 'llm') + assertObjectContains(span, { + name: 'langchain.request', + resource: 'langchain.llms.openai.OpenAI', + meta: { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-3.5-turbo-instruct', + 'langchain.request.type': 'llm' + } + }) }) const result = await llm.generate(['what is 2 + 2?']) - assert.ok(result.generations[0][0].text != null) + assert.ok(result.generations[0][0].text) await checkTraces }) @@ -183,15 +185,17 @@ describe('Plugin', () => { .assertSomeTraces(traces => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-3.5-turbo-instruct') + assertObjectContains(span.meta, { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-3.5-turbo-instruct' + }) }) const llm = getLangChainOpenAiClient('llm', { model: 'gpt-3.5-turbo-instruct' }) const result = await llm.generate(['what is 2 + 2?', 'what is the circumference of the earth?']) - assert.ok(result.generations[0][0].text != null) - assert.ok(result.generations[1][0].text != null) + assert.ok(result.generations[0][0].text) + assert.ok(result.generations[1][0].text) await checkTraces }) @@ -202,15 +206,17 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-3.5-turbo-instruct') + assertObjectContains(span.meta, { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-3.5-turbo-instruct' + }) }) const llm = getLangChainOpenAiClient('llm', { model: 'gpt-3.5-turbo-instruct', n: 2 }) const result = await llm.generate(['what is 2 + 2?']) - assert.ok(result.generations[0][0].text != null) - assert.ok(result.generations[0][1].text != null) + assert.ok(result.generations[0][0].text) + assert.ok(result.generations[0][1].text) await checkTraces }) @@ -247,18 +253,21 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.name, 'langchain.request') - assert.strictEqual(span.resource, 'langchain.chat_models.openai.ChatOpenAI') - - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-4') - assert.strictEqual(span.meta['langchain.request.type'], 'chat_model') + assertObjectContains(span, { + name: 'langchain.request', + resource: 'langchain.chat_models.openai.ChatOpenAI', + meta: { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-4', + 'langchain.request.type': 'chat_model' + } + }) }) const chatModel = getLangChainOpenAiClient('chat', { model: 'gpt-4' }) const result = await chatModel.invoke('Hello!') - assert.ok(result.content != null) + assert.ok(result.content) await checkTraces }) @@ -269,8 +278,10 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-4') + assertObjectContains(span.meta, { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-4' + }) }) const chatModel = getLangChainOpenAiClient('chat', { model: 'gpt-4' }) @@ -280,7 +291,7 @@ describe('Plugin', () => { ] const result = await chatModel.invoke(messages) - assert.ok(result.content != null) + assert.ok(result.content) await checkTraces }) @@ -291,8 +302,10 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-4') + assertObjectContains(span.meta, { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-4' + }) }) const chatModel = getLangChainOpenAiClient('chat', { model: 'gpt-4' }) @@ -302,7 +315,7 @@ describe('Plugin', () => { ] const result = await chatModel.invoke(messages) - assert.ok(result.content != null) + assert.ok(result.content) await checkTraces }) @@ -313,8 +326,10 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'gpt-4') + assertObjectContains(span.meta, { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'gpt-4' + }) }) const tools = [ @@ -351,18 +366,22 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.name, 'langchain.request') - assert.strictEqual(span.resource, 'langchain.chat_models.anthropic.ChatAnthropic') + assertObjectContains(span, { + name: 'langchain.request', + resource: 'langchain.chat_models.anthropic.ChatAnthropic', + meta: { + 'langchain.request.provider': 'anthropic', + 'langchain.request.type': 'chat_model' + } + }) - assert.strictEqual(span.meta['langchain.request.provider'], 'anthropic') assert.ok(Object.hasOwn(span.meta, 'langchain.request.model')) - assert.strictEqual(span.meta['langchain.request.type'], 'chat_model') }) const chatModel = getLangChainAnthropicClient('chat', { modelName: 'claude-3-5-sonnet-20241022' }) const result = await chatModel.invoke('Hello!') - assert.ok(result.content != null) + assert.ok(result.content) await checkTraces }) @@ -409,10 +428,13 @@ describe('Plugin', () => { // we already check the chat model span in previous tests assert.strictEqual(spans[1].resource, 'langchain.chat_models.openai.ChatOpenAI') - assert.strictEqual(chainSpan.name, 'langchain.request') - assert.strictEqual(chainSpan.resource, 'langchain_core.runnables.RunnableSequence') - - assert.strictEqual(chainSpan.meta['langchain.request.type'], 'chain') + assertObjectContains(chainSpan, { + name: 'langchain.request', + resource: 'langchain_core.runnables.RunnableSequence', + meta: { + 'langchain.request.type': 'chain' + } + }) }) const model = getLangChainOpenAiClient('chat', { model: 'gpt-4' }) @@ -425,7 +447,7 @@ describe('Plugin', () => { ] const result = await chain.invoke(messages) - assert.ok(result != null) + assert.ok(result) await checkTraces }) @@ -458,12 +480,14 @@ describe('Plugin', () => { // we already check the chat model span in previous tests assert.strictEqual(spans[1].resource, 'langchain.chat_models.openai.ChatOpenAI') - assert.strictEqual(chainSpan.meta['langchain.request.type'], 'chain') + assertObjectContains(chainSpan.meta, { + 'langchain.request.type': 'chain' + }) }) const result = await chain.invoke({ topic: 'chickens', style: 'dad joke' }) - assert.ok(result != null) + assert.ok(result) await checkTraces }) @@ -491,14 +515,16 @@ describe('Plugin', () => { const chainSpan = spans[0] - assert.strictEqual(chainSpan.meta['langchain.request.type'], 'chain') + assertObjectContains(chainSpan.meta, { + 'langchain.request.type': 'chain' + }) }) const result = await chain.batch(['chickens', 'dogs']) assert.strictEqual(result.length, 2) - assert.ok(result[0] != null) - assert.ok(result[1] != null) + assert.ok(result[0]) + assert.ok(result[1]) await checkTraces }) @@ -513,7 +539,9 @@ describe('Plugin', () => { const chainSpan = spans[0] - assert.strictEqual(chainSpan.meta['langchain.request.type'], 'chain') + assertObjectContains(chainSpan.meta, { + 'langchain.request.type': 'chain' + }) }) const parser = new langchainOutputParsers.JsonOutputParser() @@ -561,12 +589,15 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.name, 'langchain.request') - assert.strictEqual(span.resource, 'langchain.embeddings.openai.OpenAIEmbeddings') - - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'text-embedding-ada-002') - assert.strictEqual(span.meta['langchain.request.type'], 'embedding') + assertObjectContains(span, { + name: 'langchain.request', + resource: 'langchain.embeddings.openai.OpenAIEmbeddings', + meta: { + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'text-embedding-ada-002', + 'langchain.request.type': 'embedding' + } + }) }) const query = 'Hello, world!' @@ -583,9 +614,11 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.meta['langchain.request.type'], 'embedding') - assert.strictEqual(span.meta['langchain.request.provider'], 'openai') - assert.strictEqual(span.meta['langchain.request.model'], 'text-embedding-ada-002') + assertObjectContains(span.meta, { + 'langchain.request.type': 'embedding', + 'langchain.request.provider': 'openai', + 'langchain.request.model': 'text-embedding-ada-002' + }) }) const embeddings = getLangChainOpenAiClient('embedding') @@ -616,12 +649,15 @@ describe('Plugin', () => { assert.strictEqual(traces[0].length, 1) const span = traces[0][0] - assert.strictEqual(span.name, 'langchain.request') - assert.strictEqual(span.resource, 'langchain.embeddings.GoogleGenerativeAIEmbeddings') - - assert.strictEqual(span.meta['langchain.request.provider'], 'googlegenerativeai') - assert.strictEqual(span.meta['langchain.request.model'], 'text-embedding-004') - assert.strictEqual(span.meta['langchain.request.type'], 'embedding') + assertObjectContains(span, { + name: 'langchain.request', + resource: 'langchain.embeddings.GoogleGenerativeAIEmbeddings', + meta: { + 'langchain.request.provider': 'googlegenerativeai', + 'langchain.request.model': 'text-embedding-004', + 'langchain.request.type': 'embedding' + } + }) }) const query = 'Hello, world!' @@ -681,7 +717,7 @@ describe('Plugin', () => { try { await myTool.invoke() - expect.fail('Expected an error to be thrown') + assert.fail('Expected an error to be thrown') } catch {} await checkTraces @@ -722,7 +758,7 @@ describe('Plugin', () => { // we need the spanResourceMatch, otherwise we'll match from the beforeEach const result = await vectorstore.similaritySearch('The powerhouse of the cell is the mitochondria', 2) - assert.ok(result != null) + assert.ok(result) await checkTraces }) @@ -747,7 +783,7 @@ describe('Plugin', () => { const result = await vectorstore.similaritySearchWithScore( 'The powerhouse of the cell is the mitochondria', 2 ) - assert.ok(result != null) + assert.ok(result) await checkTraces }) diff --git a/packages/datadog-plugin-langchain/test/integration-test/client.spec.js b/packages/datadog-plugin-langchain/test/integration-test/client.spec.js index d40dd2224a8..0d0e0f83d44 100644 --- a/packages/datadog-plugin-langchain/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-langchain/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -39,7 +39,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'langchain.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, null, { + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port, { NODE_OPTIONS: '--import dd-trace/initialize.mjs' }) diff --git a/packages/datadog-plugin-ldapjs/test/integration-test/client.spec.js b/packages/datadog-plugin-ldapjs/test/integration-test/client.spec.js index e896b47bae7..d33cb4d0576 100644 --- a/packages/datadog-plugin-ldapjs/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-ldapjs/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('ldapjs', 'ldapjs', '>=2', version => { diff --git a/packages/datadog-plugin-limitd-client/test/integration-test/client.spec.js b/packages/datadog-plugin-limitd-client/test/integration-test/client.spec.js index 8cf16cb57d4..1a2777feadf 100644 --- a/packages/datadog-plugin-limitd-client/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-limitd-client/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -42,7 +42,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'tcp.connect'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-lodash/test/integration-test/client.spec.js b/packages/datadog-plugin-lodash/test/integration-test/client.spec.js index 4b31015cf22..8e3a3fdd4f9 100644 --- a/packages/datadog-plugin-lodash/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-lodash/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('lodash', 'lodash', version => { diff --git a/packages/datadog-plugin-mariadb/test/index.spec.js b/packages/datadog-plugin-mariadb/test/index.spec.js index 07ce63d182c..70562811e7a 100644 --- a/packages/datadog-plugin-mariadb/test/index.spec.js +++ b/packages/datadog-plugin-mariadb/test/index.spec.js @@ -10,7 +10,7 @@ const semver = require('semver') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') - +const { assertObjectContains } = require('../../../integration-tests/helpers') const { expectedSchema, rawExpectedSchema } = require('./naming') // https://github.com/mariadb-corporation/mariadb-connector-nodejs/commit/0a90b71ab20ab4e8b6a86a77ba291bba8ba6a34e @@ -105,20 +105,20 @@ describe('Plugin', () => { }) it('should do automatic instrumentation', done => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) - assert.strictEqual(traces[0][0].resource, 'SELECT 1 + 1 AS solution') - assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'db') - assert.strictEqual(traces[0][0].meta['db.user'], 'root') - assert.strictEqual(traces[0][0].meta['db.type'], 'mariadb') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta.component, 'mariadb') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'mariadb') - }) + agent.assertFirstTraceSpan({ + name: expectedSchema.outbound.opName, + service: expectedSchema.outbound.serviceName, + resource: 'SELECT 1 + 1 AS solution', + type: 'sql', + meta: { + 'span.kind': 'client', + 'db.name': 'db', + 'db.user': 'root', + 'db.type': 'mariadb', + component: 'mariadb', + '_dd.integration': 'mariadb' + } + }) .then(done) .catch(done) @@ -129,19 +129,19 @@ describe('Plugin', () => { if (semver.intersects(version, '>=3')) { it('should support prepared statement shorthand', done => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) - assert.strictEqual(traces[0][0].resource, 'SELECT ? + ? AS solution') - assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'db') - assert.strictEqual(traces[0][0].meta['db.user'], 'root') - assert.strictEqual(traces[0][0].meta['db.type'], 'mariadb') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta.component, 'mariadb') - }) + agent.assertFirstTraceSpan({ + name: expectedSchema.outbound.opName, + service: expectedSchema.outbound.serviceName, + resource: 'SELECT ? + ? AS solution', + type: 'sql', + meta: { + 'span.kind': 'client', + 'db.name': 'db', + 'db.user': 'root', + 'db.type': 'mariadb', + component: 'mariadb' + } + }) .then(done) .catch(done) @@ -151,19 +151,19 @@ describe('Plugin', () => { }) it('should support prepared statements', done => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) - assert.strictEqual(traces[0][0].resource, 'SELECT ? + ? AS solution') - assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'db') - assert.strictEqual(traces[0][0].meta['db.user'], 'root') - assert.strictEqual(traces[0][0].meta['db.type'], 'mariadb') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta.component, 'mariadb') - }) + agent.assertFirstTraceSpan({ + name: expectedSchema.outbound.opName, + service: expectedSchema.outbound.serviceName, + resource: 'SELECT ? + ? AS solution', + type: 'sql', + meta: { + 'span.kind': 'client', + 'db.name': 'db', + 'db.user': 'root', + 'db.type': 'mariadb', + component: 'mariadb' + } + }) .then(done) .catch(done) @@ -184,10 +184,12 @@ describe('Plugin', () => { agent .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'mariadb') + assertObjectContains(traces[0][0].meta, { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'mariadb' + }) }) .then(done) .catch(done) @@ -344,18 +346,18 @@ describe('Plugin', () => { }) it('should do automatic instrumentation', done => { - agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].name, expectedSchema.outbound.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) - assert.strictEqual(traces[0][0].resource, 'SELECT 1 + 1 AS solution') - assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.user'], 'root') - assert.strictEqual(traces[0][0].meta['db.type'], 'mariadb') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta.component, 'mariadb') - }) + agent.assertFirstTraceSpan({ + name: expectedSchema.outbound.opName, + service: expectedSchema.outbound.serviceName, + resource: 'SELECT 1 + 1 AS solution', + type: 'sql', + meta: { + 'span.kind': 'client', + 'db.user': 'root', + 'db.type': 'mariadb', + component: 'mariadb' + } + }) .then(done) .catch(done) diff --git a/packages/datadog-plugin-mariadb/test/integration-test/client.spec.js b/packages/datadog-plugin-mariadb/test/integration-test/client.spec.js index ae4bae994e1..b9fbe71fad4 100644 --- a/packages/datadog-plugin-mariadb/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-mariadb/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -35,7 +35,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'mariadb.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-memcached/test/integration-test/client.spec.js b/packages/datadog-plugin-memcached/test/integration-test/client.spec.js index b0db3cede08..f4ef158ae01 100644 --- a/packages/datadog-plugin-memcached/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-memcached/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'memcached.command'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(50000) diff --git a/packages/datadog-plugin-moleculer/src/index.js b/packages/datadog-plugin-moleculer/src/index.js index a820d9d0135..8b59208dc9d 100644 --- a/packages/datadog-plugin-moleculer/src/index.js +++ b/packages/datadog-plugin-moleculer/src/index.js @@ -2,9 +2,9 @@ // TODO: support https://moleculer.services/docs/0.13/actions.html#Streaming +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const MoleculerServerPlugin = require('./server') const MoleculerClientPlugin = require('./client') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class MoleculerPlugin extends CompositePlugin { static id = 'moleculer' diff --git a/packages/datadog-plugin-moleculer/test/integration-test/client.spec.js b/packages/datadog-plugin-moleculer/test/integration-test/client.spec.js index 8314dbc0a6b..b1ceaf9235d 100644 --- a/packages/datadog-plugin-moleculer/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-moleculer/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -35,7 +35,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'moleculer.action'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-mongodb-core/src/index.js b/packages/datadog-plugin-mongodb-core/src/index.js index 2dcd801a119..6c7d2dc5181 100644 --- a/packages/datadog-plugin-mongodb-core/src/index.js +++ b/packages/datadog-plugin-mongodb-core/src/index.js @@ -204,8 +204,12 @@ function isBinary (val) { } function isHeartbeat (ops, config) { - // Check if it's a heartbeat command hello: 1 or helloOk: 1 - return ops && typeof ops === 'object' && (ops.hello === 1 || ops.helloOk === true) + // Check if it's a heartbeat command https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.md + return ( + ops && + typeof ops === 'object' && + (ops.hello === 1 || ops.helloOk === true || ops.ismaster === 1 || ops.isMaster === 1) + ) } module.exports = MongodbCorePlugin diff --git a/packages/datadog-plugin-mongodb-core/test/core.spec.js b/packages/datadog-plugin-mongodb-core/test/core.spec.js index 928de310102..187e430553b 100644 --- a/packages/datadog-plugin-mongodb-core/test/core.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/core.spec.js @@ -10,9 +10,8 @@ const semver = require('semver') const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') -const { expectedSchema, rawExpectedSchema } = require('./naming') - const MongodbCorePlugin = require('../../datadog-plugin-mongodb-core/src/index') +const { expectedSchema, rawExpectedSchema } = require('./naming') const withTopologies = fn => { withVersions('mongodb-core', ['mongodb-core', 'mongodb'], '<4', (version, moduleName) => { diff --git a/packages/datadog-plugin-mongodb-core/test/integration-test/client.spec.js b/packages/datadog-plugin-mongodb-core/test/integration-test/client.spec.js index 49a3236874a..b17d6aa4a16 100644 --- a/packages/datadog-plugin-mongodb-core/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -41,7 +41,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'mongodb.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(30000) @@ -74,7 +74,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'mongodb.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(30000) diff --git a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js index 0b08a3d8989..ca9f479ac2e 100644 --- a/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js +++ b/packages/datadog-plugin-mongodb-core/test/mongodb.spec.js @@ -3,6 +3,7 @@ const assert = require('node:assert/strict') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') +const ddpv = require('mocha/package.json').version const semver = require('semver') const sinon = require('sinon') @@ -10,7 +11,6 @@ const MongodbCorePlugin = require('../../datadog-plugin-mongodb-core/src/index') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const { expectedSchema, rawExpectedSchema } = require('./naming') -const ddpv = require('mocha/package.json').version const withTopologies = fn => { withVersions('mongodb-core', 'mongodb', '>=2', (version, moduleName) => { @@ -683,7 +683,7 @@ describe('Plugin', () => { db = client.db('test') }) - it('should NOT create a span for heartbeat commands', (done) => { + it('should NOT create a span for heartbeat commands (hello, helloOk, ismaster, isMaster)', (done) => { const parentSpan = tracer.startSpan('test.parent') agent @@ -694,15 +694,15 @@ describe('Plugin', () => { assert.strictEqual(span.name, 'test.parent') }) .then(done) + .catch(done) - // Activate parent span scope and trigger heartbeat command - tracer.scope().activate(parentSpan, async () => { - // Admin connect should be all that is needed to trigger heartbeat command for newer versions of mongo - client = await createClient() - db = client.db('test') - - // but we should send a test heartbeat command since older versions of mongo don't auto-send heartbeats - db.command({ hello: 1 }) + // Activate parent span scope and trigger heartbeat commands + tracer.scope().activate(parentSpan, () => { + // Test all heartbeat command variations + db.command({ hello: 1 }).catch(() => {}) + db.command({ helloOk: true }).catch(() => {}) + db.command({ ismaster: 1 }).catch(() => {}) + db.command({ isMaster: 1 }).catch(() => {}) setTimeout(() => parentSpan.finish(), 50) }) }) @@ -724,7 +724,7 @@ describe('Plugin', () => { db = client.db('test') }) - it('should create a child span for heartbeat commands', (done) => { + it('should create a child span for heartbeat commands (hello, helloOk, ismaster, isMaster)', (done) => { const parentSpan = tracer.startSpan('test.parent') agent @@ -742,15 +742,15 @@ describe('Plugin', () => { } }) .then(done) + .catch(done) - // Activate parent span scope and trigger heartbeat command - tracer.scope().activate(parentSpan, async () => { - // Admin connect should be all that is needed to trigger heartbeat command for newer versions of mongo - client = await createClient() - db = client.db('test') - - // but we should send a test heartbeat command since older versions of mongo don't auto-send heartbeats - db.command({ hello: 1 }) + // Activate parent span scope and trigger heartbeat commands + tracer.scope().activate(parentSpan, () => { + // Test all heartbeat command variations + db.command({ hello: 1 }).catch(() => {}) + db.command({ helloOk: true }).catch(() => {}) + db.command({ ismaster: 1 }).catch(() => {}) + db.command({ isMaster: 1 }).catch(() => {}) setTimeout(() => parentSpan.finish(), 200) }) }) @@ -771,7 +771,7 @@ describe('Plugin', () => { db = client.db('test') }) - it('should NOT create a span for heartbeat commands', (done) => { + it('should NOT create a span for heartbeat commands (hello, helloOk, ismaster, isMaster)', (done) => { const parentSpan = tracer.startSpan('test.parent') agent @@ -782,15 +782,15 @@ describe('Plugin', () => { assert.strictEqual(span.name, 'test.parent') }) .then(done) + .catch(done) - // Activate parent span scope and trigger heartbeat command - tracer.scope().activate(parentSpan, async () => { - // Admin connect should be all that is needed to trigger heartbeat command for newer versions of mongo - client = await createClient() - db = client.db('test') - - // but we should send a test heartbeat command since older versions of mongo don't auto-send heartbeats - db.command({ hello: 1 }) + // Activate parent span scope and trigger heartbeat commands + tracer.scope().activate(parentSpan, () => { + // Test all heartbeat command variations + db.command({ hello: 1 }).catch(() => {}) + db.command({ helloOk: true }).catch(() => {}) + db.command({ ismaster: 1 }).catch(() => {}) + db.command({ isMaster: 1 }).catch(() => {}) setTimeout(() => parentSpan.finish(), 50) }) }) @@ -811,7 +811,7 @@ describe('Plugin', () => { db = client.db('test') }) - it('should create a child span for heartbeat commands', (done) => { + it('should create a child span for heartbeat commands (hello, helloOk, ismaster, isMaster)', (done) => { const parentSpan = tracer.startSpan('test.parent') agent @@ -829,15 +829,15 @@ describe('Plugin', () => { } }) .then(done) + .catch(done) - // Activate parent span scope and trigger heartbeat command - tracer.scope().activate(parentSpan, async () => { - // Admin connect should be all that is needed to trigger heartbeat command for newer versions of mongo - client = await createClient() - db = client.db('test') - - // but we should send a test heartbeat command since older versions of mongo don't auto-send heartbeats - db.command({ hello: 1 }) + // Activate parent span scope and trigger heartbeat commands + tracer.scope().activate(parentSpan, () => { + // Test all heartbeat command variations + db.command({ hello: 1 }).catch(() => {}) + db.command({ helloOk: true }).catch(() => {}) + db.command({ ismaster: 1 }).catch(() => {}) + db.command({ isMaster: 1 }).catch(() => {}) setTimeout(() => parentSpan.finish(), 200) }) }) diff --git a/packages/datadog-plugin-mongoose/test/integration-test/client.spec.js b/packages/datadog-plugin-mongoose/test/integration-test/client.spec.js index 3b17b7c811e..f15f58e5956 100644 --- a/packages/datadog-plugin-mongoose/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-mongoose/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -40,7 +40,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'mongodb.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-multer/test/integration-test/client.spec.js b/packages/datadog-plugin-multer/test/integration-test/client.spec.js index 9b9737eb8ec..3defada2067 100644 --- a/packages/datadog-plugin-multer/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-multer/test/integration-test/client.spec.js @@ -1,12 +1,12 @@ 'use strict' +const assert = require('node:assert/strict') +const axios = require('axios') const { useSandbox, sandboxCwd, varySandbox, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const axios = require('axios') withVersions('multer', 'multer', version => { describe('ESM', () => { diff --git a/packages/datadog-plugin-mysql/test/index.spec.js b/packages/datadog-plugin-mysql/test/index.spec.js index b6cd733b848..c8f716e94cf 100644 --- a/packages/datadog-plugin-mysql/test/index.spec.js +++ b/packages/datadog-plugin-mysql/test/index.spec.js @@ -6,6 +6,7 @@ const { afterEach, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire').noPreserveCache() const sinon = require('sinon') +const ddpv = require('mocha/package.json').version const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') @@ -13,8 +14,6 @@ const { assertObjectContains } = require('../../../integration-tests/helpers') const { expectedSchema, rawExpectedSchema } = require('./naming') -const ddpv = require('mocha/package.json').version - describe('Plugin', () => { let mysql let tracer diff --git a/packages/datadog-plugin-mysql/test/integration-test/client.spec.js b/packages/datadog-plugin-mysql/test/integration-test/client.spec.js index 902f900b848..ea87b434592 100644 --- a/packages/datadog-plugin-mysql/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-mysql/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -41,7 +41,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'mysql.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-mysql2/test/index.spec.js b/packages/datadog-plugin-mysql2/test/index.spec.js index a3f7d52d475..728caf32517 100644 --- a/packages/datadog-plugin-mysql2/test/index.spec.js +++ b/packages/datadog-plugin-mysql2/test/index.spec.js @@ -6,6 +6,7 @@ const { afterEach, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire').noPreserveCache() const sinon = require('sinon') +const ddpv = require('mocha/package.json').version const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') @@ -13,8 +14,6 @@ const { assertObjectContains } = require('../../../integration-tests/helpers') const { expectedSchema, rawExpectedSchema } = require('./naming') -const ddpv = require('mocha/package.json').version - describe('Plugin', () => { let mysql2 let tracer diff --git a/packages/datadog-plugin-mysql2/test/integration-test/client.spec.js b/packages/datadog-plugin-mysql2/test/integration-test/client.spec.js index 8672e2fd572..95d9f448840 100644 --- a/packages/datadog-plugin-mysql2/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-mysql2/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'mysql.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-net/test/integration-test/client.spec.js b/packages/datadog-plugin-net/test/integration-test/client.spec.js index b8e3b9c3ff6..73274a2c881 100644 --- a/packages/datadog-plugin-net/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-net/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -42,7 +42,7 @@ describe('esm', () => { assert.strictEqual(metaContainsNet, true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-next/test/index.spec.js b/packages/datadog-plugin-next/test/index.spec.js index 2c5e2c114c4..f756b42e619 100644 --- a/packages/datadog-plugin-next/test/index.spec.js +++ b/packages/datadog-plugin-next/test/index.spec.js @@ -3,18 +3,19 @@ const assert = require('node:assert/strict') /* eslint import/no-extraneous-dependencies: ["error", {"packageDir": ['./']}] */ +const path = require('node:path') +const { execSync, spawn } = require('node:child_process') +const { writeFileSync, readdirSync } = require('node:fs') const axios = require('axios') const { after, before, describe, it } = require('mocha') const { satisfies } = require('semver') -const path = require('node:path') -const { execSync, spawn } = require('node:child_process') -const { writeFileSync, readdirSync } = require('node:fs') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') -const { rawExpectedSchema } = require('./naming') const { NODE_MAJOR } = require('../../../version') +const { rawExpectedSchema } = require('./naming') const min = NODE_MAJOR >= 25 ? '>=13' : '>=11.1' @@ -169,15 +170,19 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET /api/hello/[name]') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta.component, 'next') - assert.strictEqual(spans[1].meta['_dd.integration'], 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + resource: 'GET /api/hello/[name]', + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '200', + component: 'next', + '_dd.integration': 'next' + } + }) }) .then(done) .catch(done) @@ -224,13 +229,17 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '404') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '404', + component: 'next' + } + }) }) .then(done) .catch(done) @@ -245,14 +254,18 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET /_error') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '400') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + resource: 'GET /_error', + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '400', + component: 'next' + } + }) }) .then(done) .catch(done) @@ -286,10 +299,14 @@ describe('Plugin', function () { const nextRequestSpan = spans.find(span => span.name === 'next.request') assert.ok(nextRequestSpan, 'next.request span should exist') - assert.strictEqual(nextRequestSpan.resource, 'GET /api/hello/[name]') - assert.strictEqual(nextRequestSpan.meta['next.page'], '/api/hello/[name]') - assert.strictEqual(nextRequestSpan.meta['http.method'], 'GET') - assert.strictEqual(nextRequestSpan.meta['http.status_code'], '200') + assertObjectContains(nextRequestSpan, { + resource: 'GET /api/hello/[name]', + meta: { + 'next.page': '/api/hello/[name]', + 'http.method': 'GET', + 'http.status_code': '200' + } + }) const webRequestSpan = spans.find(span => span.name === 'web.request') assert.ok(webRequestSpan, 'web.request span should exist') @@ -314,14 +331,18 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET /hello/[name]') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + resource: 'GET /hello/[name]', + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '200', + component: 'next' + } + }) }) .then(done) .catch(done) @@ -344,8 +365,12 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].resource, `GET ${expectedPath}`) - assert.strictEqual(spans[1].meta['http.status_code'], `${statusCode || 200}`) + assertObjectContains(spans[1], { + resource: `GET ${expectedPath}`, + meta: { + 'http.status_code': `${statusCode || 200}` + } + }) }) .then(done) .catch(done) @@ -359,13 +384,17 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '404') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '404', + component: 'next' + } + }) }) .then(done) .catch(done) @@ -396,13 +425,16 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].error, 1) - - assert.strictEqual(spans[1].meta['http.status_code'], '500') - assert.strictEqual(spans[1].meta['error.message'], 'fail') - assert.strictEqual(spans[1].meta['error.type'], 'Error') - assert.ok(spans[1].meta['error.stack'] != null) + assertObjectContains(spans[1], { + name: 'next.request', + error: 1, + meta: { + 'http.status_code': '500', + 'error.message': 'fail', + 'error.type': 'Error' + } + }) + assert.ok(spans[1].meta['error.stack']) }) .then(done) .catch(done) @@ -417,14 +449,18 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET /public/*') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + resource: 'GET /public/*', + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '200', + component: 'next' + } + }) }) return Promise.all([axios.get(`http://127.0.0.1:${port}/test.txt`), tracingPromise]) @@ -438,11 +474,15 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].resource, 'GET /_next/static/*') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + resource: 'GET /_next/static/*', + meta: { + 'http.method': 'GET', + 'http.status_code': '200', + component: 'next' + } + }) }) return Promise.all([axios.get(`http://127.0.0.1:${port}/_next/static/chunks/${file}`), tracingPromise]) @@ -520,20 +560,24 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET /api/hello/[name]') - assert.strictEqual(spans[1].error, 1) - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta.foo, 'bar') - assert.strictEqual(spans[1].meta.req, 'IncomingMessage') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + resource: 'GET /api/hello/[name]', + error: 1, + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '200', + foo: 'bar', + req: 'IncomingMessage', + component: 'next', + times_hook_called: '1' + } + }) // assert request hook was only called once across the whole request - assert.strictEqual(spans[1].meta.times_hook_called, '1') }) .then(done) .catch(done) @@ -549,12 +593,16 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].error, 1) + assertObjectContains(spans[1], { + name: 'next.request', + error: 1, + meta: { + 'error.message': 'error in app dir api route', + 'error.type': 'Error' + } + }) - assert.strictEqual(spans[1].meta['error.message'], 'error in app dir api route') - assert.strictEqual(spans[1].meta['error.type'], 'Error') - assert.ok(spans[1].meta['error.stack'] != null) + assert.ok(spans[1].meta['error.stack']) }) .then(done) .catch(done) @@ -589,14 +637,18 @@ describe('Plugin', function () { .assertSomeTraces(traces => { const spans = traces[0] - assert.strictEqual(spans[1].name, 'next.request') - assert.strictEqual(spans[1].service, 'test') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, expectedResource) - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta.component, 'next') + assertObjectContains(spans[1], { + name: 'next.request', + service: 'test', + type: 'web', + resource: expectedResource, + meta: { + 'span.kind': 'server', + 'http.method': 'GET', + 'http.status_code': '200', + component: 'next' + } + }) }) return Promise.all([axios.get(`http://127.0.0.1:${port}${resource}`), promise]) diff --git a/packages/datadog-plugin-next/test/integration-test/client.spec.js b/packages/datadog-plugin-next/test/integration-test/client.spec.js index 4abeb2fb312..07e3ea6cf0f 100644 --- a/packages/datadog-plugin-next/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-next/test/integration-test/client.spec.js @@ -2,6 +2,7 @@ const assert = require('node:assert/strict') +const { execSync } = require('child_process') const { FakeAgent, curlAndAssertMessage, @@ -14,7 +15,6 @@ const { const { withVersions } = require('../../../dd-trace/test/setup/mocha') const { assertObjectContains } = require('../../../../integration-tests/helpers') const { NODE_MAJOR } = require('../../../../version') -const { execSync } = require('child_process') const hookFile = 'dd-trace/loader-hook.mjs' const min = NODE_MAJOR >= 25 ? '>=13' : '>=11.1' @@ -53,7 +53,7 @@ describe('esm', () => { for (const variant of varySandbox.VARIANTS) { it(`is instrumented loaded with ${variant}`, async () => { - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port, undefined, { + proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port, { NODE_OPTIONS: `--loader=${hookFile} --require dd-trace/init --openssl-legacy-provider` }) return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { diff --git a/packages/datadog-plugin-node-serialize/test/integration-test/client.spec.js b/packages/datadog-plugin-node-serialize/test/integration-test/client.spec.js index eb8f1ff6059..ce3f642e3df 100644 --- a/packages/datadog-plugin-node-serialize/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-node-serialize/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { useSandbox, sandboxCwd, varySandbox, FakeAgent, spawnPluginIntegrationTestProc, curl } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('node-serialize', 'node-serialize', version => { diff --git a/packages/datadog-plugin-openai/src/index.js b/packages/datadog-plugin-openai/src/index.js index 52a36f74776..dbeabad2646 100644 --- a/packages/datadog-plugin-openai/src/index.js +++ b/packages/datadog-plugin-openai/src/index.js @@ -1,8 +1,8 @@ 'use strict' const CompositePlugin = require('../../dd-trace/src/plugins/composite') -const OpenAiTracingPlugin = require('./tracing') const OpenAiLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/openai') +const OpenAiTracingPlugin = require('./tracing') class OpenAiPlugin extends CompositePlugin { static id = 'openai' diff --git a/packages/datadog-plugin-openai/src/stream-helpers.js b/packages/datadog-plugin-openai/src/stream-helpers.js index 5d9a4ce33a1..379f47e077e 100644 --- a/packages/datadog-plugin-openai/src/stream-helpers.js +++ b/packages/datadog-plugin-openai/src/stream-helpers.js @@ -1,10 +1,30 @@ 'use strict' +/** + * @typedef {Record} JsonObject + * + * @typedef {{ function: { arguments: string } }} ToolCall + * @typedef {{ content?: string, tool_calls?: ToolCall[] }} ChatDelta + * + * @typedef {{ + * index: number, + * finish_reason?: string | null, + * text?: string, + * delta?: ChatDelta + * }} StreamChoice + * + * @typedef {JsonObject & { choices: StreamChoice[], usage?: unknown }} StreamChunk + * @typedef {JsonObject & { choices: Array, usage?: unknown }} StreamResponseBody + * + * @typedef {JsonObject & { status?: string }} ResponsesApiResponse + * @typedef {JsonObject & { response?: ResponsesApiResponse }} ResponsesApiChunk + */ + /** * Combines legacy OpenAI streamed chunks into a single object. * These legacy chunks are returned as buffers instead of individual objects. * @param {readonly Uint8Array[]} chunks - * @returns {Array>} + * @returns {JsonObject[]} */ function convertBuffersToObjects (chunks) { return Buffer @@ -21,10 +41,10 @@ function convertBuffersToObjects (chunks) { * The shared logic will add a new choice index entry if it doesn't exist, and otherwise * hand off to a onChoice handler to add that choice to the previously stored choice. * - * @param {Array>} chunks + * @param {StreamChunk[]} chunks * @param {number} n - * @param {function(Record, Record): void} onChoice - * @returns {Record} + * @param {(newChoice: StreamChoice, existingChoice: StreamChoice) => void} onChoice + * @returns {StreamResponseBody} */ function constructResponseFromStreamedChunks (chunks, n, onChoice) { const body = { ...chunks[0], choices: Array.from({ length: n }) } @@ -54,9 +74,9 @@ function constructResponseFromStreamedChunks (chunks, n, onChoice) { /** * Constructs the entire response from a stream of OpenAI completion chunks, * mainly combining the text choices of each chunk into a single string per choice. - * @param {Array>} chunks + * @param {StreamChunk[]} chunks * @param {number} n the number of choices to expect in the response - * @returns {Record} + * @returns {StreamResponseBody} */ function constructCompletionResponseFromStreamedChunks (chunks, n) { return constructResponseFromStreamedChunks(chunks, n, (choice, oldChoice) => { @@ -74,9 +94,9 @@ function constructCompletionResponseFromStreamedChunks (chunks, n) { /** * Constructs the entire response from a stream of OpenAI chat completion chunks, * mainly combining the text choices of each chunk into a single string per choice. - * @param {Array>} chunks + * @param {StreamChunk[]} chunks * @param {number} n the number of choices to expect in the response - * @returns {Record} + * @returns {StreamResponseBody} */ function constructChatCompletionResponseFromStreamedChunks (chunks, n) { return constructResponseFromStreamedChunks(chunks, n, (choice, oldChoice) => { @@ -110,8 +130,8 @@ function constructChatCompletionResponseFromStreamedChunks (chunks, n) { /** * Constructs the entire response from a stream of OpenAI responses chunks. * The responses API uses event-based streaming with delta chunks. - * @param {Array>} chunks - * @returns {Record|undefined} + * @param {ResponsesApiChunk[]} chunks + * @returns {ResponsesApiResponse|undefined} */ function constructResponseResponseFromStreamedChunks (chunks) { // The responses API streams events with different types: diff --git a/packages/datadog-plugin-openai/src/tracing.js b/packages/datadog-plugin-openai/src/tracing.js index b5f3ab4a7a2..32347fc3c9c 100644 --- a/packages/datadog-plugin-openai/src/tracing.js +++ b/packages/datadog-plugin-openai/src/tracing.js @@ -4,10 +4,10 @@ const path = require('path') const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const { storage } = require('../../datadog-core') -const services = require('./services') const Sampler = require('../../dd-trace/src/sampler') const { MEASURED } = require('../../../ext/tags') +const { DD_MAJOR } = require('../../../version') const { convertBuffersToObjects, constructCompletionResponseFromStreamedChunks, @@ -15,7 +15,7 @@ const { constructResponseResponseFromStreamedChunks } = require('./stream-helpers') -const { DD_MAJOR } = require('../../../version') +const services = require('./services') class OpenAiTracingPlugin extends TracingPlugin { static id = 'openai' diff --git a/packages/datadog-plugin-openai/test/index.spec.js b/packages/datadog-plugin-openai/test/index.spec.js index 4c652562443..9f1ce739cc0 100644 --- a/packages/datadog-plugin-openai/test/index.spec.js +++ b/packages/datadog-plugin-openai/test/index.spec.js @@ -8,7 +8,7 @@ const Path = require('path') const semver = require('semver') const sinon = require('sinon') -const { useEnv } = require('../../../integration-tests/helpers') +const { assertObjectContains, useEnv } = require('../../../integration-tests/helpers') const { DogStatsDClient } = require('../../dd-trace/src/dogstatsd') const { NoopExternalLogger } = require('../../dd-trace/src/external-logger/src') const Sampler = require('../../dd-trace/src/sampler') @@ -114,14 +114,12 @@ describe('Plugin', () => { }) it('should attach an error to the span', async () => { - const checkTraces = agent - .assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].error, 1) - // the message content differs on OpenAI version, even between patches - assert.ok(traces[0][0].meta['error.message'] != null) - assert.strictEqual(traces[0][0].meta['error.type'], 'Error') - assert.ok(traces[0][0].meta['error.stack'] != null) - }) + const checkTraces = agent.assertFirstTraceSpan({ + error: 1, + meta: { + 'error.type': 'Error' + } + }) const params = { model: 'gpt-3.5-turbo', // incorrect model @@ -174,10 +172,10 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.completions.create(params) - assert.ok(result.id != null) + assert.ok(result.id) } else { const result = await openai.createCompletion(params) - assert.ok(result.data.id != null) + assert.ok(result.data.id) } tracer.trace('child of outer', innerSpan => { @@ -234,14 +232,17 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createCompletion') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/completions') - - assert.strictEqual(traces[0][0].meta.component, 'openai') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'openai') - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'gpt-3.5-turbo-instruct') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/completions', + component: 'openai', + '_dd.integration': 'openai', + 'openai.request.model': 'gpt-3.5-turbo-instruct' + } + }) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.model')) }) @@ -256,10 +257,10 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.completions.create(params) - assert.ok(result.id != null) + assert.ok(result.id) } else { const result = await openai.createCompletion(params) - assert.ok(result.data.id != null) + assert.ok(result.data.id) } await checkTraces @@ -288,10 +289,10 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.completions.create(params) - assert.ok(result.id != null) + assert.ok(result.id) } else { const result = await openai.createCompletion(params) - assert.ok(result.data.id != null) + assert.ok(result.data.id) } await checkTraces @@ -397,11 +398,14 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createEmbedding') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/embeddings') - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') - - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'text-embedding-ada-002') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.endpoint': '/vcr/openai/embeddings', + 'openai.request.method': 'POST', + 'openai.request.model': 'text-embedding-ada-002' + } + }) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.model')) }) @@ -413,10 +417,10 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.embeddings.create(params) - assert.ok(result.model != null) + assert.ok(result.model) } else { const result = await openai.createEmbedding(params) - assert.ok(result.data.model != null) + assert.ok(result.data.model) } await checkTraces @@ -434,9 +438,13 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'listModels') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/models') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/vcr/openai/models' + } + }) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.count')) }) @@ -444,11 +452,11 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.models.list() assert.deepStrictEqual(result.object, 'list') - assert.ok(result.data.length != null) + assert.ok(result.data.length) } else { const result = await openai.listModels() assert.deepStrictEqual(result.data.object, 'list') - assert.ok(result.data.data.length != null) + assert.ok(result.data.data.length) } await checkTraces @@ -464,12 +472,16 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'retrieveModel') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') // TODO: this might be a bug... - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/models/*') - assert.strictEqual(traces[0][0].meta['openai.request.id'], 'gpt-4') - assert.strictEqual(traces[0][0].meta['openai.response.owned_by'], 'openai') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/v1/models/*', + 'openai.request.id': 'gpt-4', + 'openai.response.owned_by': 'openai' + } + }) }) if (semver.satisfies(realVersion, '>=4.0.0')) { @@ -496,16 +508,18 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'deleteModel') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'DELETE') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/models/*') - - assert.strictEqual(traces[0][0].metrics['openai.response.deleted'], 1) + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'DELETE', + 'openai.request.endpoint': '/v1/models/*', + 'openai.response.id': 'ft:gpt-4.1-mini-2025-04-14:datadog-staging::BkaILRSh' + }, + metrics: { + 'openai.response.deleted': 1 + } + }) assert.ok('openai.response.id' in traces[0][0].meta) - assert.strictEqual( - traces[0][0].meta['openai.response.id'], - 'ft:gpt-4.1-mini-2025-04-14:datadog-staging::BkaILRSh' - ) }) if (semver.satisfies(realVersion, '>=4.0.0')) { @@ -532,23 +546,26 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'listFiles') } - assert.strictEqual(traces[0][0].error, 0) - - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/files') - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.endpoint': '/vcr/openai/files', + 'openai.request.method': 'GET' + } + }) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.count')) }) if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.files.list() - assert.ok(result.data.length != null) - assert.ok(result.data[0].id != null) + assert.ok(result.data.length) + assert.ok(result.data[0].id) } else { const result = await openai.listFiles() - assert.ok(result.data.data.length != null) - assert.ok(result.data.data[0].id != null) + assert.ok(result.data.data.length) + assert.ok(result.data.data[0].id) } await checkTraces @@ -568,16 +585,19 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createFile') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/files') - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') - - assert.strictEqual(traces[0][0].meta['openai.request.filename'], 'fine-tune.jsonl') - assert.strictEqual(traces[0][0].meta['openai.request.purpose'], 'fine-tune') - assert.strictEqual(traces[0][0].meta['openai.response.purpose'], 'fine-tune') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.endpoint': '/vcr/openai/files', + 'openai.request.method': 'POST', + 'openai.request.filename': 'fine-tune.jsonl', + 'openai.request.purpose': 'fine-tune', + 'openai.response.purpose': 'fine-tune', + 'openai.response.filename': 'fine-tune.jsonl' + } + }) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.status')) assert.match(traces[0][0].meta['openai.response.id'], /^file-/) - assert.strictEqual(traces[0][0].meta['openai.response.filename'], 'fine-tune.jsonl') assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.bytes')) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.created_at')) }) @@ -609,13 +629,16 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'retrieveFile') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/files/*') - - assert.strictEqual(traces[0][0].meta['openai.response.filename'], 'fine-tune.jsonl') - assert.strictEqual(traces[0][0].meta['openai.response.id'], 'file-RpTpuvRVtnKpdKZb7DDGto') - assert.strictEqual(traces[0][0].meta['openai.response.purpose'], 'fine-tune') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/v1/files/*', + 'openai.response.filename': 'fine-tune.jsonl', + 'openai.response.id': 'file-RpTpuvRVtnKpdKZb7DDGto', + 'openai.response.purpose': 'fine-tune' + } + }) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.status')) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.bytes')) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.created_at')) @@ -624,11 +647,11 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.files.retrieve('file-RpTpuvRVtnKpdKZb7DDGto') - assert.ok(result.filename != null) + assert.ok(result.filename) } else { const result = await openai.retrieveFile('file-RpTpuvRVtnKpdKZb7DDGto') - assert.ok(result.data.filename != null) + assert.ok(result.data.filename) } await checkTraces @@ -646,15 +669,19 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'downloadFile') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/files/*/content') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/v1/files/*/content' + } + }) }) if (semver.satisfies(realVersion, '>=4.0.0 < 4.17.1')) { const result = await openai.files.retrieveContent('file-RpTpuvRVtnKpdKZb7DDGto') - assert.ok(result != null) + assert.ok(result) } else if (semver.satisfies(realVersion, '>=4.17.1')) { const result = await openai.files.content('file-RpTpuvRVtnKpdKZb7DDGto') @@ -662,7 +689,7 @@ describe('Plugin', () => { } else { const result = await openai.downloadFile('file-RpTpuvRVtnKpdKZb7DDGto') - assert.ok(result.data != null) + assert.ok(result.data) } await checkTraces @@ -679,11 +706,14 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'deleteFile') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'DELETE') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/files/*') - - assert.strictEqual(traces[0][0].meta['openai.response.id'], 'file-RpTpuvRVtnKpdKZb7DDGto') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'DELETE', + 'openai.request.endpoint': '/v1/files/*', + 'openai.response.id': 'file-RpTpuvRVtnKpdKZb7DDGto' + } + }) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.deleted')) }) @@ -716,14 +746,17 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createFineTune') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/fine_tuning/jobs') - - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'gpt-4.1-mini-2025-04-14') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/fine_tuning/jobs', + 'openai.request.model': 'gpt-4.1-mini-2025-04-14', + 'openai.response.model': 'gpt-4.1-mini-2025-04-14' + } + }) assert.match(traces[0][0].meta['openai.response.id'], /^ftjob-/) - assert.strictEqual(traces[0][0].meta['openai.response.model'], 'gpt-4.1-mini-2025-04-14') assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.created_at')) }) @@ -733,7 +766,7 @@ describe('Plugin', () => { } const result = await openai.fineTuning.jobs.create(params) - assert.ok(result.id != null) + assert.ok(result.id) await checkTraces }) @@ -752,11 +785,14 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'retrieveFineTune') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/fine_tuning/jobs/*') - - assert.strictEqual(traces[0][0].meta['openai.response.id'], 'ftjob-q9CUUUsHJemGUVQ1Ecc01zcf') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/v1/fine_tuning/jobs/*', + 'openai.response.id': 'ftjob-q9CUUUsHJemGUVQ1Ecc01zcf' + } + }) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.model')) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.created_at')) }) @@ -782,10 +818,14 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].resource, 'cancelFineTune') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/fine_tuning/jobs/*/cancel') - assert.strictEqual(traces[0][0].meta['openai.response.id'], 'ftjob-q9CUUUsHJemGUVQ1Ecc01zcf') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/v1/fine_tuning/jobs/*/cancel', + 'openai.response.id': 'ftjob-q9CUUUsHJemGUVQ1Ecc01zcf' + } + }) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.created_at')) }) @@ -810,9 +850,13 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].resource, 'listFineTuneEvents') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/v1/fine_tuning/jobs/*/events') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/v1/fine_tuning/jobs/*/events' + } + }) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.count')) }) @@ -837,10 +881,14 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'listFineTunes') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'GET') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/fine_tuning/jobs') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'GET', + 'openai.request.endpoint': '/vcr/openai/fine_tuning/jobs' + } + }) assert.ok(Object.hasOwn(traces[0][0].metrics, 'openai.response.count')) }) @@ -865,9 +913,13 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createModeration') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/moderations') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/moderations' + } + }) assert.match(traces[0][0].meta['openai.response.id'], /^modr-/) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.model')) @@ -905,11 +957,15 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createImage') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/images/generations') - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'dall-e-3') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/images/generations', + 'openai.request.model': 'dall-e-3' + } + }) }) if (semver.satisfies(realVersion, '>=4.0.0')) { @@ -924,7 +980,7 @@ describe('Plugin', () => { if (responseFormat === 'url') { assert.strictEqual(result.data[0].url.startsWith('https://'), true) } else { - assert.ok(result.data[0].b64_json != null) + assert.ok(result.data[0].b64_json) } } else { const result = await openai.createImage({ @@ -938,7 +994,7 @@ describe('Plugin', () => { if (responseFormat === 'url') { assert.strictEqual(result.data.data[0].url.startsWith('https://'), true) } else { - assert.ok(result.data.data[0].b64_json != null) + assert.ok(result.data.data[0].b64_json) } } @@ -966,10 +1022,14 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createImageEdit') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/images/edits') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/images/edits' + } + }) // TODO(sabrenner): fix in a follow-up (super simple - img.name) }) @@ -1009,10 +1069,14 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createImageVariation') } - assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/images/variations') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/images/variations' + } + }) }) if (semver.satisfies(realVersion, '>=4.0.0')) { @@ -1053,12 +1117,15 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createTranscription') } - assert.strictEqual(traces[0][0].error, 0) - assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/audio/transcriptions') - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'gpt-4o-mini-transcribe') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.endpoint': '/vcr/openai/audio/transcriptions', + 'openai.request.method': 'POST', + 'openai.request.model': 'gpt-4o-mini-transcribe' + } + }) }) const result = await openai.audio.transcriptions.create({ @@ -1095,12 +1162,15 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createTranslation') } - assert.strictEqual(traces[0][0].error, 0) - assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/audio/translations') - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'whisper-1') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.endpoint': '/vcr/openai/audio/translations', + 'openai.request.method': 'POST', + 'openai.request.model': 'whisper-1' + } + }) }) if (semver.satisfies(realVersion, '>=4.0.0')) { @@ -1111,7 +1181,7 @@ describe('Plugin', () => { temperature: 0.5 }) - assert.ok(result.text != null) + assert.ok(result.text) } else { const result = await openai.createTranslation( fs.createReadStream(Path.join(__dirname, 'translation.m4a')), @@ -1121,7 +1191,7 @@ describe('Plugin', () => { 0.5 ) - assert.ok(result.data.text != null) + assert.ok(result.data.text) } await checkTraces @@ -1146,13 +1216,15 @@ describe('Plugin', () => { } else { assert.strictEqual(traces[0][0].resource, 'createChatCompletion') } - assert.strictEqual(traces[0][0].error, 0) - - assert.strictEqual(traces[0][0].meta['openai.request.method'], 'POST') assert.ok('openai.request.endpoint' in traces[0][0].meta) - assert.strictEqual(traces[0][0].meta['openai.request.endpoint'], '/vcr/openai/chat/completions') - - assert.strictEqual(traces[0][0].meta['openai.request.model'], 'gpt-3.5-turbo') + assertObjectContains(traces[0][0], { + error: 0, + meta: { + 'openai.request.method': 'POST', + 'openai.request.endpoint': '/vcr/openai/chat/completions', + 'openai.request.model': 'gpt-3.5-turbo' + } + }) assert.ok(Object.hasOwn(traces[0][0].meta, 'openai.response.model')) }) @@ -1181,19 +1253,19 @@ describe('Plugin', () => { const result = await prom - assert.ok(result.id != null) - assert.ok(result.model != null) + assert.ok(result.id) + assert.ok(result.model) assert.deepStrictEqual(result.choices[0].message.role, 'assistant') - assert.ok(result.choices[0].message.content != null) - assert.ok(result.choices[0].finish_reason != null) + assert.ok(result.choices[0].message.content) + assert.ok(result.choices[0].finish_reason) } else { const result = await openai.createChatCompletion(params) - assert.ok(result.data.id != null) - assert.ok(result.data.model != null) + assert.ok(result.data.id) + assert.ok(result.data.model) assert.deepStrictEqual(result.data.choices[0].message.role, 'assistant') - assert.ok(result.data.choices[0].message.content != null) - assert.ok(result.data.choices[0].finish_reason != null) + assert.ok(result.data.choices[0].message.content) + assert.ok(result.data.choices[0].finish_reason) } await checkTraces @@ -1271,10 +1343,10 @@ describe('Plugin', () => { if (semver.satisfies(realVersion, '>=4.0.0')) { const result = await openai.chat.completions.create(params) - assert.ok(result.id != null) + assert.ok(result.id) } else { const result = await openai.createChatCompletion(params) - assert.ok(result.data.id != null) + assert.ok(result.data.id) } await checkTraces @@ -1591,7 +1663,7 @@ describe('Plugin', () => { assert.ok(!Object.hasOwn(prom, 'withResponse') && ('withResponse' in prom)) const response = await prom - assert.ok(response.choices[0].message.content != null) + assert.ok(response.choices[0].message.content) await checkTraces }) diff --git a/packages/datadog-plugin-openai/test/integration-test/client.spec.js b/packages/datadog-plugin-openai/test/integration-test/client.spec.js index 98136c6b1f4..91b8dc375d5 100644 --- a/packages/datadog-plugin-openai/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-openai/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -48,11 +48,10 @@ describe('esm', () => { ) }) - proc = await spawnPluginIntegrationTestProc( + proc = await spawnPluginIntegrationTestProcAndExpectExit( sandboxCwd(), 'server.mjs', agent.port, - null, { NODE_OPTIONS: '--import dd-trace/initialize.mjs', } diff --git a/packages/datadog-plugin-opensearch/test/integration-test/client.spec.js b/packages/datadog-plugin-opensearch/test/integration-test/client.spec.js index 381d7312dea..aaa82045628 100644 --- a/packages/datadog-plugin-opensearch/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-opensearch/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'opensearch.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-oracledb/test/integration-test/client.spec.js b/packages/datadog-plugin-oracledb/test/integration-test/client.spec.js index 483a57d6a5d..ae7499aa9ce 100644 --- a/packages/datadog-plugin-oracledb/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-oracledb/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'oracle.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-passport-http/test/integration-test/client.spec.js b/packages/datadog-plugin-passport-http/test/integration-test/client.spec.js index 47f0fcb3283..a69a50366e1 100644 --- a/packages/datadog-plugin-passport-http/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-passport-http/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('passport-http', 'passport-http', version => { diff --git a/packages/datadog-plugin-pg/test/index.spec.js b/packages/datadog-plugin-pg/test/index.spec.js index 61c2b48b93e..2cf9f73fece 100644 --- a/packages/datadog-plugin-pg/test/index.spec.js +++ b/packages/datadog-plugin-pg/test/index.spec.js @@ -6,11 +6,12 @@ const net = require('node:net') const semver = require('semver') +const ddpv = require('mocha/package.json').version const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') const { expectedSchema, rawExpectedSchema } = require('./naming') -const ddpv = require('mocha/package.json').version const clients = { pg: pg => pg.Client @@ -71,13 +72,19 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) assert.strictEqual(traces[0][0].resource, 'SELECT $1::text as message') assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.user'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.type'], 'postgres') - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].meta['_dd.integration'], 'pg') - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0], { + meta: { + 'span.kind': 'client', + 'db.name': 'postgres', + 'db.user': 'postgres', + 'db.type': 'postgres', + component: 'pg', + '_dd.integration': 'pg' + }, + metrics: { + 'network.destination.port': 5432 + } + }) if (implementation !== 'pg.native') { assert.ok(Object.hasOwn(traces[0][0].metrics, 'db.pid')) @@ -119,12 +126,18 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) assert.strictEqual(traces[0][0].resource, 'SELECT $1::text as message') assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.user'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.type'], 'postgres') - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0], { + meta: { + 'span.kind': 'client', + 'db.name': 'postgres', + 'db.user': 'postgres', + 'db.type': 'postgres', + component: 'pg' + }, + metrics: { + 'network.destination.port': 5432 + } + }) if (implementation !== 'pg.native') { assert.ok(Object.hasOwn(traces[0][0].metrics, 'db.pid')) @@ -143,11 +156,17 @@ describe('Plugin', () => { let error agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) - assert.strictEqual(traces[0][0].meta[ERROR_STACK], error.stack) - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0], { + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + component: 'pg' + }, + metrics: { + 'network.destination.port': 5432 + } + }) }) .then(done) .catch(done) @@ -165,16 +184,19 @@ describe('Plugin', () => { let error agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta[ERROR_TYPE], error.name) - assert.strictEqual(traces[0][0].meta[ERROR_MESSAGE], error.message) + assertObjectContains(traces[0][0].meta, { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + component: 'pg' + }) // pg modifies stacktraces as of v8.11.1 const actualErrorNoStack = traces[0][0].meta[ERROR_STACK].split('\n')[0] const expectedErrorNoStack = error.stack.split('\n')[0] assert.deepStrictEqual(actualErrorNoStack, expectedErrorNoStack) - - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0].metrics, { + 'network.destination.port': 5432 + }) }) .then(done) .catch(done) @@ -234,12 +256,18 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].service, expectedSchema.outbound.serviceName) assert.strictEqual(traces[0][0].resource, 'SELECT * FROM generate_series(0, 1) num') assert.strictEqual(traces[0][0].type, 'sql') - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.type'], 'postgres') - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].metrics['db.stream'], 1) - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0], { + meta: { + 'span.kind': 'client', + 'db.name': 'postgres', + 'db.type': 'postgres', + component: 'pg' + }, + metrics: { + 'db.stream': 1, + 'network.destination.port': 5432 + } + }) }) const cursor = client.query(new Cursor('SELECT * FROM generate_series(0, 1) num')) @@ -265,12 +293,18 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].resource, 'SELECT * FROM generate_series(0, 1) num') assert.strictEqual(traces[0][0].type, 'sql') assert.strictEqual(traces[0][0].error, 0) - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.type'], 'postgres') - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].metrics['db.stream'], 1) - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0], { + meta: { + 'span.kind': 'client', + 'db.name': 'postgres', + 'db.type': 'postgres', + component: 'pg' + }, + metrics: { + 'db.stream': 1, + 'network.destination.port': 5432 + } + }) }) const query = new QueryStream('SELECT * FROM generate_series(0, 1) num', []) @@ -294,12 +328,18 @@ describe('Plugin', () => { assert.strictEqual(traces[0][0].resource, 'SELECT * FROM generate_series(0, 1) num') assert.strictEqual(traces[0][0].type, 'sql') assert.strictEqual(traces[0][0].error, 1) - assert.strictEqual(traces[0][0].meta['span.kind'], 'client') - assert.strictEqual(traces[0][0].meta['db.name'], 'postgres') - assert.strictEqual(traces[0][0].meta['db.type'], 'postgres') - assert.strictEqual(traces[0][0].meta.component, 'pg') - assert.strictEqual(traces[0][0].metrics['db.stream'], 1) - assert.strictEqual(traces[0][0].metrics['network.destination.port'], 5432) + assertObjectContains(traces[0][0], { + meta: { + 'span.kind': 'client', + 'db.name': 'postgres', + 'db.type': 'postgres', + component: 'pg' + }, + metrics: { + 'db.stream': 1, + 'network.destination.port': 5432 + } + }) }) const query = new QueryStream('SELECT * FROM generate_series(0, 1) num', []) @@ -609,7 +649,9 @@ describe('Plugin', () => { it('query should inject _dd.dbm_trace_injected into span', done => { agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].meta['_dd.dbm_trace_injected'], 'true') + assertObjectContains(traces[0][0].meta, { + '_dd.dbm_trace_injected': 'true' + }) done() }) diff --git a/packages/datadog-plugin-pg/test/integration-test/client.spec.js b/packages/datadog-plugin-pg/test/integration-test/client.spec.js index ab101858d9b..19768d80ac4 100644 --- a/packages/datadog-plugin-pg/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-pg/test/integration-test/client.spec.js @@ -2,16 +2,16 @@ const assert = require('node:assert/strict') +const semver = require('semver') const { FakeAgent, checkSpansForServiceName, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') -const semver = require('semver') describe('esm', () => { let agent @@ -51,7 +51,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'pg.query'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-pino/test/integration-test/client.spec.js b/packages/datadog-plugin-pino/test/integration-test/client.spec.js index 139cb8e95dc..a93083df01f 100644 --- a/packages/datadog-plugin-pino/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-pino/test/integration-test/client.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict') const { FakeAgent, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, sandboxCwd, useSandbox, varySandbox @@ -34,10 +34,12 @@ describe('esm', () => { for (const variant of varySandbox.VARIANTS) { it(`is instrumented loaded with ${variant}`, async () => { - proc = await spawnPluginIntegrationTestProc( + proc = await spawnPluginIntegrationTestProcAndExpectExit( sandboxCwd(), variants[variant], agent.port, + undefined, + undefined, (data) => { const jsonObject = JSON.parse(data.toString()) assert.ok(Object.hasOwn(jsonObject, 'dd')) diff --git a/packages/datadog-plugin-prisma/test/index.spec.js b/packages/datadog-plugin-prisma/test/index.spec.js index 969ee6d5e34..a333bd858cf 100644 --- a/packages/datadog-plugin-prisma/test/index.spec.js +++ b/packages/datadog-plugin-prisma/test/index.spec.js @@ -4,300 +4,431 @@ 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 semifies = require('semifies') - const { assertObjectContains } = require('../../../integration-tests/helpers') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withVersions } = require('../../dd-trace/test/setup/mocha') const { expectedSchema, rawExpectedSchema } = require('./naming') + +const { + PRISMA_CLIENT_OUTPUT_RELATIVE, + SCHEMA_FIXTURES, + TEST_DATABASE_URL +} = require('./prisma-fixtures') + +function execPrismaGenerate (config, cwd) { + if (config.ts) { + const outDir = config.v7 ? '../v7/dist' : '../dist' + execSync([ + './node_modules/.bin/prisma generate', + [ + './node_modules/.bin/tsc ../generated/**/*.ts', + `--outDir ${outDir}`, + '--target esnext', + '--module commonjs', + '--allowJs true', + '--moduleResolution node' + ].join(' ') + ].join(' && '), { + cwd, + stdio: 'inherit' + }) + } else { + execSync('./node_modules/.bin/prisma generate', { + cwd, + stdio: 'inherit' + }) + } +} + +function loadPrismaModule (config, range) { + if (config.file.includes('generated')) { + return require(config.file) + } + + const file = config.file.replace('range', range) + const prismaModule = require(file) + return config.ts ? prismaModule : prismaModule.get() +} + +function clearPrismaEnv () { + delete process.env.PRISMA_CLIENT_OUTPUT + delete process.env.DATABASE_URL +} + +function setGeneratedClientEnv () { + process.env.PRISMA_CLIENT_OUTPUT = PRISMA_CLIENT_OUTPUT_RELATIVE + process.env.DATABASE_URL = TEST_DATABASE_URL +} + +async function copySchemaToVersionDir (schemaPath, range) { + const cwd = path.resolve(__dirname, `../../../versions/@prisma/client@${range}`) + await fs.cp( + path.resolve(__dirname, schemaPath), + path.join(cwd, 'schema.prisma') + ) + return cwd +} + +function createPrismaClient (prisma, config) { + // With the introduction of v7 prisma now enforces the use of adpaters + if (config.v7) { + const { PrismaPg } = require('@prisma/adapter-pg') + const adapter = new PrismaPg({ connectionString: `${process.env.DATABASE_URL}` }) + return new prisma.PrismaClient({ adapter }) + } + return new prisma.PrismaClient() +} + describe('Plugin', () => { let prisma let prismaClient let tracingHelper describe('prisma', () => { - // Prisma 7.0.0+ is not supported in Node.js < 20.19.0 - const supportedRange = semifies(process.version, '>=20.19.0') ? '*' : '>=7.0.0' - - withVersions('prisma', ['@prisma/client'], supportedRange, async (range, _moduleName_, version) => { - describe('without configuration', () => { - before(async () => { - const cwd = path.resolve(__dirname, `../../../versions/@prisma/client@${range}`) - await fs.cp( - path.resolve(__dirname, './schema.prisma'), - cwd + '/schema.prisma', - ) - await agent.load('prisma') - execSync('./node_modules/.bin/prisma generate', { - cwd, // Ensure the current working directory is where the schema is located - stdio: 'inherit' + const prismaClients = [{ + schema: `./${SCHEMA_FIXTURES.clientOutputJs}`, + file: '../../../versions/@prisma/generated/prisma', + usesGeneratedClientOutput: true + }, + { + schema: `./${SCHEMA_FIXTURES.clientJs}`, + file: '../../../versions/@prisma/client@range' + }, + { + schema: `./${SCHEMA_FIXTURES.tsCjsV6}`, + file: '../../../versions/@prisma/dist/client.js', + usesGeneratedClientOutput: true, + ts: true + }, + { + schema: `./${SCHEMA_FIXTURES.tsCjsV7}`, + file: '../../../versions/@prisma/v7/dist/client.js', + usesGeneratedClientOutput: true, + ts: true, + v7: true + }] + + prismaClients.forEach(config => { + // Prisma 7.0.0+ is not supported in Node.js < 20.19.0 + if (config.v7 && !semifies(process.version.slice(1), '>=20.19.0')) return + + let supportedRange = config.v7 ? '>=7.0.0' : '<7.0.0' + // prisma-generator is only available starting prisma >= 6.16.0 + if (config.ts && supportedRange === '<7.0.0') { + supportedRange = '>=6.16.0 <7.0.0' + } + withVersions('prisma', ['@prisma/client'], supportedRange, async (range, _moduleName_, version) => { + describe(`without configuration ${config.schema}`, () => { + before(async function () { + this.timeout(10000) + clearPrismaEnv() + if (config.usesGeneratedClientOutput) setGeneratedClientEnv() + + const cwd = await copySchemaToVersionDir(config.schema, range) + + await agent.load(['prisma', 'pg']) + execPrismaGenerate(config, cwd) + prisma = loadPrismaModule(config, range) + + prismaClient = createPrismaClient(prisma, config) + + tracingHelper = prismaClient._tracingHelper + + if (!tracingHelper) { + throw new Error('Prisma tracing helper was not initialized') + } }) - prisma = require(`../../../versions/@prisma/client@${range}`).get() - prismaClient = new prisma.PrismaClient() - const matched = version.match(/(\d+)\.\d+\.\d+$/) - const majorVersion = matched[1] - tracingHelper = global.PRISMA_INSTRUMENTATION?.helper || - global[`V${majorVersion}_PRISMA_INSTRUMENTATION`]?.helper - }) - - after(() => { - return agent.close({ ritmReset: false }) - }) - it('should do automatic instrumentation', async () => { - const tracingPromise = agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].resource, 'queryRaw') - assert.strictEqual(traces[0][0].meta['prisma.type'], 'client') - assert.strictEqual(traces[0][0].meta['prisma.method'], 'queryRaw') - assert.strictEqual(traces[0][0].name, expectedSchema.client.opName) - assert.strictEqual(traces[0][0].service, expectedSchema.client.serviceName) - - // grabbing actual db query span - const engineDBSpan = traces[0].find(span => span.meta['prisma.name'] === 'db_query') - assert.strictEqual(engineDBSpan.resource, 'SELECT 1') - assert.strictEqual(engineDBSpan.type, 'sql') - assert.strictEqual(engineDBSpan.meta['span.kind'], 'client') - assert.strictEqual(engineDBSpan.name, expectedSchema.engine.opName) - assert.strictEqual(engineDBSpan.service, expectedSchema.engine.serviceName) + after(() => { + return agent.close({ ritmReset: false }) }) - await Promise.all([ - prismaClient.$queryRaw`SELECT 1`, - tracingPromise - ]) - }) - - it('should handle errors', async () => { - let error - const tracingPromise = agent.assertFirstTraceSpan((trace) => { - assertObjectContains(trace, { - meta: { - [ERROR_TYPE]: error.name, - [ERROR_MESSAGE]: error.message, - [ERROR_STACK]: error.stack, - } + it('should do automatic instrumentation', async () => { + const tracingPromise = agent.assertSomeTraces(traces => { + assertObjectContains(traces, [[{ + resource: 'queryRaw', + meta: { + 'prisma.type': 'client', + 'prisma.method': 'queryRaw' + }, + name: expectedSchema.client.opName, + service: expectedSchema.client.serviceName + }, + { + resource: 'SELECT 1', + type: 'sql', + meta: { + 'span.kind': 'client' + }, + name: config.v7 ? 'pg.query' : expectedSchema.engine.opName, + service: config.v7 ? 'test-postgres' : expectedSchema.engine.serviceName + }]]) }) - }) - await Promise.all([ - // This will throw an error because no data object is provided - prismaClient.User.create({}).catch(e => { - error = e - }), - tracingPromise - ]) - }) - it('should create client spans from callback', async () => { - const tracingPromise = agent.assertFirstTraceSpan({ - name: 'prisma.client', - resource: 'users.findMany', - meta: { - 'prisma.type': 'client', - 'prisma.method': 'findMany', - 'prisma.model': 'users', - } + await Promise.all([ + prismaClient.$queryRaw`SELECT 1`, + tracingPromise + ]) }) - tracingHelper.runInChildSpan( - { - name: 'operation', - attributes: { method: 'findMany', model: 'users' } - }, - () => { - return 'Test Function' - } - ) - - await Promise.all([ - tracingPromise - ]) - }) - - it('should generate engine span from array of spans', async () => { - const tracingPromise = agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0].length, 2) - assert.strictEqual(traces[0][0].span_id, traces[0][1].parent_id) - assert.strictEqual(traces[0][0].name, 'prisma.engine') - assert.strictEqual(traces[0][0].resource, 'query') - assert.strictEqual(traces[0][0].meta['prisma.type'], 'engine') - assert.strictEqual(traces[0][0].meta['prisma.name'], 'query') - assert.strictEqual(traces[0][1].name, 'prisma.engine') - assert.strictEqual(traces[0][1].resource, 'SELECT 1') - assert.strictEqual(traces[0][1].type, 'sql') - assert.strictEqual(traces[0][1].meta['prisma.type'], 'engine') - assert.strictEqual(traces[0][1].meta['prisma.name'], 'db_query') - assert.strictEqual(traces[0][1].meta['db.type'], 'postgres') + it('should handle errors', async () => { + let error + const tracingPromise = agent.assertFirstTraceSpan((trace) => { + assertObjectContains(trace, { + meta: { + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack, + } + }) + }) + await Promise.all([ + // This will throw an error because no data object is provided + prismaClient.User.create({}).catch(e => { + error = e + }), + tracingPromise + ]) }) - const engineSpans = [ - { - id: '1', - parentId: null, - name: 'prisma:engine:query', - startTime: [1745340876, 436692000], - endTime: [1745340876, 438653250], - kind: 'internal' - }, - { - id: '2', - parentId: '1', - name: 'prisma:engine:db_query', - startTime: [1745340876, 436861000], - endTime: [1745340876, 438601541], - kind: 'client', - attributes: { - 'db.system': 'postgresql', - 'db.query.text': 'SELECT 1' + it('should create client spans from callback', async () => { + const tracingPromise = agent.assertFirstTraceSpan({ + name: 'prisma.client', + resource: 'users.findMany', + meta: { + 'prisma.type': 'client', + 'prisma.method': 'findMany', + 'prisma.model': 'users', } - } - ] - tracingHelper.dispatchEngineSpans(engineSpans) - await Promise.all([ - tracingPromise - ]) - }) - - it('should include database connection attributes in db_query spans', async () => { - // Set up database config that should be parsed from connection URL - const dbConfig = { - user: 'foo', - host: 'localhost', - port: '5432', - database: 'postgres' - } - tracingHelper.setDbString(dbConfig) - - const tracingPromise = agent.assertSomeTraces(traces => { - // Find the db_query span - const dbQuerySpan = traces[0].find(span => span.meta['prisma.name'] === 'db_query') - assert.ok(dbQuerySpan != null) - - // Verify database connection attributes are present - assert.strictEqual(dbQuerySpan.meta['db.name'], 'postgres') - assert.strictEqual(dbQuerySpan.meta['db.user'], 'foo') - assert.strictEqual(dbQuerySpan.meta['out.host'], 'localhost') - assert.strictEqual(dbQuerySpan.meta['network.destination.port'], '5432') - assert.strictEqual(dbQuerySpan.meta['db.type'], 'postgres') - }) + }) - 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' + tracingHelper.runInChildSpan( + { + name: 'operation', + attributes: { method: 'findMany', model: 'users' } + }, + () => { + return 'Test Function' } - } - ] - tracingHelper.dispatchEngineSpans(engineSpans) - await Promise.all([ - tracingPromise - ]) - }) - }) - - describe('with configuration', () => { - describe('with custom service name', () => { - before(() => { - const config = { - service: 'custom' - } - return agent.load('prisma', config) - }) + ) - after(() => { return agent.close({ ritmReset: false }) }) - - beforeEach(() => { - prisma = require(`../../../versions/@prisma/client@${range}`).get() - prismaClient = new prisma.PrismaClient() + await Promise.all([ + tracingPromise + ]) }) - it('should be configured with the correct values', async () => { - const tracingPromise = agent.assertFirstTraceSpan({ - service: 'custom' + it('should generate engine span from array of spans', async () => { + const tracingPromise = agent.assertSomeTraces(traces => { + assert.strictEqual(traces[0].length, 2) + assert.strictEqual(traces[0][0].span_id, traces[0][1].parent_id) + assert.strictEqual(traces[0][0].name, 'prisma.engine') + assert.strictEqual(traces[0][0].resource, 'query') + assert.strictEqual(traces[0][0].meta['prisma.type'], 'engine') + assert.strictEqual(traces[0][0].meta['prisma.name'], 'query') + assert.strictEqual(traces[0][1].name, 'prisma.engine') + assert.strictEqual(traces[0][1].resource, 'SELECT 1') + assert.strictEqual(traces[0][1].type, 'sql') + assert.strictEqual(traces[0][1].meta['prisma.type'], 'engine') + assert.strictEqual(traces[0][1].meta['prisma.name'], 'db_query') + assert.strictEqual(traces[0][1].meta['db.type'], 'postgres') }) + const engineSpans = [ + { + id: '1', + parentId: null, + name: 'prisma:engine:query', + startTime: [1745340876, 436692000], + endTime: [1745340876, 438653250], + kind: 'internal' + }, + { + id: '2', + parentId: '1', + name: 'prisma:engine:db_query', + startTime: [1745340876, 436861000], + endTime: [1745340876, 438601541], + kind: 'client', + attributes: { + 'db.system': 'postgresql', + 'db.query.text': 'SELECT 1' + } + } + ] + tracingHelper.dispatchEngineSpans(engineSpans) await Promise.all([ - prismaClient.$queryRaw`SELECT 1`, tracingPromise ]) }) - }) - describe('with prisma client disabled', () => { - before(() => { - const config = { - client: false + it('should include database connection attributes in db_query spans', async () => { + // Set up database config that should be parsed from connection URL + const dbConfig = { + user: 'foo', + host: 'localhost', + port: '5432', + database: 'postgres' } - return agent.load('prisma', config) - }) - - after(() => { return agent.close({ ritmReset: false }) }) - - beforeEach(() => { - prisma = require(`../../../versions/@prisma/client@${range}`).get() - prismaClient = new prisma.PrismaClient() - }) + tracingHelper.setDbString(dbConfig) - it('should disable prisma client', async () => { const tracingPromise = agent.assertSomeTraces(traces => { - const clientSpans = traces[0].find(span => span.meta['prisma.type'] === 'client') - assert.ok(clientSpans == null) + // Find the db_query span + const dbQuerySpan = traces[0].find(span => span.meta['prisma.name'] === 'db_query') + // Verify database connection attributes are present + assertObjectContains(dbQuerySpan, { + meta: { + 'db.name': 'postgres', + 'db.user': 'foo', + 'out.host': 'localhost', + 'network.destination.port': '5432', + 'db.type': 'postgres' + } + }) }) + 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' + } + } + ] + tracingHelper.dispatchEngineSpans(engineSpans) await Promise.all([ - prismaClient.$queryRaw`SELECT 1`, tracingPromise ]) }) - - withNamingSchema( - done => prismaClient.$queryRaw`SELECT 1`.catch(done), - rawExpectedSchema.engine, - { desc: 'Prisma Engine' } - ) }) - describe('with prisma engine disabled', () => { - before(() => { - const config = { - engine: false - } - return agent.load('prisma', config) - }) + describe('with configuration', () => { + describe('with custom service name', () => { + before(async function () { + this.timeout(10000) + clearPrismaEnv() + if (config.usesGeneratedClientOutput) setGeneratedClientEnv() + + const cwd = await copySchemaToVersionDir(config.schema, range) - after(() => { return agent.close({ ritmReset: false }) }) + execPrismaGenerate(config, cwd) - beforeEach(() => { - prisma = require(`../../../versions/@prisma/client@${range}`).get() - prismaClient = new prisma.PrismaClient() + const pluginConfig = { + service: 'custom' + } + return agent.load(['prisma', 'pg'], pluginConfig) + }) + + after(() => { return agent.close({ ritmReset: false }) }) + + beforeEach(() => { + prisma = loadPrismaModule(config, range) + prismaClient = createPrismaClient(prisma, config) + }) + + it('should be configured with the correct values', async () => { + const tracingPromise = agent.assertFirstTraceSpan({ + service: 'custom' + }) + + await Promise.all([ + prismaClient.$queryRaw`SELECT 1`, + tracingPromise + ]) + }) }) - it('should disable prisma engine', async () => { - const tracingPromise = agent.assertSomeTraces(traces => { - const engineSpans = traces[0].find(span => span.meta['prisma.type'] === 'engine') - assert.ok(engineSpans == null) + describe('with prisma client disabled', () => { + before(async function () { + this.timeout(10000) + clearPrismaEnv() + if (config.usesGeneratedClientOutput) setGeneratedClientEnv() + + const cwd = await copySchemaToVersionDir(config.schema, range) + + execPrismaGenerate(config, cwd) + + const pluginConfig = { + client: false + } + return agent.load(['prisma', 'pg'], pluginConfig) }) - await Promise.all([ - prismaClient.$queryRaw`SELECT 1`, - tracingPromise - ]) + after(() => { return agent.close({ ritmReset: false }) }) + + beforeEach(() => { + prisma = loadPrismaModule(config, range) + prismaClient = createPrismaClient(prisma, config) + }) + + it('should disable prisma client', async () => { + const tracingPromise = agent.assertSomeTraces(traces => { + const clientSpans = traces[0].find(span => span.meta['prisma.type'] === 'client') + assert.ok(clientSpans == null) + }) + + await Promise.all([ + prismaClient.$queryRaw`SELECT 1`, + tracingPromise + ]) + }) + + withNamingSchema( + done => prismaClient.$queryRaw`SELECT 1`.catch(done), + config.v7 ? 'pg.query' : rawExpectedSchema.engine, + { desc: 'Prisma Engine' } + ) }) - withNamingSchema( - done => prismaClient.$queryRaw`SELECT 1`.catch(done), - rawExpectedSchema.client, - { desc: 'Prisma Client' } - ) + describe('with prisma engine disabled', () => { + before(async function () { + this.timeout(10000) + clearPrismaEnv() + if (config.usesGeneratedClientOutput) setGeneratedClientEnv() + + const cwd = await copySchemaToVersionDir(config.schema, range) + + execPrismaGenerate(config, cwd) + + const pluginConfig = { + engine: false + } + return agent.load(['prisma', 'pg'], pluginConfig) + }) + + after(() => { return agent.close({ ritmReset: false }) }) + + beforeEach(() => { + prisma = loadPrismaModule(config, range) + prismaClient = createPrismaClient(prisma, config) + }) + + it('should disable prisma engine', async () => { + const tracingPromise = agent.assertSomeTraces(traces => { + const engineSpans = traces[0].find(span => span.meta['prisma.type'] === 'engine') + assert.ok(engineSpans == null) + }) + + await Promise.all([ + prismaClient.$queryRaw`SELECT 1`, + tracingPromise + ]) + }) + + withNamingSchema( + done => prismaClient.$queryRaw`SELECT 1`.catch(done), + rawExpectedSchema.client, + { desc: 'Prisma Client' } + ) + }) }) }) }) 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 181191fd4a4..bef5c5e78ad 100644 --- a/packages/datadog-plugin-prisma/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-prisma/test/integration-test/client.spec.js @@ -5,84 +5,163 @@ const { execSync } = require('node:child_process') const { describe, it, beforeEach, afterEach } = require('mocha') +const semifies = require('semifies') +const semver = require('semver') const { FakeAgent, sandboxCwd, useSandbox, - spawnPluginIntegrationTestProc, + spawnPluginIntegrationTestProcAndExpectExit, assertObjectContains, varySandbox } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') +const { SCHEMA_FIXTURES, TEST_DATABASE_URL } = require('../prisma-fixtures') + +const prismaClientConfigs = [{ + name: 'prisma-generator-js with no output', + schema: `./packages/datadog-plugin-prisma/test/${SCHEMA_FIXTURES.clientJs}`, + serverFile: 'server.mjs', + importPath: '@prisma/client' +}, +{ + name: 'prisma-generator-js with custom output', + serverFile: 'server-output.mjs', + importPath: './generated/prisma/index.js', + schema: `./packages/datadog-plugin-prisma/test/${SCHEMA_FIXTURES.clientOutputJs}`, + env: { PRISMA_CLIENT_OUTPUT: './generated/prisma' } +}, +{ + name: 'prisma-generator v6', + serverFile: 'server-ts-v6.mjs', + importPath: './dist/client.js', + schema: `./packages/datadog-plugin-prisma/test/${SCHEMA_FIXTURES.tsEsmV6}`, + env: { PRISMA_CLIENT_OUTPUT: './generated/prisma', DATABASE_URL: TEST_DATABASE_URL }, + ts: true +}, +{ + name: 'prisma-generator v7', + serverFile: 'server-ts-v7.mjs', + importPath: './dist/client.js', + 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 +}] describe('esm', () => { let agent let proc - let variants - - withVersions('prisma', '@prisma/client', version => { - useSandbox([`'prisma@${version}'`, `'@prisma/client@${version}'`], false, [ - './packages/datadog-plugin-prisma/test/integration-test/*', - './packages/datadog-plugin-prisma/test/schema.prisma' - ]) - - beforeEach(async function () { - this.timeout(60000) - agent = await new FakeAgent().start() - execSync( - './node_modules/.bin/prisma migrate reset --force && ' + - './node_modules/.bin/prisma db push --accept-data-loss && ' + - './node_modules/.bin/prisma generate', - { - cwd: sandboxCwd(), // Ensure the current working directory is where the schema is located - stdio: 'inherit' - } - ) - }) + prismaClientConfigs.forEach(config => { + describe(config.name, () => { + const isNodeSupported = semifies(semver.clean(process.version), '>=20.19.0') + const isPrismaV7 = config.configFile + if (config.configFile && !isNodeSupported) { + return + } - before(function () { - variants = varySandbox('server.mjs', '@prisma/client', 'PrismaClient') - }) + const supportedRange = config.configFile ? '>=7.0.0' : '<7.0.0' - afterEach(async () => { - proc?.kill() - await agent.stop() - }) + withVersions('prisma', '@prisma/client', supportedRange, version => { + if (config.ts && version === '6.1.0') return + let variants + const paths = ['./packages/datadog-plugin-prisma/test/integration-test/*', config.schema] + + if (isPrismaV7) paths.push(config.configFile) + + const deps = [`prisma@${version}`, `@prisma/client@${version}`, 'typescript'] + if (isPrismaV7) deps.push('@prisma/adapter-pg') + useSandbox(deps, false, paths) + + before(function () { + variants = varySandbox(config.serverFile, config.importPath, 'PrismaClient') + }) + + beforeEach(async function () { + this.timeout(60000) + agent = await new FakeAgent().start() + const commands = [ + './node_modules/.bin/prisma migrate reset --force', + './node_modules/.bin/prisma db push --accept-data-loss', + './node_modules/.bin/prisma generate' + ] + const cwd = sandboxCwd() - for (const variant of varySandbox.VARIANTS) { - it('is instrumented', async function () { - this.timeout(60000) - 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', - }], [{ - name: 'prisma.engine', - service: 'node-prisma', - meta: { - 'db.user': 'postgres', - 'db.name': 'postgres', - 'db.type': 'postgres', + if (config.ts) { + commands.push( + './node_modules/.bin/tsc ./generated/**/*.ts' + + ' --outDir ./dist' + + ' --target ES2023' + + ' --module ESNext' + + ' --strict true' + + ' --moduleResolution node' + + ' --esModuleInterop true' + ) + } + + execSync(commands.join(' && '), { + cwd, + stdio: 'inherit', + env: { + ...process.env, + ...config.env } - }]]) + }) + + // node v18 needs the package.json to have type module to treat .js files as esm + if (config.ts && config.name.includes('v6')) { + const fs = require('fs') + const path = require('path') + const distPath = path.join(cwd, 'dist') + try { + fs.mkdirSync(distPath, { recursive: true }) + } catch {} + const distPkgJsonPath = path.join(distPath, 'package.json') + fs.writeFileSync(distPkgJsonPath, JSON.stringify({ type: 'module' }, null, 2)) + } }) - // TODO: Integrate the assertions into the spawn command by adding a - // callback. It should end the process when the assertions are met. That - // way we can remove the Promise.all and the procPromise.then(). - const procPromise = spawnPluginIntegrationTestProc(sandboxCwd(), variants[variant], agent.port, { - DD_TRACE_FLUSH_INTERVAL: '2000' + afterEach(async () => { + proc?.kill() + await agent.stop() }) - await Promise.all([ - procPromise.then((res) => { - proc = res - }), - res - ]) + for (const variant of varySandbox.VARIANTS) { + it(`is instrumented with ${variant} import`, async function () { + this.timeout(60000) + 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' + }], [{ + 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 procPromise = spawnPluginIntegrationTestProcAndExpectExit( + sandboxCwd(), + variants[variant], + agent.port, + { DD_TRACE_FLUSH_INTERVAL: '2000', ...config.env } + ) + + await Promise.all([ + procPromise.then((res) => { + proc = res + }), + res + ]) + }) + } }) - } + }) }) }) diff --git a/packages/datadog-plugin-prisma/test/integration-test/server-output.mjs b/packages/datadog-plugin-prisma/test/integration-test/server-output.mjs new file mode 100644 index 00000000000..5063bab8710 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/integration-test/server-output.mjs @@ -0,0 +1,16 @@ +import 'dd-trace/init.js' +import { PrismaClient } from './generated/prisma/index.js' + +const prismaClient = new PrismaClient() +const user = await prismaClient.user.create({ + data: { + name: 'John Doe', + email: 'john.doe@datadoghq.com' + } +}) + +await prismaClient.user.findUnique({ + where: { + id: user.id + } +}) diff --git a/packages/datadog-plugin-prisma/test/integration-test/server-ts-v6.mjs b/packages/datadog-plugin-prisma/test/integration-test/server-ts-v6.mjs new file mode 100644 index 00000000000..ed94c56bc3b --- /dev/null +++ b/packages/datadog-plugin-prisma/test/integration-test/server-ts-v6.mjs @@ -0,0 +1,16 @@ +import 'dd-trace/init.js' +import { PrismaClient } from './dist/client.js' + +const prismaClient = new PrismaClient() +const user = await prismaClient.user.create({ + data: { + name: 'John Doe', + email: 'john.doe@datadoghq.com' + } +}) + +await prismaClient.user.findUnique({ + where: { + id: user.id + } +}) diff --git a/packages/datadog-plugin-prisma/test/integration-test/server-ts-v7.mjs b/packages/datadog-plugin-prisma/test/integration-test/server-ts-v7.mjs new file mode 100644 index 00000000000..9b4f86e20e6 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/integration-test/server-ts-v7.mjs @@ -0,0 +1,18 @@ +import 'dd-trace/init.js' +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from './dist/client.js' + +const adapter = new PrismaPg({ connectionString: `${process.env.DATABASE_URL}` }) +const prismaClient = new PrismaClient({ adapter }) +const user = await prismaClient.user.create({ + data: { + name: 'John Doe', + email: 'john.doe@datadoghq.com' + } +}) + +await prismaClient.user.findUnique({ + where: { + id: user.id + } +}) diff --git a/packages/datadog-plugin-prisma/test/prisma-fixtures.js b/packages/datadog-plugin-prisma/test/prisma-fixtures.js new file mode 100644 index 00000000000..cbaca59763d --- /dev/null +++ b/packages/datadog-plugin-prisma/test/prisma-fixtures.js @@ -0,0 +1,23 @@ +'use strict' + +// Shared test constants for prisma plugin tests. + +const TEST_DATABASE_URL = 'postgres://postgres:postgres@localhost:5432/postgres' + +const PRISMA_CLIENT_OUTPUT_RELATIVE = '../generated/prisma' + +const SCHEMA_FIXTURES = { + clientJs: 'provider-prisma-client-js/schema.prisma', + clientOutputJs: 'provider-prisma-client-js/output/schema.prisma', + tsCjsV6: 'provider-prisma-client-ts/cjs/v6/schema.prisma', + tsCjsV7: 'provider-prisma-client-ts/cjs/v7/schema.prisma', + tsEsmV6: 'provider-prisma-client-ts/esm/v6/schema.prisma', + tsEsmV7: 'provider-prisma-client-ts/esm/v7/schema.prisma', + tsEsmV7Config: 'provider-prisma-client-ts/esm/v7/prisma.config.ts' +} + +module.exports = { + PRISMA_CLIENT_OUTPUT_RELATIVE, + SCHEMA_FIXTURES, + TEST_DATABASE_URL +} diff --git a/packages/datadog-plugin-prisma/test/provider-prisma-client-js/output/schema.prisma b/packages/datadog-plugin-prisma/test/provider-prisma-client-js/output/schema.prisma new file mode 100644 index 00000000000..e8cc7c919af --- /dev/null +++ b/packages/datadog-plugin-prisma/test/provider-prisma-client-js/output/schema.prisma @@ -0,0 +1,15 @@ +generator client { + provider = "prisma-client-js" + output = env("PRISMA_CLIENT_OUTPUT") +} + +datasource db { + provider = "postgresql" + url = "postgres://postgres:postgres@localhost:5432/postgres" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/packages/datadog-plugin-prisma/test/schema.prisma b/packages/datadog-plugin-prisma/test/provider-prisma-client-js/schema.prisma similarity index 100% rename from packages/datadog-plugin-prisma/test/schema.prisma rename to packages/datadog-plugin-prisma/test/provider-prisma-client-js/schema.prisma diff --git a/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/cjs/v6/schema.prisma b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/cjs/v6/schema.prisma new file mode 100644 index 00000000000..5c0ed53b694 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/cjs/v6/schema.prisma @@ -0,0 +1,17 @@ +generator client { + provider = "prisma-client" + output = env("PRISMA_CLIENT_OUTPUT") + moduleFormat = "cjs" + importFileExtension = "js" +} + +datasource db { + provider = "postgresql" + url = "postgres://postgres:postgres@localhost:5432/postgres" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/cjs/v7/schema.prisma b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/cjs/v7/schema.prisma new file mode 100644 index 00000000000..cc61ffbfdb4 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/cjs/v7/schema.prisma @@ -0,0 +1,16 @@ +generator client { + provider = "prisma-client" + output = env("PRISMA_CLIENT_OUTPUT") + moduleFormat = "cjs" + importFileExtension = "js" +} + +datasource db { + provider = "postgresql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v6/schema.prisma b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v6/schema.prisma new file mode 100644 index 00000000000..4b7f2804c9d --- /dev/null +++ b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v6/schema.prisma @@ -0,0 +1,17 @@ +generator client { + provider = "prisma-client" + output = env("PRISMA_CLIENT_OUTPUT") + moduleFormat = "esm" + importFileExtension = "js" +} + +datasource db { + provider = "postgresql" + url = "postgres://postgres:postgres@localhost:5432/postgres" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v7/prisma.config.ts b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v7/prisma.config.ts new file mode 100644 index 00000000000..fa1714d1a16 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v7/prisma.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "prisma/config" + +export default defineConfig({ + datasource: { + url: "postgres://postgres:postgres@localhost:5432/postgres" + } +}) diff --git a/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v7/schema.prisma b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v7/schema.prisma new file mode 100644 index 00000000000..6a973112d59 --- /dev/null +++ b/packages/datadog-plugin-prisma/test/provider-prisma-client-ts/esm/v7/schema.prisma @@ -0,0 +1,16 @@ +generator client { + provider = "prisma-client" + output = env("PRISMA_CLIENT_OUTPUT") + moduleFormat = "esm" + importFileExtension = "js" +} + +datasource db { + provider = "postgresql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} diff --git a/packages/datadog-plugin-process/test/integration-test/client.spec.js b/packages/datadog-plugin-process/test/integration-test/client.spec.js index 22962ead972..99db6940295 100644 --- a/packages/datadog-plugin-process/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-process/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { useSandbox, sandboxCwd, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') describe('ESM', () => { let variants, proc, agent diff --git a/packages/datadog-plugin-process/test/integration-test/server.mjs b/packages/datadog-plugin-process/test/integration-test/server.mjs index 14ae24dbbec..d2438cd5643 100644 --- a/packages/datadog-plugin-process/test/integration-test/server.mjs +++ b/packages/datadog-plugin-process/test/integration-test/server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import express from 'express' import process from 'node:process' +import express from 'express' import dc from 'dc-polyfill' const startCh = dc.channel('datadog:process:setUncaughtExceptionCaptureCallback:start') diff --git a/packages/datadog-plugin-protobufjs/test/index.spec.js b/packages/datadog-plugin-protobufjs/test/index.spec.js index f5755aa19ef..cf7c4752373 100644 --- a/packages/datadog-plugin-protobufjs/test/index.spec.js +++ b/packages/datadog-plugin-protobufjs/test/index.spec.js @@ -4,6 +4,7 @@ const fs = require('fs') const assert = require('node:assert/strict') const path = require('path') +const sinon = require('sinon') const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') const { @@ -14,9 +15,8 @@ const { SCHEMA_WEIGHT, SCHEMA_TYPE } = require('../../dd-trace/src/constants') -const sinon = require('sinon') -const { loadMessage } = require('./helpers') const { SchemaBuilder } = require('../../dd-trace/src/datastreams/schemas/schema_builder') +const { loadMessage } = require('./helpers') const schemas = JSON.parse(fs.readFileSync(path.join(__dirname, 'schemas/expected_schemas.json'), 'utf8')) const MESSAGE_SCHEMA_DEF = schemas.MESSAGE_SCHEMA_DEF diff --git a/packages/datadog-plugin-pug/test/integration-test/client.spec.js b/packages/datadog-plugin-pug/test/integration-test/client.spec.js index aa9fb430ff2..a63ffd1e70a 100644 --- a/packages/datadog-plugin-pug/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-pug/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('pug', 'pug', version => { diff --git a/packages/datadog-plugin-redis/test/integration-test/client.spec.js b/packages/datadog-plugin-redis/test/integration-test/client.spec.js index 1437f6286a2..b00adffc938 100644 --- a/packages/datadog-plugin-redis/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-redis/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -35,7 +35,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'redis.command'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-rhea/src/index.js b/packages/datadog-plugin-rhea/src/index.js index 234932e1c01..dd69a9aa906 100644 --- a/packages/datadog-plugin-rhea/src/index.js +++ b/packages/datadog-plugin-rhea/src/index.js @@ -1,8 +1,8 @@ 'use strict' +const CompositePlugin = require('../../dd-trace/src/plugins/composite') const ProducerPlugin = require('./producer') const ConsumerPlugin = require('./consumer') -const CompositePlugin = require('../../dd-trace/src/plugins/composite') class RheaPlugin extends CompositePlugin { static id = 'rhea' diff --git a/packages/datadog-plugin-rhea/test/index.spec.js b/packages/datadog-plugin-rhea/test/index.spec.js index 9f9c1727b5c..402e31227c4 100644 --- a/packages/datadog-plugin-rhea/test/index.spec.js +++ b/packages/datadog-plugin-rhea/test/index.spec.js @@ -8,8 +8,8 @@ const sinon = require('sinon') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') const { withNamingSchema, withPeerService, withVersions } = require('../../dd-trace/test/setup/mocha') -const { expectedSchema, rawExpectedSchema } = require('./naming') const { assertObjectContains } = require('../../../integration-tests/helpers') +const { expectedSchema, rawExpectedSchema } = require('./naming') describe('Plugin', () => { let tracer @@ -449,13 +449,16 @@ describe('Plugin', () => { agent.assertSomeTraces(traces => { const span = traces[0][0] - assert.strictEqual(span.error, 1) - assertObjectContains(span.meta, { - [ERROR_MESSAGE]: 'this is an error', - [ERROR_TYPE]: 'Error', - [ERROR_STACK]: error.stack, - component: 'rhea' + assertObjectContains(span, { + error: 1, + meta: { + [ERROR_MESSAGE]: 'this is an error', + [ERROR_TYPE]: 'Error', + [ERROR_STACK]: error.stack, + component: 'rhea' + } }) + Session.prototype.on_transfer = onTransfer }).then(done, done) diff --git a/packages/datadog-plugin-rhea/test/integration-test/client.spec.js b/packages/datadog-plugin-rhea/test/integration-test/client.spec.js index a6ded49c956..73653402869 100644 --- a/packages/datadog-plugin-rhea/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-rhea/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -34,7 +34,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'amqp.send'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-router/test/integration-test/server.mjs b/packages/datadog-plugin-router/test/integration-test/server.mjs index f2ba8497d9d..f3f869b7bb0 100644 --- a/packages/datadog-plugin-router/test/integration-test/server.mjs +++ b/packages/datadog-plugin-router/test/integration-test/server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import router from 'router' import http from 'http' +import router from 'router' const app = router() diff --git a/packages/datadog-plugin-sequelize/test/integration-test/client.spec.js b/packages/datadog-plugin-sequelize/test/integration-test/client.spec.js index e4b953d938e..0587ed0fba2 100644 --- a/packages/datadog-plugin-sequelize/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-sequelize/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { sandboxCwd, useSandbox, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') const { withVersions } = require('../../../dd-trace/test/setup/mocha') withVersions('sequelize', 'sequelize', version => { diff --git a/packages/datadog-plugin-sharedb/test/integration-test/client.spec.js b/packages/datadog-plugin-sharedb/test/integration-test/client.spec.js index 1ad3e4cd221..e37a753d876 100644 --- a/packages/datadog-plugin-sharedb/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-sharedb/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') describe('esm', () => { @@ -35,7 +35,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'sharedb.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-tedious/test/integration-test/client.spec.js b/packages/datadog-plugin-tedious/test/integration-test/client.spec.js index 6cd38b5bb21..64959a18991 100644 --- a/packages/datadog-plugin-tedious/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-tedious/test/integration-test/client.spec.js @@ -7,7 +7,7 @@ const { sandboxCwd, useSandbox, checkSpansForServiceName, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const version = require('../../../../version.js') const { withVersions } = require('../../../dd-trace/test/setup/mocha') @@ -42,7 +42,7 @@ describe('esm', () => { assert.strictEqual(checkSpansForServiceName(payload, 'tedious.request'), true) }) - proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port) + proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), 'server.mjs', agent.port) await res }).timeout(20000) diff --git a/packages/datadog-plugin-url/test/integration-test/client.spec.js b/packages/datadog-plugin-url/test/integration-test/client.spec.js index cef56e50110..8c5d973c2bd 100644 --- a/packages/datadog-plugin-url/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-url/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { useSandbox, sandboxCwd, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') describe('ESM', () => { let variants, proc, agent diff --git a/packages/datadog-plugin-url/test/integration-test/server.mjs b/packages/datadog-plugin-url/test/integration-test/server.mjs index 04c17d7646b..73f0b7f1deb 100644 --- a/packages/datadog-plugin-url/test/integration-test/server.mjs +++ b/packages/datadog-plugin-url/test/integration-test/server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import express from 'express' import { URL } from 'node:url' +import express from 'express' import dc from 'dc-polyfill' const parseFinishChannel = dc.channel('datadog:url:parse:finish') diff --git a/packages/datadog-plugin-vm/test/integration-test/client.spec.js b/packages/datadog-plugin-vm/test/integration-test/client.spec.js index 2f8edbdeb49..4462610c852 100644 --- a/packages/datadog-plugin-vm/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-vm/test/integration-test/client.spec.js @@ -1,10 +1,10 @@ 'use strict' +const assert = require('node:assert/strict') const { useSandbox, sandboxCwd, varySandbox, curl, FakeAgent, spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') -const assert = require('node:assert/strict') describe('ESM', () => { let variants, proc, agent diff --git a/packages/datadog-plugin-vm/test/integration-test/server.mjs b/packages/datadog-plugin-vm/test/integration-test/server.mjs index 5f386632678..c1dfa63cd8c 100644 --- a/packages/datadog-plugin-vm/test/integration-test/server.mjs +++ b/packages/datadog-plugin-vm/test/integration-test/server.mjs @@ -1,6 +1,6 @@ import 'dd-trace/init.js' -import express from 'express' import { runInThisContext } from 'node:vm' +import express from 'express' import dc from 'dc-polyfill' const runScriptCh = dc.channel('datadog:vm:run-script:start') diff --git a/packages/datadog-plugin-winston/test/index.spec.js b/packages/datadog-plugin-winston/test/index.spec.js index f29a54443df..081ebf54082 100644 --- a/packages/datadog-plugin-winston/test/index.spec.js +++ b/packages/datadog-plugin-winston/test/index.spec.js @@ -2,16 +2,14 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') +const http = require('node:http') +const { inspect } = require('node:util') const { afterEach, beforeEach, describe, it } = require('mocha') -const { assertObjectContains } = require('../../../integration-tests/helpers') const proxyquire = require('proxyquire').noPreserveCache() const semver = require('semver') const sinon = require('sinon') - -const http = require('node:http') -const { inspect } = require('node:util') +const { assertObjectContains } = require('../../../integration-tests/helpers') const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') @@ -291,7 +289,7 @@ describe('Plugin', () => { winston.log('info', 'test', meta) assert.strictEqual(meta.dd, undefined) - expect(spy).to.have.been.calledWith() + sinon.assert.calledWithMatch(spy) }) assert.strictEqual(await logServer.logPromise, undefined) }) diff --git a/packages/datadog-plugin-winston/test/integration-test/client.spec.js b/packages/datadog-plugin-winston/test/integration-test/client.spec.js index 1482c041373..e82d6aa574b 100644 --- a/packages/datadog-plugin-winston/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-winston/test/integration-test/client.spec.js @@ -5,7 +5,7 @@ const { FakeAgent, sandboxCwd, useSandbox, - spawnPluginIntegrationTestProc + spawnPluginIntegrationTestProcAndExpectExit } = require('../../../../integration-tests/helpers') const { withVersions } = require('../../../dd-trace/test/setup/mocha') @@ -28,10 +28,12 @@ describe('esm', () => { }) it('is instrumented', async () => { - proc = await spawnPluginIntegrationTestProc( + proc = await spawnPluginIntegrationTestProcAndExpectExit( sandboxCwd(), 'server.mjs', agent.port, + undefined, + undefined, (data) => { const jsonObject = JSON.parse(data.toString()) assert.ok(Object.hasOwn(jsonObject, 'dd')) diff --git a/packages/datadog-plugin-ws/src/close.js b/packages/datadog-plugin-ws/src/close.js index c2574de43e4..42e7a8fd329 100644 --- a/packages/datadog-plugin-ws/src/close.js +++ b/packages/datadog-plugin-ws/src/close.js @@ -1,6 +1,16 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') +const { + WEBSOCKET_PTR_KIND, + SPAN_POINTER_DIRECTION, + SPAN_POINTER_DIRECTION_NAME +} = require('../../dd-trace/src/constants') +const { + incrementWebSocketCounter, + buildWebSocketSpanPointerHash, + hasDistributedTracingContext +} = require('./util') class WSClosePlugin extends TracingPlugin { static get id () { return 'ws' } @@ -10,11 +20,9 @@ class WSClosePlugin extends TracingPlugin { bindStart (ctx) { const { - traceWebsocketMessagesEnabled, traceWebsocketMessagesInheritSampling, traceWebsocketMessagesSeparateTraces } = this.config - if (!traceWebsocketMessagesEnabled) return const { code, data, socket, isPeerClose } = ctx if (!socket?.spanContext) return @@ -60,7 +68,52 @@ class WSClosePlugin extends TracingPlugin { end (ctx) { if (!Object.hasOwn(ctx, 'result') || !ctx.span) return - if (ctx.socket.spanContext) ctx.span.addLink({ context: ctx.socket.spanContext }) + if (ctx.socket.spanContext) { + const linkAttributes = {} + + // Determine link kind based on whether this is peer close (incoming) or self close (outgoing) + const isIncoming = ctx.isPeerClose + linkAttributes['dd.kind'] = isIncoming ? 'executed_by' : 'resuming' + + // Add span pointer for context propagation + if (this.config.traceWebsocketMessagesEnabled && ctx.socket.handshakeSpan) { + const handshakeSpan = ctx.socket.handshakeSpan + + // Only add span pointers if distributed tracing is enabled and handshake has distributed context + if (hasDistributedTracingContext(handshakeSpan, ctx.socket)) { + const counterType = isIncoming ? 'receiveCounter' : 'sendCounter' + const counter = incrementWebSocketCounter(ctx.socket, counterType) + const handshakeContext = handshakeSpan.context() + + const ptrHash = buildWebSocketSpanPointerHash( + handshakeContext._traceId, + handshakeContext._spanId, + counter, + true, // isServer + isIncoming + ) + + const directionName = isIncoming + ? SPAN_POINTER_DIRECTION_NAME.UPSTREAM + : SPAN_POINTER_DIRECTION_NAME.DOWNSTREAM + const direction = isIncoming + ? SPAN_POINTER_DIRECTION.UPSTREAM + : SPAN_POINTER_DIRECTION.DOWNSTREAM + + // Add span pointer attributes to link + linkAttributes['link.name'] = directionName + linkAttributes['dd.kind'] = 'span-pointer' + linkAttributes['ptr.kind'] = WEBSOCKET_PTR_KIND + linkAttributes['ptr.dir'] = direction + linkAttributes['ptr.hash'] = ptrHash + } + } + + ctx.span.addLink({ + context: ctx.socket.spanContext, + attributes: linkAttributes + }) + } ctx.span.finish() } diff --git a/packages/datadog-plugin-ws/src/index.js b/packages/datadog-plugin-ws/src/index.js index b2cd7c38b8a..49a13785fe2 100644 --- a/packages/datadog-plugin-ws/src/index.js +++ b/packages/datadog-plugin-ws/src/index.js @@ -19,6 +19,10 @@ class WSPlugin extends CompositePlugin { } configure (config) { + if (!config.traceWebsocketMessagesEnabled) { + super.configure(false) + return + } return super.configure(config) } } diff --git a/packages/datadog-plugin-ws/src/producer.js b/packages/datadog-plugin-ws/src/producer.js index 8a0ea6a1613..4f6cf016534 100644 --- a/packages/datadog-plugin-ws/src/producer.js +++ b/packages/datadog-plugin-ws/src/producer.js @@ -1,6 +1,16 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') +const { + WEBSOCKET_PTR_KIND, + SPAN_POINTER_DIRECTION, + SPAN_POINTER_DIRECTION_NAME +} = require('../../dd-trace/src/constants') +const { + incrementWebSocketCounter, + buildWebSocketSpanPointerHash, + hasDistributedTracingContext +} = require('./util') class WSProducerPlugin extends TracingPlugin { static get id () { return 'ws' } @@ -9,9 +19,6 @@ class WSProducerPlugin extends TracingPlugin { static get kind () { return 'producer' } bindStart (ctx) { - const messagesEnabled = this.config.traceWebsocketMessagesEnabled - if (!messagesEnabled) return - const { byteLength, socket, binary } = ctx if (!socket.spanContext) return @@ -51,9 +58,37 @@ class WSProducerPlugin extends TracingPlugin { if (!Object.hasOwn(ctx, 'result') || !ctx.span) return if (ctx.socket.spanContext) { + const linkAttributes = { 'dd.kind': 'resuming' } + + // Add span pointer for context propagation + if (this.config.traceWebsocketMessagesEnabled && ctx.socket.handshakeSpan) { + const handshakeSpan = ctx.socket.handshakeSpan + + // Only add span pointers if distributed tracing is enabled and handshake has distributed context + if (hasDistributedTracingContext(handshakeSpan, ctx.socket)) { + const counter = incrementWebSocketCounter(ctx.socket, 'sendCounter') + const handshakeContext = handshakeSpan.context() + + const ptrHash = buildWebSocketSpanPointerHash( + handshakeContext._traceId, + handshakeContext._spanId, + counter, + true, // isServer + false // isIncoming (this is outgoing) + ) + + // Add span pointer attributes to link + linkAttributes['link.name'] = SPAN_POINTER_DIRECTION_NAME.DOWNSTREAM + linkAttributes['dd.kind'] = 'span-pointer' + linkAttributes['ptr.kind'] = WEBSOCKET_PTR_KIND + linkAttributes['ptr.dir'] = SPAN_POINTER_DIRECTION.DOWNSTREAM + linkAttributes['ptr.hash'] = ptrHash + } + } + ctx.span.addLink({ context: ctx.socket.spanContext, - attributes: { 'dd.kind': 'resuming' }, + attributes: linkAttributes, }) } diff --git a/packages/datadog-plugin-ws/src/receiver.js b/packages/datadog-plugin-ws/src/receiver.js index 35c5a4bfb6f..692515f5e8e 100644 --- a/packages/datadog-plugin-ws/src/receiver.js +++ b/packages/datadog-plugin-ws/src/receiver.js @@ -1,6 +1,16 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') +const { + WEBSOCKET_PTR_KIND, + SPAN_POINTER_DIRECTION, + SPAN_POINTER_DIRECTION_NAME +} = require('../../dd-trace/src/constants') +const { + incrementWebSocketCounter, + buildWebSocketSpanPointerHash, + hasDistributedTracingContext +} = require('./util') class WSReceiverPlugin extends TracingPlugin { static get id () { return 'ws' } @@ -10,11 +20,9 @@ class WSReceiverPlugin extends TracingPlugin { bindStart (ctx) { const { - traceWebsocketMessagesEnabled, traceWebsocketMessagesInheritSampling, traceWebsocketMessagesSeparateTraces } = this.config - if (!traceWebsocketMessagesEnabled) return const { byteLength, socket, binary } = ctx if (!socket.spanContext) return @@ -61,9 +69,37 @@ class WSReceiverPlugin extends TracingPlugin { if (!Object.hasOwn(ctx, 'result') || !ctx.span) return if (ctx.socket.spanContext) { + const linkAttributes = { 'dd.kind': 'executed_by' } + + // Add span pointer for context propagation + if (this.config.traceWebsocketMessagesEnabled && ctx.socket.handshakeSpan) { + const handshakeSpan = ctx.socket.handshakeSpan + + // Only add span pointers if distributed tracing is enabled and handshake has distributed context + if (hasDistributedTracingContext(handshakeSpan, ctx.socket)) { + const counter = incrementWebSocketCounter(ctx.socket, 'receiveCounter') + const handshakeContext = handshakeSpan.context() + + const ptrHash = buildWebSocketSpanPointerHash( + handshakeContext._traceId, + handshakeContext._spanId, + counter, + true, // isServer + true // isIncoming + ) + + // Add span pointer attributes to link + linkAttributes['link.name'] = SPAN_POINTER_DIRECTION_NAME.UPSTREAM + linkAttributes['dd.kind'] = 'span-pointer' + linkAttributes['ptr.kind'] = WEBSOCKET_PTR_KIND + linkAttributes['ptr.dir'] = SPAN_POINTER_DIRECTION.UPSTREAM + linkAttributes['ptr.hash'] = ptrHash + } + } + ctx.span.addLink({ context: ctx.socket.spanContext, - attributes: { 'dd.kind': 'executed_by' }, + attributes: linkAttributes, }) } diff --git a/packages/datadog-plugin-ws/src/server.js b/packages/datadog-plugin-ws/src/server.js index aea3c6a16d2..55c2a3e0ced 100644 --- a/packages/datadog-plugin-ws/src/server.js +++ b/packages/datadog-plugin-ws/src/server.js @@ -2,6 +2,8 @@ const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') const tags = require('../../../ext/tags.js') +const { FORMAT_HTTP_HEADERS } = require('../../../ext/formats') +const { initWebSocketMessageCounters } = require('./util') const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE @@ -28,9 +30,13 @@ class WSServerPlugin extends TracingPlugin { ctx.args = { options } + // Extract distributed tracing context from request headers + const childOf = this.tracer.extract(FORMAT_HTTP_HEADERS, req.headers) + const service = this.serviceName({ pluginConfig: this.config }) const span = this.startSpan(this.operationName(), { service, + childOf, meta: { 'span.type': 'websocket', 'http.upgraded': 'websocket', @@ -38,7 +44,6 @@ class WSServerPlugin extends TracingPlugin { 'http.url': uri, 'resource.name': `${options.method} ${route}`, 'span.kind': 'server' - } }, ctx) @@ -46,6 +51,13 @@ class WSServerPlugin extends TracingPlugin { ctx.socket.spanContext = ctx.span._spanContext ctx.socket.spanContext.spanTags = ctx.span._spanContext._tags + // Store the handshake span for use in message span pointers + ctx.socket.handshakeSpan = ctx.span + // Store the request headers for distributed tracing check + ctx.socket.requestHeaders = req.headers + + // Initialize message counters for span pointers + initWebSocketMessageCounters(ctx.socket) return ctx.currentStore } diff --git a/packages/datadog-plugin-ws/src/util.js b/packages/datadog-plugin-ws/src/util.js new file mode 100644 index 00000000000..4e5d665e0e6 --- /dev/null +++ b/packages/datadog-plugin-ws/src/util.js @@ -0,0 +1,107 @@ +'use strict' + +// WeakMap to store message counters per socket without mutating the socket object +const socketCounters = new WeakMap() + +/** + * Initializes WebSocket message counters for a socket. + * @param {object} socket - The WebSocket socket object + */ +function initWebSocketMessageCounters (socket) { + if (!socketCounters.has(socket)) { + socketCounters.set(socket, { + receiveCounter: 0, + sendCounter: 0 + }) + } +} + +/** + * Increments and returns the WebSocket message counter. + * @param {object} socket - The WebSocket socket object + * @param {string} counterType - Either 'receiveCounter' or 'sendCounter' + * @returns {number} The incremented counter value + */ +function incrementWebSocketCounter (socket, counterType) { + if (!socketCounters.has(socket)) { + initWebSocketMessageCounters(socket) + } + const counters = socketCounters.get(socket) + counters[counterType]++ + return counters[counterType] +} + +/** + * Builds a WebSocket span pointer hash. + * + * Format: <128 bit hex trace id><64 bit hex span id><32 bit hex counter> + * Prefix: 'S' for server outgoing or client incoming, 'C' for server incoming or client outgoing + * + * @param {bigint} handshakeTraceId - The trace ID from the handshake span (as a BigInt) + * @param {bigint} handshakeSpanId - The span ID from the handshake span (as a BigInt) + * @param {number} counter - The message counter + * @param {boolean} isServer - Whether this is a server (true) or client (false) + * @param {boolean} isIncoming - Whether this is an incoming message (true) or outgoing (false) + * @returns {string} The span pointer hash + */ +function buildWebSocketSpanPointerHash (handshakeTraceId, handshakeSpanId, counter, isServer, isIncoming) { + // Determine prefix based on server/client and incoming/outgoing + // Server outgoing or client incoming: 'S' + // Server incoming or client outgoing: 'C' + const prefix = (isServer && !isIncoming) || (!isServer && isIncoming) ? 'S' : 'C' + + // Pad trace ID to 32 hex chars (128 bits) + const traceIdHex = handshakeTraceId.toString(16).padStart(32, '0') + + // Pad span ID to 16 hex chars (64 bits) + const spanIdHex = handshakeSpanId.toString(16).padStart(16, '0') + + // Pad counter to 8 hex chars (32 bits) + const counterHex = counter.toString(16).padStart(8, '0') + + return `${prefix}${traceIdHex}${spanIdHex}${counterHex}` +} + +/** + * Checks if the handshake span has extracted distributed tracing context. + * A websocket server must not set the span pointer if the handshake has not extracted a context. + * + * A span has distributed tracing context if it has a parent context that was + * extracted from headers (remote parent). + * + * @param {object} span - The handshake span + * @param {object} socket - The WebSocket socket object + * @returns {boolean} True if the span has distributed tracing context + */ +function hasDistributedTracingContext (span, socket) { + if (!span) return false + const context = span.context() + if (!context) return false + + // Check if this span has a parent. If the parent was extracted from remote headers, + // then this span is part of a distributed trace. + // We check if the span has a parent by looking at _parentId. + // In the JavaScript tracer, when a context is extracted from headers and a child span + // is created, the child will have _parentId set to the extracted parent's span ID. + // + // For testing purposes, we also check if Datadog trace headers are present in the socket's + // upgrade request, which indicates distributed tracing context was sent by the client. + if (context._parentId !== null) { + return true + } + + // Fallback check: look for distributed tracing headers in the stored request headers + if (socket && socket.requestHeaders) { + const headers = socket.requestHeaders + return !!(headers['x-datadog-trace-id'] || headers.traceparent) + } + + return false +} + +module.exports = { + initWebSocketMessageCounters, + incrementWebSocketCounter, + buildWebSocketSpanPointerHash, + hasDistributedTracingContext +} diff --git a/packages/datadog-plugin-ws/test/index.spec.js b/packages/datadog-plugin-ws/test/index.spec.js index 4a0d968e80f..7719f9943b4 100644 --- a/packages/datadog-plugin-ws/test/index.spec.js +++ b/packages/datadog-plugin-ws/test/index.spec.js @@ -7,6 +7,7 @@ const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const agent = require('../../dd-trace/test/plugins/agent') const { withVersions } = require('../../dd-trace/test/setup/mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') describe('Plugin', () => { let WebSocket let wsServer @@ -279,7 +280,7 @@ describe('Plugin', () => { beforeEach(async () => { await agent.load(['ws'], [{ service: 'custom-ws-service', - traceWebsocketMessagesEnabled: true + traceWebsocketMessagesEnabled: false }]) WebSocket = require(`../../../versions/ws@${version}`).get() @@ -293,34 +294,14 @@ describe('Plugin', () => { agent.close({ ritmReset: false, wipe: true }) }) - it('should not produce message spans when traceWebsocketMessagesEnabled is not set to true', () => { - wsServer.on('connection', (ws) => { - ws.send('test message') - }) - messageReceived = false - - client.on('message', (data) => { - assert.strictEqual(data.toString(), 'test message') - messageReceived = true - }) - - return agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, 'custom-ws-service') - assert.strictEqual(traces[0][0].name, 'web.request') - assert.strictEqual(traces[0][0].type, 'websocket') - }) - }) - - it('should not produce close event spans when traceWebsocketMessagesEnabled is not set to true', () => { - wsServer.on('connection', (ws) => { - ws.close() - }) + it('should not initialize sub-plugins when traceWebsocketMessagesEnabled is false', () => { + const tracer = require('../../dd-trace') + const wsPlugin = tracer._pluginManager._pluginsByName.ws - return agent.assertSomeTraces(traces => { - assert.strictEqual(traces[0][0].service, 'custom-ws-service') - assert.strictEqual(traces[0][0].name, 'web.request') - assert.strictEqual(traces[0][0].type, 'websocket') - }) + assert.strictEqual(wsPlugin.server._enabled, false) + assert.strictEqual(wsPlugin.producer._enabled, false) + assert.strictEqual(wsPlugin.receiver._enabled, false) + assert.strictEqual(wsPlugin.close._enabled, false) }) }) describe('with WebSocket configurations settings', () => { @@ -390,6 +371,162 @@ describe('Plugin', () => { }) }) }) + + describe('with span pointers', () => { + let tracer + + beforeEach(async () => { + tracer = require('../../dd-trace') + await agent.load(['ws'], [{ + service: 'ws-with-pointers', + traceWebsocketMessagesEnabled: true, + }]) + WebSocket = require(`../../../versions/ws@${version}`).get() + + wsServer = new WebSocket.Server({ port: clientPort }) + + // Create a parent span within a trace to properly set up distributed tracing context + tracer.trace('test.parent', parentSpan => { + const headers = {} + tracer.inject(parentSpan, 'http_headers', headers) + + // Inject distributed tracing headers to enable span pointers + client = new WebSocket(`ws://localhost:${clientPort}/${route}?active=true`, { + headers + }) + }) + }) + + afterEach(async () => { + clientPort++ + agent.close({ ritmReset: false, wipe: true }) + }) + + it('should add span pointers to producer spans', async () => { + wsServer.on('connection', (ws) => { + ws.send('test message with pointer') + }) + + client.on('message', (data) => { + assert.strictEqual(data.toString(), 'test message with pointer') + }) + + let didFindPointerLink = false + + await agent.assertSomeTraces(traces => { + const producerSpan = traces[0][0] + assert.strictEqual(producerSpan.name, 'websocket.send') + assert.strictEqual(producerSpan.service, 'ws-with-pointers') + + // Check for span links with span pointer attributes + assert.ok(producerSpan.meta['_dd.span_links'], 'Producer span should have span links') + const spanLinks = JSON.parse(producerSpan.meta['_dd.span_links']) + const pointerLink = spanLinks.find(link => + link.attributes && link.attributes['dd.kind'] === 'span-pointer' + ) + assert.ok(pointerLink, 'Should have a span pointer link') + + assertObjectContains(pointerLink, { + attributes: { + 'ptr.kind': 'websocket', + 'ptr.dir': 'd', + 'link.name': 'span-pointer-down' + } + }) + didFindPointerLink = true + + const { attributes } = pointerLink + assert.ok(Object.hasOwn(attributes, 'ptr.hash')) + // Hash format: <32 hex trace id><16 hex span id><8 hex counter> + assert.match(attributes['ptr.hash'], /^[SC][0-9a-f]{32}[0-9a-f]{16}[0-9a-f]{8}$/) + assert.strictEqual(attributes['ptr.hash'].length, 57) + }) + + assert.strictEqual(didFindPointerLink, true) + }) + + it('should add span pointers to consumer spans', async () => { + wsServer.on('connection', (ws) => { + ws.on('message', (data) => { + assert.strictEqual(data.toString(), 'client message with pointer') + }) + }) + + client.on('open', () => { + client.send('client message with pointer') + }) + + let didFindPointerLink = false + + await agent.assertSomeTraces(traces => { + const consumerSpan = traces.find(t => t[0].name === 'websocket.receive')?.[0] + assert.ok(consumerSpan, 'Should have a consumer span') + assert.strictEqual(consumerSpan.service, 'ws-with-pointers') + + // Check for span links with span pointer attributes + assert.ok(consumerSpan.meta['_dd.span_links'], 'Consumer span should have span links') + const spanLinks = JSON.parse(consumerSpan.meta['_dd.span_links']) + const pointerLink = spanLinks.find(link => + link.attributes && link.attributes['dd.kind'] === 'span-pointer' + ) + + assertObjectContains(pointerLink, { + attributes: { + 'ptr.kind': 'websocket', + 'ptr.dir': 'u', + 'link.name': 'span-pointer-up' + } + }) + didFindPointerLink = true + + const { attributes } = pointerLink + assert.ok(Object.hasOwn(attributes, 'ptr.hash')) + // Hash format: <32 hex trace id><16 hex span id><8 hex counter> + assert.match(attributes['ptr.hash'], /^[SC][0-9a-f]{32}[0-9a-f]{16}[0-9a-f]{8}$/) + assert.strictEqual(attributes['ptr.hash'].length, 57) + }) + + assert.strictEqual(didFindPointerLink, true) + }) + + it('should generate unique hashes for each message', () => { + const testMessage = 'test message' + const hashes = new Set() + + wsServer.on('connection', (ws) => { + ws.send(testMessage) + // Send a second message to test counter increment + setTimeout(() => ws.send(testMessage), 10) + }) + + client.on('message', (data) => { + assert.strictEqual(data.toString(), testMessage) + }) + + return agent.assertSomeTraces(traces => { + // Find all producer spans + const producerTraces = traces.filter(t => t[0].name === 'websocket.send') + + producerTraces.forEach(trace => { + if (trace[0].meta['_dd.span_links']) { + const spanLinks = JSON.parse(trace[0].meta['_dd.span_links']) + const pointerLink = spanLinks.find(link => + link.attributes && link.attributes['dd.kind'] === 'span-pointer' + ) + if (pointerLink) { + const hash = pointerLink.attributes['ptr.hash'] + hashes.add(hash) + } + } + }) + + // Each message should have a unique hash due to counter increment + if (hashes.size > 1) { + assert.ok(hashes.size >= 2, 'Multiple messages should have different hashes') + } + }) + }) + }) }) }) }) diff --git a/packages/datadog-shimmer/src/shimmer.js b/packages/datadog-shimmer/src/shimmer.js index add93b8b8f8..587263ec93e 100644 --- a/packages/datadog-shimmer/src/shimmer.js +++ b/packages/datadog-shimmer/src/shimmer.js @@ -91,7 +91,7 @@ function wrapFunction (original, wrapper) { * @param {Record | Function | undefined} target - The target * object. * @param {string | symbol} name - The property key of the method to wrap. - * @param {(original: Function) => (...args: unknown[]) => any} wrapper - The wrapper function. + * @param {(original: Function) => (...args: unknown[]) => unknown} wrapper - The wrapper function. * @param {{ replaceGetter?: boolean }} [options] - If `replaceGetter` is set to * true, the getter is accessed and the getter is replaced with one that just * returns the earlier retrieved value. Use with care! This may only be done in @@ -214,7 +214,7 @@ function wrap (target, name, wrapper, options) { * Record | * Function} targets - The target objects. * @param {Array | string | symbol} names - The property keys of the methods to wrap. - * @param {(original: Function) => (...args: unknown[]) => any} wrapper - The wrapper function. + * @param {(original: Function) => (...args: unknown[]) => unknown} wrapper - The wrapper function. */ function massWrap (targets, names, wrapper) { targets = toArray(targets) diff --git a/packages/datadog-shimmer/test/shimmer.spec.js b/packages/datadog-shimmer/test/shimmer.spec.js index 8529c8f0d46..43521149b18 100644 --- a/packages/datadog-shimmer/test/shimmer.spec.js +++ b/packages/datadog-shimmer/test/shimmer.spec.js @@ -2,8 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') - const shimmer = require('../src/shimmer') describe('shimmer', () => { @@ -237,7 +235,7 @@ describe('shimmer', () => { shimmer.wrap(obj, 'count', () => () => {}) assert.strictEqual(obj.count.test, 'test') - expect(Object.getOwnPropertyNames(obj.count)).to.not.include('test') + assert.strictEqual(Object.hasOwn(obj.count, 'test'), false) }) it('should inherit from the original method prototype 2', () => { @@ -253,7 +251,7 @@ describe('shimmer', () => { assert.strictEqual(obj.count.test, 'test') assert.strictEqual(obj.count.foo, 42) - expect(Object.getOwnPropertyNames(obj.count)).to.not.include('test') + assert.strictEqual(Object.hasOwn(obj.count, 'test'), false) }) it('should preserve the property descriptor of the original', () => { @@ -477,7 +475,7 @@ describe('shimmer', () => { const wrapped = shimmer.wrapFunction(count, count => () => {}) assert.strictEqual(wrapped.test, 'test') - expect(Object.getOwnPropertyNames(wrapped)).to.not.include('test') + assert.strictEqual(Object.hasOwn(wrapped, 'test'), false) }) it('should mass wrap methods on objects', () => { diff --git a/packages/dd-trace/src/aiguard/sdk.js b/packages/dd-trace/src/aiguard/sdk.js index 93b9c651797..06f75df3fc6 100644 --- a/packages/dd-trace/src/aiguard/sdk.js +++ b/packages/dd-trace/src/aiguard/sdk.js @@ -1,6 +1,9 @@ 'use strict' const rfdc = require('../../../../vendor/dist/rfdc')({ proto: false, circles: false }) +const log = require('../log') +const telemetryMetrics = require('../telemetry/metrics') +const tracerVersion = require('../../../../package.json').version const NoopAIGuard = require('./noop') const executeRequest = require('./client') const { @@ -14,9 +17,6 @@ const { AI_GUARD_TELEMETRY_REQUESTS, AI_GUARD_TELEMETRY_TRUNCATED } = require('./tags') -const log = require('../log') -const telemetryMetrics = require('../telemetry/metrics') -const tracerVersion = require('../../../../package.json').version const appsecMetrics = telemetryMetrics.manager.namespace('appsec') diff --git a/packages/dd-trace/src/appsec/graphql.js b/packages/dd-trace/src/appsec/graphql.js index 2c59657fe2f..daa851fd85e 100644 --- a/packages/dd-trace/src/appsec/graphql.js +++ b/packages/dd-trace/src/appsec/graphql.js @@ -1,16 +1,16 @@ 'use strict' const { storage } = require('../../../datadog-core') +const log = require('../log') +const web = require('../plugins/util/web') const { addSpecificEndpoint, specificBlockingTypes, getBlockingData, getBlockingAction } = require('./blocking') -const log = require('../log') const waf = require('./waf') const addresses = require('./addresses') -const web = require('../plugins/util/web') const { startGraphqlResolve, graphqlMiddlewareChannel, diff --git a/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js index 60d1f81e541..12ab7e75e3b 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js @@ -1,10 +1,10 @@ 'use strict' const { CODE_INJECTION } = require('../vulnerabilities') -const StoredInjectionAnalyzer = require('./stored-injection-analyzer') const { INSTRUMENTED_SINK } = require('../telemetry/iast-metric') const { storage } = require('../../../../../datadog-core') const { getIastContext } = require('../iast-context') +const StoredInjectionAnalyzer = require('./stored-injection-analyzer') class CodeInjectionAnalyzer extends StoredInjectionAnalyzer { constructor () { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js index fd2a230a2a8..c7b2d8dae49 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const InjectionAnalyzer = require('./injection-analyzer') const { COMMAND_INJECTION } = require('../vulnerabilities') +const InjectionAnalyzer = require('./injection-analyzer') class CommandInjectionAnalyzer extends InjectionAnalyzer { constructor () { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js index 9cc9e86e9be..33ab3d37a57 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js @@ -1,7 +1,7 @@ 'use strict' -const Analyzer = require('./vulnerability-analyzer') const { getNodeModulesPaths } = require('../path-line') +const Analyzer = require('./vulnerability-analyzer') const EXCLUDED_PATHS = [ // Express diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js index 36a4832ee74..5d253941194 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js @@ -1,7 +1,7 @@ 'use strict' -const Analyzer = require('./vulnerability-analyzer') const { getRelativePath } = require('../path-line') +const Analyzer = require('./vulnerability-analyzer') class HardcodedBaseAnalyzer extends Analyzer { constructor (type, allRules = [], valueOnlyRules = []) { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js index 47914ad822c..fedf0603188 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js @@ -1,7 +1,7 @@ 'use strict' -const Analyzer = require('./vulnerability-analyzer') const { getRanges } = require('../taint-tracking/operations') const { SQL_ROW_VALUE } = require('../taint-tracking/source-types') +const Analyzer = require('./vulnerability-analyzer') class InjectionAnalyzer extends Analyzer { _isVulnerable (value, iastContext) { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js index acdc9f2a895..8a97cc66ef9 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/ldap-injection-analyzer.js @@ -1,7 +1,7 @@ 'use strict' -const InjectionAnalyzer = require('./injection-analyzer') const { LDAP_INJECTION } = require('../vulnerabilities') const { getNodeModulesPaths } = require('../path-line') +const InjectionAnalyzer = require('./injection-analyzer') const EXCLUDED_PATHS = getNodeModulesPaths('ldapjs-promise') diff --git a/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js index 98598621884..40f38dc3ff9 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js @@ -1,16 +1,16 @@ 'use strict' -const InjectionAnalyzer = require('./injection-analyzer') const { NOSQL_MONGODB_INJECTION } = require('../vulnerabilities') const { getRanges, addSecureMark } = require('../taint-tracking/operations') const { getNodeModulesPaths } = require('../path-line') const { storage } = require('../../../../../datadog-core') const { getIastContext } = require('../iast-context') const { HTTP_REQUEST_PARAMETER, HTTP_REQUEST_BODY } = require('../taint-tracking/source-types') - -const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose', 'mquery') const { NOSQL_MONGODB_INJECTION_MARK } = require('../taint-tracking/secure-marks') const { iterateObjectStrings } = require('../utils') +const InjectionAnalyzer = require('./injection-analyzer') + +const EXCLUDED_PATHS_FROM_STACK = getNodeModulesPaths('mongodb', 'mongoose', 'mquery') const SAFE_OPERATORS = new Set(['$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin', '$exists', '$type', '$mod', '$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet']) diff --git a/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js index 8fed2e7bf21..b3d4491ca80 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js @@ -2,10 +2,10 @@ const path = require('path') -const InjectionAnalyzer = require('./injection-analyzer') const { getIastContext } = require('../iast-context') const { storage } = require('../../../../../datadog-core') const { PATH_TRAVERSAL } = require('../vulnerabilities') +const InjectionAnalyzer = require('./injection-analyzer') const ignoredOperations = new Set(['dir.close', 'close']) diff --git a/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js index f829e4fbcfe..e3010cced00 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js @@ -1,7 +1,7 @@ 'use strict' -const InjectionAnalyzer = require('./injection-analyzer') const { SSRF } = require('../vulnerabilities') +const InjectionAnalyzer = require('./injection-analyzer') class SSRFAnalyzer extends InjectionAnalyzer { constructor () { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js index fcec3e4d576..a21d7eed2d8 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js @@ -1,7 +1,7 @@ 'use strict' -const InjectionAnalyzer = require('./injection-analyzer') const { UNTRUSTED_DESERIALIZATION } = require('../vulnerabilities') +const InjectionAnalyzer = require('./injection-analyzer') class UntrustedDeserializationAnalyzer extends InjectionAnalyzer { constructor () { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js index 67057dddeef..4fcfee58d6d 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js @@ -1,6 +1,5 @@ 'use strict' -const InjectionAnalyzer = require('./injection-analyzer') const { UNVALIDATED_REDIRECT } = require('../vulnerabilities') const { getNodeModulesPaths } = require('../path-line') const { getRanges } = require('../taint-tracking/operations') @@ -8,6 +7,7 @@ const { HTTP_REQUEST_BODY, HTTP_REQUEST_PARAMETER } = require('../taint-tracking/source-types') +const InjectionAnalyzer = require('./injection-analyzer') const EXCLUDED_PATHS = [ getNodeModulesPaths('express/lib/response.js'), diff --git a/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js index 7a5542cc9a5..028d4cc9a27 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const Analyzer = require('./vulnerability-analyzer') const { WEAK_CIPHER } = require('../vulnerabilities') +const Analyzer = require('./vulnerability-analyzer') const INSECURE_CIPHERS = new Set([ 'des', 'des-cbc', 'des-cfb', 'des-cfb1', 'des-cfb8', 'des-ecb', 'des-ede', 'des-ede-cbc', 'des-ede-cfb', diff --git a/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js index 457e9f0ff74..039aa79a318 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js @@ -3,8 +3,8 @@ const path = require('path') const { getNodeModulesPaths } = require('../path-line') -const Analyzer = require('./vulnerability-analyzer') const { WEAK_HASH } = require('../vulnerabilities') +const Analyzer = require('./vulnerability-analyzer') const INSECURE_HASH_ALGORITHMS = new Set([ 'md4', 'md4WithRSAEncryption', 'RSA-MD4', @@ -23,7 +23,8 @@ const EXCLUDED_LOCATIONS = getNodeModulesPaths( 'ws/lib/websocket-server.js', 'google-gax/build/src/grpc.js', 'cookie-signature/index.js', - 'express-session/index.js' + 'express-session/index.js', + 'node-preload/preload-list-env.js' ) const EXCLUDED_PATHS_FROM_STACK = [ diff --git a/packages/dd-trace/src/appsec/iast/analyzers/weak-randomness-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/weak-randomness-analyzer.js index b150d172325..f5aa39f9525 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/weak-randomness-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/weak-randomness-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const Analyzer = require('./vulnerability-analyzer') const { WEAK_RANDOMNESS } = require('../vulnerabilities') +const Analyzer = require('./vulnerability-analyzer') class WeakRandomnessAnalyzer extends Analyzer { constructor () { diff --git a/packages/dd-trace/src/appsec/iast/iast-plugin.js b/packages/dd-trace/src/appsec/iast/iast-plugin.js index 9c6dc57c921..b825fea206a 100644 --- a/packages/dd-trace/src/appsec/iast/iast-plugin.js +++ b/packages/dd-trace/src/appsec/iast/iast-plugin.js @@ -3,13 +3,13 @@ const { channel } = require('dc-polyfill') const Plugin = require('../../plugins/plugin') +const { storage } = require('../../../../datadog-core') +const instrumentations = require('../../../../datadog-instrumentations/src/helpers/instrumentations') +const log = require('../../log') const iastTelemetry = require('./telemetry') const { getInstrumentedMetric, getExecutedMetric, TagKey, EXECUTED_SOURCE, formatTags } = require('./telemetry/iast-metric') -const { storage } = require('../../../../datadog-core') const { getIastContext } = require('./iast-context') -const instrumentations = require('../../../../datadog-instrumentations/src/helpers/instrumentations') -const log = require('../../log') /** * Used by vulnerability sources and sinks to subscribe diagnostic channel events diff --git a/packages/dd-trace/src/appsec/iast/index.js b/packages/dd-trace/src/appsec/iast/index.js index 76209f0599f..7548a1361b9 100644 --- a/packages/dd-trace/src/appsec/iast/index.js +++ b/packages/dd-trace/src/appsec/iast/index.js @@ -1,11 +1,13 @@ 'use strict' -const vulnerabilityReporter = require('./vulnerability-reporter') -const { enableAllAnalyzers, disableAllAnalyzers } = require('./analyzers') +const dc = require('dc-polyfill') const web = require('../../plugins/util/web') const { storage } = require('../../../../datadog-core') +const { enable: enableFsPlugin, disable: disableFsPlugin, IAST_MODULE } = require('../rasp/fs-plugin') +const { incomingHttpRequestStart, incomingHttpRequestEnd, responseWriteHead } = require('../channels') +const vulnerabilityReporter = require('./vulnerability-reporter') +const { enableAllAnalyzers, disableAllAnalyzers } = require('./analyzers') const overheadController = require('./overhead-controller') -const dc = require('dc-polyfill') const iastContextFunctions = require('./iast-context') const { enableTaintTracking, @@ -16,9 +18,7 @@ const { } = require('./taint-tracking') const { IAST_ENABLED_TAG_KEY } = require('./tags') const iastTelemetry = require('./telemetry') -const { enable: enableFsPlugin, disable: disableFsPlugin, IAST_MODULE } = require('../rasp/fs-plugin') const securityControls = require('./security-controls') -const { incomingHttpRequestStart, incomingHttpRequestEnd, responseWriteHead } = require('../channels') const collectedResponseHeaders = new WeakMap() diff --git a/packages/dd-trace/src/appsec/iast/security-controls/index.js b/packages/dd-trace/src/appsec/iast/security-controls/index.js index 03c2cbe85d6..5d466251a73 100644 --- a/packages/dd-trace/src/appsec/iast/security-controls/index.js +++ b/packages/dd-trace/src/appsec/iast/security-controls/index.js @@ -5,10 +5,10 @@ const dc = require('dc-polyfill') const { storage } = require('../../../../../datadog-core') const shimmer = require('../../../../../datadog-shimmer') const log = require('../../../log') -const { parse, SANITIZER_TYPE } = require('./parser') const TaintTrackingOperations = require('../taint-tracking/operations') const { getIastContext } = require('../iast-context') const { iterateObjectStrings } = require('../utils') +const { parse, SANITIZER_TYPE } = require('./parser') // esm const moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart') diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/index.js b/packages/dd-trace/src/appsec/iast/taint-tracking/index.js index 0f53c74043c..9f331adebe7 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/index.js @@ -1,5 +1,6 @@ 'use strict' +const kafkaContextPlugin = require('../context/kafka-ctx-plugin') const { createTransaction, removeTransaction, @@ -11,8 +12,6 @@ const { const taintTrackingPlugin = require('./plugin') const kafkaConsumerPlugin = require('./plugins/kafka') -const kafkaContextPlugin = require('../context/kafka-ctx-plugin') - module.exports = { enableTaintTracking (config, telemetryVerbosity) { enableTaintOperations(telemetryVerbosity) diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js b/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js index 9a0f1db45de..0b8cea25945 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js @@ -2,8 +2,8 @@ const TaintedUtils = require('@datadog/native-iast-taint-tracking') const { IAST_TRANSACTION_ID } = require('../iast-context') -const { HTTP_REQUEST_PARAMETER } = require('./source-types') const log = require('../../../log') +const { HTTP_REQUEST_PARAMETER } = require('./source-types') const SEPARATOR = '\u0000' // Unit Separator (cannot be in URL keys) diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js index 32a27cbacf6..6d548c5afd9 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js @@ -3,6 +3,7 @@ const { SourceIastPlugin } = require('../iast-plugin') const { getIastContext } = require('../iast-context') const { storage } = require('../../../../../datadog-core') +const { EXECUTED_SOURCE } = require('../telemetry/iast-metric') const { taintObject, newTaintedString, getRanges, taintQueryWithCache } = require('./operations') const { HTTP_REQUEST_BODY, @@ -15,7 +16,6 @@ const { HTTP_REQUEST_URI, SQL_ROW_VALUE } = require('./source-types') -const { EXECUTED_SOURCE } = require('../telemetry/iast-metric') const REQ_HEADER_TAGS = EXECUTED_SOURCE.formatTags(HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_HEADER_NAME) const REQ_URI_TAGS = EXECUTED_SOURCE.formatTags(HTTP_REQUEST_URI) 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 d793c2ebaf9..c9827ca82d4 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js @@ -5,18 +5,18 @@ const Module = require('module') const { pathToFileURL } = require('url') const { MessageChannel } = require('worker_threads') +const { isMainThread } = require('worker_threads') +const dc = require('dc-polyfill') const shimmer = require('../../../../../datadog-shimmer') -const { isPrivateModule, isDdTrace } = require('./filter') -const { csiMethods } = require('./csi-methods') const { getName } = require('../telemetry/verbosity') const telemetry = require('../telemetry') -const { incrementTelemetryIfNeeded } = require('./rewriter-telemetry') -const dc = require('dc-polyfill') const log = require('../../../log') -const { isMainThread } = require('worker_threads') -const { LOG_MESSAGE, REWRITTEN_MESSAGE } = require('./constants') const orchestrionConfig = require('../../../../../datadog-instrumentations/src/orchestrion-config') const { getEnvironmentVariable } = require('../../../config-helper') +const { LOG_MESSAGE, REWRITTEN_MESSAGE } = require('./constants') +const { incrementTelemetryIfNeeded } = require('./rewriter-telemetry') +const { csiMethods } = require('./csi-methods') +const { isPrivateModule, isDdTrace } = require('./filter') let config const hardcodedSecretCh = dc.channel('datadog:secrets:result') @@ -183,14 +183,10 @@ function enableRewriter (telemetryVerbosity) { } function isEsmConfigured () { - const hasLoaderArg = isFlagPresent('--loader') || isFlagPresent('--experimental-loader') - if (hasLoaderArg) return true - - // Fast path for common case when enabled - if (require.cache[`${process.cwd()}/node_modules/import-in-the-middle/hook.js`]) { - return true - } - return Object.keys(require.cache).some(file => file.endsWith('import-in-the-middle/hook.js')) + return (isFlagPresent('--loader') || + isFlagPresent('--experimental-loader') || + isFlagPresent('dd-trace/initialize.mjs')) || + isFlagPresent('dd-trace/register.js') } let enableEsmRewriter = function (telemetryVerbosity) { diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js b/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js index 7381a8a786a..c28426e9678 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js @@ -6,8 +6,8 @@ const { storage } = require('../../../../../datadog-core') const iastContextFunctions = require('../iast-context') const { EXECUTED_PROPAGATION } = require('../telemetry/iast-metric') const { isDebugAllowed } = require('../telemetry/verbosity') -const { taintObject } = require('./operations-taint-object') const log = require('../../../log') +const { taintObject } = require('./operations-taint-object') const mathRandomCallCh = dc.channel('datadog:random:call') const evalCallCh = dc.channel('datadog:eval:call') diff --git a/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js b/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js index de460270405..6cd2c322b70 100644 --- a/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js +++ b/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js @@ -2,8 +2,8 @@ const log = require('../../../log') const { Namespace } = require('../../../telemetry/metrics') -const { addMetricsToSpan } = require('./span-tags') const { IAST_TRACE_METRIC_PREFIX } = require('../tags') +const { addMetricsToSpan } = require('./span-tags') const DD_IAST_METRICS_NAMESPACE = Symbol('_dd.iast.request.metrics.namespace') diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index 48044e78c2f..431d87b5e21 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -1,12 +1,12 @@ 'use strict' const { LRUCache } = require('../../../../../vendor/dist/lru-cache') -const vulnerabilitiesFormatter = require('./vulnerabilities-formatter') -const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags') const { keepTrace } = require('../../priority_sampler') const { reportStackTrace, getCallsiteFrames, canReportStackTrace, STACK_TRACE_NAMESPACES } = require('../stack_trace') -const { getOriginalPathAndLineFromSourceMap } = require('./taint-tracking/rewriter') const { ASM } = require('../../standalone/product') +const vulnerabilitiesFormatter = require('./vulnerabilities-formatter') +const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags') +const { getOriginalPathAndLineFromSourceMap } = require('./taint-tracking/rewriter') const VULNERABILITIES_KEY = 'vulnerabilities' const VULNERABILITY_HASHES_MAX_SIZE = 1000 diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index 093e52db8be..067cbc448ab 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -1,8 +1,13 @@ 'use strict' const log = require('../log') +const web = require('../plugins/util/web') +const { extractIp } = require('../plugins/util/ip_extractor') +const { HTTP_CLIENT_IP } = require('../../../../ext/tags') +const { storage } = require('../../../datadog-core') +const { isInServerlessEnvironment } = require('../serverless') const RuleManager = require('./rule_manager') -const remoteConfig = require('../remote_config') +const appsecRemoteConfig = require('./remote_config') const { bodyParser, cookieParser, @@ -31,15 +36,10 @@ const addresses = require('./addresses') const Reporter = require('./reporter') const appsecTelemetry = require('./telemetry') const apiSecuritySampler = require('./api_security_sampler') -const web = require('../plugins/util/web') -const { extractIp } = require('../plugins/util/ip_extractor') -const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, callBlockDelegation, setTemplates, getBlockingAction } = require('./blocking') const UserTracking = require('./user_tracking') -const { storage } = require('../../../datadog-core') const graphql = require('./graphql') const rasp = require('./rasp') -const { isInServerlessEnvironment } = require('../serverless') const responseAnalyzedSet = new WeakSet() const storedResponseHeaders = new WeakMap() @@ -63,7 +63,7 @@ function enable (_config) { RuleManager.loadRules(_config.appsec) - remoteConfig.enableWafUpdate(_config.appsec) + appsecRemoteConfig.enableWafUpdate(_config.appsec) Reporter.init(_config.appsec) @@ -373,7 +373,7 @@ function disable () { graphql.disable() rasp.disable() - remoteConfig.disableWafUpdate() + appsecRemoteConfig.disableWafUpdate() apiSecuritySampler.disable() diff --git a/packages/dd-trace/src/appsec/rasp/command_injection.js b/packages/dd-trace/src/appsec/rasp/command_injection.js index d403fcf36d4..98d6992f363 100644 --- a/packages/dd-trace/src/appsec/rasp/command_injection.js +++ b/packages/dd-trace/src/appsec/rasp/command_injection.js @@ -1,10 +1,10 @@ 'use strict' const { childProcessExecutionTracingChannel } = require('../channels') -const { RULE_TYPES, handleResult } = require('./utils') const { storage } = require('../../../../datadog-core') const addresses = require('../addresses') const waf = require('../waf') +const { RULE_TYPES, handleResult } = require('./utils') let config diff --git a/packages/dd-trace/src/appsec/rasp/index.js b/packages/dd-trace/src/appsec/rasp/index.js index 36ddb5bfef8..395e6bff7b0 100644 --- a/packages/dd-trace/src/appsec/rasp/index.js +++ b/packages/dd-trace/src/appsec/rasp/index.js @@ -8,11 +8,11 @@ const { routerMiddlewareError } = require('../channels') const { block, registerBlockDelegation, isBlocked } = require('../blocking') +const { updateRaspRuleMatchMetricTags } = require('../telemetry') const ssrf = require('./ssrf') const sqli = require('./sql_injection') const lfi = require('./lfi') const cmdi = require('./command_injection') -const { updateRaspRuleMatchMetricTags } = require('../telemetry') const { DatadogRaspAbortError } = require('./utils') diff --git a/packages/dd-trace/src/appsec/rasp/lfi.js b/packages/dd-trace/src/appsec/rasp/lfi.js index 69cb23d7ed1..6bf8adbe838 100644 --- a/packages/dd-trace/src/appsec/rasp/lfi.js +++ b/packages/dd-trace/src/appsec/rasp/lfi.js @@ -4,9 +4,9 @@ const { isAbsolute } = require('path') const { fsOperationStart, incomingHttpRequestStart, expressResponseRenderStart } = require('../channels') const { storage } = require('../../../../datadog-core') -const { enable: enableFsPlugin, disable: disableFsPlugin, RASP_MODULE } = require('./fs-plugin') const { FS_OPERATION_PATH } = require('../addresses') const waf = require('../waf') +const { enable: enableFsPlugin, disable: disableFsPlugin, RASP_MODULE } = require('./fs-plugin') const { RULE_TYPES, handleResult } = require('./utils') let config diff --git a/packages/dd-trace/src/appsec/rc-products.js b/packages/dd-trace/src/appsec/rc-products.js new file mode 100644 index 00000000000..01ca6bfe0f6 --- /dev/null +++ b/packages/dd-trace/src/appsec/rc-products.js @@ -0,0 +1,10 @@ +'use strict' + +// Remote Config product names used by ASM/WAF. +const ASM_WAF_PRODUCTS = ['ASM', 'ASM_DD', 'ASM_DATA'] +const ASM_WAF_PRODUCTS_SET = new Set(ASM_WAF_PRODUCTS) + +module.exports = { + ASM_WAF_PRODUCTS, + ASM_WAF_PRODUCTS_SET +} diff --git a/packages/dd-trace/src/appsec/recommended.json b/packages/dd-trace/src/appsec/recommended.json index b39992af325..dff79cfde2e 100644 --- a/packages/dd-trace/src/appsec/recommended.json +++ b/packages/dd-trace/src/appsec/recommended.json @@ -1,7 +1,7 @@ { "version": "2.2", "metadata": { - "rules_version": "1.15.1" + "rules_version": "1.16.1" }, "rules": [ { @@ -4376,7 +4376,7 @@ "address": "graphql.server.resolver" } ], - "regex": "java\\.lang\\.(?:runtime|processbuilder)", + "regex": "\\bjava\\.lang\\.(?:runtime|processbuilder)\\b", "options": { "case_sensitive": true, "min_length": 17 @@ -8989,7 +8989,7 @@ "event": false, "keep": false, "attributes": { - "_dd.appsec.api.jwt_alg": { + "api.security.jwt.alg": { "address": "server.request.jwt", "key_path": [ "header", @@ -9091,6 +9091,233 @@ } } }, + { + "id": "api-010-100", + "name": "Monitor redirections to GET targets", + "tags": { + "type": "api10", + "category": "api_security", + "confidence": "0", + "module": "business-logic" + }, + "min_version": "1.25.0", + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.response.status" + } + ], + "list": [ + "301", + "302" + ] + }, + "operator": "exact_match" + } + ], + "transformers": [], + "output": { + "event": false, + "keep": false, + "attributes": { + "api.security.redirection.move_target": { + "address": "server.io.net.response.headers", + "key_path": [ + "Location" + ] + } + } + } + }, + { + "id": "api-010-110", + "name": "Monitor redirections to POST targets", + "tags": { + "type": "api10", + "category": "api_security", + "confidence": "0", + "module": "business-logic" + }, + "min_version": "1.25.0", + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.response.status" + } + ], + "list": [ + "307", + "308" + ] + }, + "operator": "exact_match" + } + ], + "transformers": [], + "output": { + "event": false, + "keep": false, + "attributes": { + "api.security.redirection.redirect_target": { + "address": "server.io.net.response.headers", + "key_path": [ + "Location" + ] + } + } + } + }, + { + "id": "api-010-200", + "name": "Large response bodies in downstream network calls", + "tags": { + "type": "api10", + "category": "api_security", + "confidence": "0", + "module": "business-logic" + }, + "min_version": "1.25.0", + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.response.headers", + "key_path": [ + "content-length" + ] + } + ], + "regex": "\\d{7,}", + "options": { + "case_sensitive": true, + "min_length": 7 + } + }, + "operator": "match_regex" + } + ], + "transformers": [], + "output": { + "event": false, + "keep": false, + "attributes": { + "api.security.large_response.length": { + "address": "server.io.net.response.headers", + "key_path": [ + "content-length" + ] + }, + "api.security.large_response.url": { + "address": "server.io.net.url" + } + } + } + }, + { + "id": "api-010-300", + "name": "Secrets transmitted in downstream URL parameters", + "tags": { + "type": "api10", + "category": "api_security", + "confidence": "0", + "module": "business-logic" + }, + "min_version": "1.25.0", + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.url" + } + ], + "regex": "[?&](?:(?:api|access)?(_)?(?:key|secret|token|password|passwd|pwd))=", + "options": { + "case_sensitive": false + } + }, + "operator": "match_regex" + } + ], + "transformers": [], + "output": { + "event": false, + "keep": false, + "attributes": { + "api.security.secret.disclosed_in_url_params": { + "value": 1 + } + } + } + }, + { + "id": "api-010-400", + "name": "Unauthenticated MCP access", + "tags": { + "type": "api10", + "category": "api_security", + "confidence": "0", + "module": "business-logic" + }, + "min_version": "1.25.0", + "conditions": [ + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.url" + } + ], + "regex": "/mcp/(?:tools|resources)/", + "options": { + "case_sensitive": false + } + }, + "operator": "match_regex" + }, + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.request.headers", + "key_path": [ + "authorization" + ] + } + ] + }, + "operator": "!exists" + }, + { + "parameters": { + "inputs": [ + { + "address": "server.io.net.response.status" + } + ], + "list": [ + "401" + ] + }, + "operator": "!exact_match" + } + ], + "transformers": [], + "output": { + "event": false, + "keep": false, + "attributes": { + "api.security.mcp.broken_auth": { + "value": 1 + } + } + } + }, { "id": "ua0-600-551", "name": "Datadog test scanner - scalar trace-tagging version: user-agent", diff --git a/packages/dd-trace/src/appsec/remote_config.js b/packages/dd-trace/src/appsec/remote_config.js new file mode 100644 index 00000000000..c87f2958ccd --- /dev/null +++ b/packages/dd-trace/src/appsec/remote_config.js @@ -0,0 +1,177 @@ +'use strict' + +const log = require('../log') +const { updateConfig } = require('../telemetry') +const RemoteConfigCapabilities = require('../remote_config/capabilities') +const { setCollectionMode } = require('./user_tracking') +const Activation = require('./activation') + +let autoUserInstrumModeId +let rc + +/** + * Configures remote config handlers for appsec features + * + * @param {object} rcInstance - RemoteConfig instance + * @param {object} config - Tracer config + * @param {object} appsec - Appsec module + */ +function enable (rcInstance, config, appsec) { + rc = rcInstance + const activation = Activation.fromConfig(config) + + if (activation !== Activation.DISABLED) { + if (activation === Activation.ONECLICK) { + rc.updateCapabilities(RemoteConfigCapabilities.ASM_ACTIVATION, true) + } + + rc.updateCapabilities(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + + rc.setProductHandler('ASM_FEATURES', (action, rcConfig, configId) => { + if (!rcConfig) return + + // this is put before other handlers because it can reject the config + if (typeof rcConfig.auto_user_instrum?.mode === 'string') { + if (action === 'apply' || action === 'modify') { + // check if there is already a config applied with this field + if (autoUserInstrumModeId && configId !== autoUserInstrumModeId) { + log.error('[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config') + // eslint-disable-next-line no-throw-literal + throw 'Multiple auto_user_instrum.mode received in ASM_FEATURES' + } + + setCollectionMode(rcConfig.auto_user_instrum.mode) + autoUserInstrumModeId = configId + } else if (configId === autoUserInstrumModeId) { + setCollectionMode(config.appsec.eventTracking.mode) + autoUserInstrumModeId = null + } + } + + if (activation === Activation.ONECLICK) { + enableOrDisableAppsec(action, rcConfig, config, appsec) + } + }) + } +} + +/** + * Enables or disables appsec based on remote config + * + * @param {string} action - 'apply', 'modify', or 'unapply' + * @param {object} rcConfig - Remote config + * @param {object} config - Tracer config + * @param {object} appsec - Appsec module + */ +function enableOrDisableAppsec (action, rcConfig, config, appsec) { + if (typeof rcConfig.asm?.enabled === 'boolean') { + const isRemoteConfigControlling = action === 'apply' || action === 'modify' + const shouldEnable = isRemoteConfigControlling + ? rcConfig.asm.enabled // take control + : config.appsec.enabled // give back control to local config + + if (shouldEnable) { + appsec.enable(config) + } else { + appsec.disable() + } + + updateConfig([ + { + name: 'appsec.enabled', + origin: isRemoteConfigControlling ? 'remote_config' : config.getOrigin('appsec.enabled'), + value: shouldEnable + } + ], config) + } +} + +/** + * Enables WAF update capabilities for remote config + * + * @param {object} appsecConfig - Appsec config + */ +function enableWafUpdate (appsecConfig) { + if (rc && appsecConfig && !appsecConfig.rules) { + // dirty require to make startup faster for serverless + const { ASM_WAF_PRODUCTS } = require('./rc-products') + const RuleManager = require('./rule_manager') + + rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_USER_BLOCKING, true) + // TODO: we should have a different capability for rule override + rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, true) + + if (appsecConfig.rasp?.enabled) { + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, true) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, true) + } + + rc.subscribeProducts(...ASM_WAF_PRODUCTS) + rc.setBatchHandler(ASM_WAF_PRODUCTS, RuleManager.updateWafFromRC) + } +} + +/** + * Disables WAF update capabilities for remote config + */ +function disableWafUpdate () { + if (rc) { + const { ASM_WAF_PRODUCTS } = require('./rc-products') + const RuleManager = require('./rule_manager') + + rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_USER_BLOCKING, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, false) + + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, false) + rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, false) + + rc.unsubscribeProducts(...ASM_WAF_PRODUCTS) + rc.removeBatchHandler(RuleManager.updateWafFromRC) + } +} + +module.exports = { + enable, + enableWafUpdate, + disableWafUpdate +} diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index 0c6da2a78e9..ac20f0e31a4 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -1,11 +1,13 @@ 'use strict' -const dc = require('dc-polyfill') const zlib = require('zlib') +const dc = require('dc-polyfill') const { storage } = require('../../../datadog-core') const web = require('../plugins/util/web') const { ipHeaderList } = require('../plugins/util/ip_extractor') +const { keepTrace } = require('../priority_sampler') +const { ASM } = require('../standalone/product') const { incrementWafInitMetric, incrementWafUpdatesMetric, @@ -16,8 +18,6 @@ const { updateRaspRuleSkippedMetricTags, getRequestMetrics } = require('./telemetry') -const { keepTrace } = require('../priority_sampler') -const { ASM } = require('../standalone/product') const { DIAGNOSTIC_KEYS } = require('./waf/diagnostics') const REQUEST_HEADER_TAG_PREFIX = 'http.request.headers.' diff --git a/packages/dd-trace/src/appsec/rule_manager.js b/packages/dd-trace/src/appsec/rule_manager.js index f50e71d708f..bda45399e11 100644 --- a/packages/dd-trace/src/appsec/rule_manager.js +++ b/packages/dd-trace/src/appsec/rule_manager.js @@ -1,14 +1,12 @@ 'use strict' -const fs = require('fs') +const { readFileSync } = require('node:fs') + const waf = require('./waf') const { DIAGNOSTIC_KEYS } = require('./waf/diagnostics') -const { ACKNOWLEDGED, ERROR } = require('../remote_config/apply_states') -const Reporter = require('./reporter') - const blocking = require('./blocking') - -const ASM_PRODUCTS = new Set(['ASM', 'ASM_DD', 'ASM_DATA']) +const Reporter = require('./reporter') +const { ASM_WAF_PRODUCTS_SET } = require('./rc-products') /* ASM Actions must be tracked in order to update the defaultBlockingActions in blocking. These actions are used @@ -16,9 +14,21 @@ const ASM_PRODUCTS = new Set(['ASM', 'ASM_DD', 'ASM_DATA']) */ let appliedActions = new Map() +/** + * @typedef {object} AsmConfigFile + * @property {Array>} [actions] + */ + +/** + * @typedef {import('./waf').WAFConfig & { rules?: string }} AppSecConfig + */ + +/** + * @param {AppSecConfig} config + */ function loadRules (config) { const defaultRules = config.rules - ? JSON.parse(fs.readFileSync(config.rules)) + ? JSON.parse(readFileSync(config.rules, 'utf8')) : require('./recommended.json') waf.init(defaultRules, config) @@ -26,19 +36,26 @@ function loadRules (config) { blocking.setDefaultBlockingActionParameters(defaultRules?.actions) } -function updateWafFromRC ({ toUnapply, toApply, toModify }) { +/** + * Apply ASM remote-config updates to the WAF in a single batch. + * + * @param {import('../remote_config/manager').RcBatchUpdateTransaction} transaction + */ +function updateWafFromRC (transaction) { + const { toUnapply, toApply, toModify } = transaction + const newActions = new SpyMap(appliedActions) let wafUpdated = false let wafUpdatedFailed = false for (const item of toUnapply) { - if (!ASM_PRODUCTS.has(item.product)) continue + if (!ASM_WAF_PRODUCTS_SET.has(item.product)) continue try { waf.removeConfig(item.path) - item.apply_state = ACKNOWLEDGED + transaction.ack(item.path) wafUpdated = true // ASM actions @@ -46,30 +63,30 @@ function updateWafFromRC ({ toUnapply, toApply, toModify }) { newActions.delete(item.id) } } catch (e) { - item.apply_state = ERROR - item.apply_error = e.toString() + transaction.error(item.path, e) wafUpdatedFailed = true } } for (const item of [...toApply, ...toModify]) { - if (!ASM_PRODUCTS.has(item.product)) continue + if (!ASM_WAF_PRODUCTS_SET.has(item.product)) continue try { waf.updateConfig(item.product, item.id, item.path, item.file) - item.apply_state = ACKNOWLEDGED + transaction.ack(item.path) wafUpdated = true // ASM actions - if (item.product === 'ASM' && item.file?.actions?.length) { - newActions.set(item.id, item.file.actions) + if (item.product === 'ASM') { + const asmFile = /** @type {AsmConfigFile} */ (item.file) + if (asmFile?.actions?.length) { + newActions.set(item.id, asmFile.actions) + } } } catch (e) { - item.apply_state = ERROR - item.apply_error = e instanceof waf.WafUpdateError - ? JSON.stringify(extractErrors(e.diagnosticErrors)) - : e.toString() + const error = e instanceof waf.WafUpdateError ? JSON.stringify(extractErrors(e.diagnosticErrors)) : e + transaction.error(item.path, error) wafUpdatedFailed = true } } diff --git a/packages/dd-trace/src/appsec/sdk/index.js b/packages/dd-trace/src/appsec/sdk/index.js index 3df0f0058fc..9f28069c11d 100644 --- a/packages/dd-trace/src/appsec/sdk/index.js +++ b/packages/dd-trace/src/appsec/sdk/index.js @@ -1,5 +1,6 @@ 'use strict' +const { setTemplates } = require('../blocking') const { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, @@ -8,7 +9,6 @@ const { trackUserLoginFailureV2 } = require('./track_event') const { checkUserAndSetUser, blockRequest } = require('./user_blocking') -const { setTemplates } = require('../blocking') const { setUser } = require('./set_user') class EventTrackingV2 { diff --git a/packages/dd-trace/src/appsec/sdk/set_user.js b/packages/dd-trace/src/appsec/sdk/set_user.js index 6f32facab44..22947e18ad6 100644 --- a/packages/dd-trace/src/appsec/sdk/set_user.js +++ b/packages/dd-trace/src/appsec/sdk/set_user.js @@ -1,9 +1,9 @@ 'use strict' -const { getRootSpan } = require('./utils') const log = require('../../log') const waf = require('../waf') const addresses = require('../addresses') +const { getRootSpan } = require('./utils') function setUserTags (user, rootSpan) { for (const k of Object.keys(user)) { diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index e66c475686e..ef18066f050 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -1,13 +1,13 @@ 'use strict' const log = require('../../log') -const { getRootSpan } = require('./utils') -const { setUserTags } = require('./set_user') const waf = require('../waf') const { keepTrace } = require('../../priority_sampler') const addresses = require('../addresses') const { ASM } = require('../../standalone/product') const { incrementSdkEventMetric } = require('../telemetry') +const { setUserTags } = require('./set_user') +const { getRootSpan } = require('./utils') /** * @deprecated in favor of trackUserLoginSuccessV2 diff --git a/packages/dd-trace/src/appsec/sdk/user_blocking.js b/packages/dd-trace/src/appsec/sdk/user_blocking.js index a854ce028e9..77ad6e839e8 100644 --- a/packages/dd-trace/src/appsec/sdk/user_blocking.js +++ b/packages/dd-trace/src/appsec/sdk/user_blocking.js @@ -2,11 +2,11 @@ const { USER_ID } = require('../addresses') const waf = require('../waf') -const { getRootSpan } = require('./utils') const { block, getBlockingAction } = require('../blocking') const { storage } = require('../../../../datadog-core') -const { setUserTags } = require('./set_user') const log = require('../../log') +const { setUserTags } = require('./set_user') +const { getRootSpan } = require('./utils') function isUserBlocked (user) { const results = waf.run({ persistent: { [USER_ID]: user.id } }) diff --git a/packages/dd-trace/src/appsec/user_tracking.js b/packages/dd-trace/src/appsec/user_tracking.js index 0943f91e796..a259c880bcf 100644 --- a/packages/dd-trace/src/appsec/user_tracking.js +++ b/packages/dd-trace/src/appsec/user_tracking.js @@ -2,11 +2,11 @@ const crypto = require('crypto') const log = require('../log') +const { keepTrace } = require('../priority_sampler') +const { ASM } = require('../standalone/product') const telemetry = require('./telemetry') const addresses = require('./addresses') -const { keepTrace } = require('../priority_sampler') const waf = require('./waf') -const { ASM } = require('../standalone/product') // the RFC doesn't include '_id', but it's common in MongoDB const USER_ID_FIELDS = ['id', '_id', 'email', 'username', 'login', 'user'] diff --git a/packages/dd-trace/src/appsec/waf/index.js b/packages/dd-trace/src/appsec/waf/index.js index a2035073f73..8a81c118697 100644 --- a/packages/dd-trace/src/appsec/waf/index.js +++ b/packages/dd-trace/src/appsec/waf/index.js @@ -19,7 +19,17 @@ class WafUpdateError extends Error { let limiter = new Limiter(100) +/** @typedef {import('./waf_manager')} WAFManager */ + +/** @type {typeof import('./waf_manager') | null} */ +let WAFManager = null + +/** + * @typedef {import('./waf_manager').WAFManagerConfig & { rateLimit: number }} WAFConfig + */ + const waf = { + /** @type {WAFManager | null} */ wafManager: null, init, destroy, @@ -31,13 +41,17 @@ const waf = { WafUpdateError } +/** + * @param {object} rules + * @param {WAFConfig} config + */ function init (rules, config) { destroy() limiter = new Limiter(config.rateLimit) - // dirty require to make startup faster for serverless - const WAFManager = require('./waf_manager') + // Lazy loading improves the startup time + WAFManager = require('./waf_manager') waf.wafManager = new WAFManager(rules, config) @@ -70,7 +84,7 @@ function updateConfig (product, configId, configPath, config) { try { if (product === 'ASM_DD') { - waf.wafManager.removeConfig(waf.wafManager.constructor.defaultWafConfigPath) + waf.wafManager.removeConfig((/** @type {NonNullable} */ (WAFManager)).defaultWafConfigPath) } const updateSucceeded = waf.wafManager.updateConfig(configPath, config) diff --git a/packages/dd-trace/src/appsec/waf/waf_manager.js b/packages/dd-trace/src/appsec/waf/waf_manager.js index 05c77272526..7cd2d6a6cbc 100644 --- a/packages/dd-trace/src/appsec/waf/waf_manager.js +++ b/packages/dd-trace/src/appsec/waf/waf_manager.js @@ -6,9 +6,20 @@ const WAFContextWrapper = require('./waf_context_wrapper') const contexts = new WeakMap() +/** + * @typedef {object} WAFManagerConfig + * @property {number} wafTimeout - Maximum time in microseconds for WAF execution + * @property {string} obfuscatorKeyRegex - Regex to redact sensitive data by key + * @property {string} obfuscatorValueRegex - Regex to redact sensitive data by value + */ + class WAFManager { static defaultWafConfigPath = 'datadog/00/ASM_DD/default/config' + /** + * @param {object} rules + * @param {WAFManagerConfig} config + */ constructor (rules, config) { this.config = config this.wafTimeout = config.wafTimeout diff --git a/packages/dd-trace/src/azure_metadata.js b/packages/dd-trace/src/azure_metadata.js index 57a548eb2ba..1bb3bf216f7 100644 --- a/packages/dd-trace/src/azure_metadata.js +++ b/packages/dd-trace/src/azure_metadata.js @@ -3,8 +3,8 @@ // Modeled after https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/ddcommon/src/azure_app_services.rs const os = require('os') -const { getIsAzureFunction } = require('./serverless') const { getEnvironmentVariable, getEnvironmentVariables } = require('../../dd-trace/src/config-helper') +const { getIsAzureFunction, getIsFlexConsumptionAzureFunction } = require('./serverless') function extractSubscriptionID (ownerName) { if (ownerName !== undefined) { @@ -38,6 +38,7 @@ function buildMetadata () { const { COMPUTERNAME, DD_AAS_DOTNET_EXTENSION_VERSION, + DD_AZURE_RESOURCE_GROUP, FUNCTIONS_EXTENSION_VERSION, FUNCTIONS_WORKER_RUNTIME, FUNCTIONS_WORKER_RUNTIME_VERSION, @@ -56,7 +57,12 @@ function buildMetadata () { ? ['functionapp', 'function'] : ['app', 'app'] - const resourceGroup = WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME) + // Azure Functions on Flex Consumption plans require the `DD_AZURE_RESOURCE_GROUP` env var. + // If this logic ever changes, update the logic in `libdatadog`, `serverless-components/src/datadog-trace-agent`, + // and the serverless compat layers accordingly. + const resourceGroup = getIsFlexConsumptionAzureFunction() + ? (DD_AZURE_RESOURCE_GROUP ?? WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME)) + : (WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME)) return trimObject({ extensionVersion: DD_AAS_DOTNET_EXTENSION_VERSION, diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js index e4811e9f3dc..73468c7dab1 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js @@ -2,6 +2,7 @@ const { workerData: { + config, breakpointSetChannel, breakpointHitChannel, breakpointRemoveChannel @@ -21,6 +22,7 @@ const { getStackFromCallFrames } = require('../../../debugger/devtools_client/state') const log = require('../../../log') +const processTags = require('../../../process-tags') let sessionStarted = false @@ -55,6 +57,10 @@ session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint] language: 'javascript' } + if (config.propagateProcessTags?.enabled) { + snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject + } + breakpointHitChannel.postMessage({ snapshot }) }) diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js index a5b677ef98b..454382b741b 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js @@ -1,10 +1,10 @@ 'use strict' const URL = require('url').URL -const Writer = require('./writer') -const CoverageWriter = require('./coverage-writer') const CiVisibilityExporter = require('../ci-visibility-exporter') const log = require('../../../log') +const Writer = require('./writer') +const CoverageWriter = require('./coverage-writer') class AgentlessCiVisibilityExporter extends CiVisibilityExporter { constructor (config) { 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 474a1114644..62cc9103653 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 @@ -2,7 +2,6 @@ const URL = require('url').URL -const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata') const { getLibraryConfiguration: getLibraryConfigurationRequest } = require('../requests/get-library-configuration') const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites') const { getKnownTests: getKnownTestsRequest } = require('../early-flake-detection/get-known-tests') @@ -11,6 +10,7 @@ const { getTestManagementTests: getTestManagementTestsRequest } = const log = require('../../log') const AgentInfoExporter = require('../../exporters/common/agent-info-exporter') const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags') +const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata') function getTestConfigurationTags (tags) { if (!tags) { 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 5cd7e775387..62e9c755e0f 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 @@ -1,6 +1,5 @@ 'use strict' -const Writer = require('./writer') const { JEST_WORKER_COVERAGE_PAYLOAD_CODE, JEST_WORKER_TRACE_PAYLOAD_CODE, @@ -12,6 +11,7 @@ const { VITEST_WORKER_LOGS_PAYLOAD_CODE } = require('../../../plugins/util/test') const { getEnvironmentVariable } = require('../../../config-helper') +const Writer = require('./writer') function getInterprocessTraceCode () { if (getEnvironmentVariable('JEST_WORKER_ID')) { diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index f32a8097b77..5f644dfd3ea 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -2,25 +2,30 @@ const fs = require('fs') const os = require('os') -const uuid = require('../../../vendor/dist/crypto-randomuuid') // we need to keep the old uuid dep because of cypress const { URL } = require('url') +const path = require('path') +const uuid = require('../../../vendor/dist/crypto-randomuuid') // we need to keep the old uuid dep because of cypress +const set = require('../../datadog-core/src/utils/src/set') +const { DD_MAJOR } = require('../../../version') const log = require('./log') const tagger = require('./tagger') -const set = require('../../datadog-core/src/utils/src/set') const { isTrue, isFalse, normalizeProfilingEnabledValue } = require('./util') const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('./plugins/util/tags') const { getGitMetadataFromGitProperties, removeUserSensitiveInfo, getRemoteOriginURL, resolveGitHeadSHA } = require('./git_properties') const { updateConfig } = require('./telemetry') const telemetryMetrics = require('./telemetry/metrics') -const { isInServerlessEnvironment, getIsGCPFunction, getIsAzureFunction } = require('./serverless') +const { + isInServerlessEnvironment, + getIsGCPFunction, + getIsAzureFunction, + enableGCPPubSubPushSubscription +} = require('./serverless') const { ORIGIN_KEY } = require('./constants') const { appendRules } = require('./payload-tagging/config') const { getEnvironmentVariable: getEnv, getEnvironmentVariables } = require('./config-helper') const defaults = require('./config_defaults') -const path = require('path') -const { DD_MAJOR } = require('../../../version') const tracerMetrics = telemetryMetrics.manager.namespace('tracers') @@ -182,7 +187,7 @@ function validateNamingVersion (versionString) { * @param {string | string[]} input */ function splitJSONPathRules (input) { - if (!input) return + if (!input || input === '$') return if (Array.isArray(input)) return input if (input === 'all') return [] return input.split(',') @@ -276,8 +281,6 @@ class Config { this.stableConfig = new StableConfig() } - const envs = getEnvironmentVariables() - options = { ...options, appsec: options.appsec == null ? options.experimental?.appsec : options.appsec, @@ -321,7 +324,7 @@ class Config { this.#defaults = defaults this.#applyDefaults() this.#applyStableConfig(this.stableConfig?.localEntries ?? {}, this.#localStableConfig) - this.#applyEnvironment(envs) + this.#applyEnvironment() this.#applyStableConfig(this.stableConfig?.fleetEntries ?? {}, this.#fleetStableConfig) this.#applyOptions(options) this.#applyCalculated() @@ -342,7 +345,7 @@ class Config { } if (this.gitMetadataEnabled) { - this.#loadGitMetadata(envs) + this.#loadGitMetadata() } } @@ -801,6 +804,7 @@ class Config { this.#setBoolean(target, 'injectForce', DD_INJECT_FORCE) this.#setBoolean(target, 'isAzureFunction', getIsAzureFunction()) this.#setBoolean(target, 'isGCPFunction', getIsGCPFunction()) + this.#setBoolean(target, 'gcpPubSubPushSubscriptionEnabled', enableGCPPubSubPushSubscription()) target['langchain.spanCharLimit'] = maybeInt(DD_LANGCHAIN_SPAN_CHAR_LIMIT) target['langchain.spanPromptCompletionSampleRate'] = maybeFloat(DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE) this.#setBoolean(target, 'legacyBaggageEnabled', DD_TRACE_LEGACY_BAGGAGE_ENABLED) @@ -1152,9 +1156,8 @@ class Config { // This is reliant on environment config being set before options. // This is to make sure the origins of each value are tracked appropriately for telemetry. // We'll only set `llmobs.enabled` on the opts when it's not set on the environment, and options.llmobs is provided. - const llmobsEnabledEnv = this.#env['llmobs.enabled'] - if (llmobsEnabledEnv == null && options.llmobs) { - this.#setBoolean(opts, 'llmobs.enabled', !!options.llmobs) + if (this.#env['llmobs.enabled'] == null && options.llmobs) { + this.#setBoolean(opts, 'llmobs.enabled', true) } } @@ -1473,26 +1476,24 @@ class Config { } } - #loadGitMetadata (envs) { + #loadGitMetadata () { // try to read Git metadata from the environment variables this.repositoryUrl = removeUserSensitiveInfo( - envs.DD_GIT_REPOSITORY_URL ?? - this.tags[GIT_REPOSITORY_URL] + getEnv('DD_GIT_REPOSITORY_URL') ?? this.tags[GIT_REPOSITORY_URL] ) - this.commitSHA = envs.DD_GIT_COMMIT_SHA ?? - this.tags[GIT_COMMIT_SHA] + this.commitSHA = getEnv('DD_GIT_COMMIT_SHA') ?? this.tags[GIT_COMMIT_SHA] // otherwise, try to read Git metadata from the git.properties file if (!this.repositoryUrl || !this.commitSHA) { - const DD_GIT_PROPERTIES_FILE = envs.DD_GIT_PROPERTIES_FILE ?? - `${process.cwd()}/git.properties` + const DD_GIT_PROPERTIES_FILE = getEnv('DD_GIT_PROPERTIES_FILE') + const gitPropertiesFile = DD_GIT_PROPERTIES_FILE ?? `${process.cwd()}/git.properties` let gitPropertiesString try { - gitPropertiesString = fs.readFileSync(DD_GIT_PROPERTIES_FILE, 'utf8') + gitPropertiesString = fs.readFileSync(gitPropertiesFile, 'utf8') } catch (e) { // Only log error if the user has set a git.properties path - if (envs.DD_GIT_PROPERTIES_FILE) { - log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', DD_GIT_PROPERTIES_FILE, e) + if (DD_GIT_PROPERTIES_FILE) { + log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', gitPropertiesFile, e) } } if (gitPropertiesString) { @@ -1503,11 +1504,11 @@ class Config { } // otherwise, try to read Git metadata from the .git/ folder if (!this.repositoryUrl || !this.commitSHA) { - const DD_GIT_FOLDER_PATH = envs.DD_GIT_FOLDER_PATH ?? - path.join(process.cwd(), '.git') + const DD_GIT_FOLDER_PATH = getEnv('DD_GIT_FOLDER_PATH') + const gitFolderPath = DD_GIT_FOLDER_PATH ?? path.join(process.cwd(), '.git') if (!this.repositoryUrl) { // try to read git config (repository URL) - const gitConfigPath = path.join(DD_GIT_FOLDER_PATH, 'config') + const gitConfigPath = path.join(gitFolderPath, 'config') try { const gitConfigContent = fs.readFileSync(gitConfigPath, 'utf8') if (gitConfigContent) { @@ -1515,14 +1516,14 @@ class Config { } } catch (e) { // Only log error if the user has set a .git/ path - if (envs.DD_GIT_FOLDER_PATH) { + if (DD_GIT_FOLDER_PATH) { log.error('Error reading git config: %s', gitConfigPath, e) } } } if (!this.commitSHA) { // try to read git HEAD (commit SHA) - const gitHeadSha = resolveGitHeadSHA(DD_GIT_FOLDER_PATH) + const gitHeadSha = resolveGitHeadSHA(gitFolderPath) if (gitHeadSha) { this.commitSHA = gitHeadSha } diff --git a/packages/dd-trace/src/config/remote_config.js b/packages/dd-trace/src/config/remote_config.js new file mode 100644 index 00000000000..01b72c72d57 --- /dev/null +++ b/packages/dd-trace/src/config/remote_config.js @@ -0,0 +1,34 @@ +'use strict' + +const RemoteConfigCapabilities = require('../remote_config/capabilities') + +/** + * Configures remote config for core APM tracing functionality + * + * @param {object} rc - RemoteConfig instance + * @param {object} config - Tracer config + * @param {Function} enableOrDisableTracing - Function to enable/disable tracing based on config + */ +function enable (rc, config, enableOrDisableTracing) { + // Register core APM tracing capabilities + rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true) + rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true) + rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_LOGS_INJECTION, true) + rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RATE, true) + rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLED, true) + rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RULES, true) + + // APM_TRACING product handler - manages tracer configuration + rc.setProductHandler('APM_TRACING', (action, conf) => { + if (action === 'unapply') { + config.configure({}, true) + } else { + config.configure(conf.lib_config, true) + } + enableOrDisableTracing(config, rc) + }) +} + +module.exports = { + enable +} diff --git a/packages/dd-trace/src/config_defaults.js b/packages/dd-trace/src/config_defaults.js index 02ca66c25ca..2d02d96a9da 100644 --- a/packages/dd-trace/src/config_defaults.js +++ b/packages/dd-trace/src/config_defaults.js @@ -114,6 +114,7 @@ module.exports = { isFlakyTestRetriesEnabled: false, flakyTestRetriesCount: 5, isGCPFunction: false, + isGCPPubSubPushSubscriptionEnabled: true, isGitUploadEnabled: false, isIntelligentTestRunnerEnabled: false, isManualApiEnabled: false, @@ -205,7 +206,7 @@ module.exports = { 'tracePropagationStyle.inject': ['datadog', 'tracecontext', 'baggage'], 'tracePropagationStyle.extract': ['datadog', 'tracecontext', 'baggage'], 'tracePropagationStyle.otelPropagators': false, - traceWebsocketMessagesEnabled: false, + traceWebsocketMessagesEnabled: true, traceWebsocketMessagesInheritSampling: true, traceWebsocketMessagesSeparateTraces: true, tracing: true, diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index c184e595237..8af34cb6e3f 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -50,8 +50,13 @@ module.exports = { GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], DYNAMODB_PTR_KIND: 'aws.dynamodb.item', S3_PTR_KIND: 'aws.s3.object', + WEBSOCKET_PTR_KIND: 'websocket', SPAN_POINTER_DIRECTION: Object.freeze({ UPSTREAM: 'u', DOWNSTREAM: 'd' + }), + SPAN_POINTER_DIRECTION_NAME: Object.freeze({ + UPSTREAM: 'span-pointer-up', + DOWNSTREAM: 'span-pointer-down' }) } diff --git a/packages/dd-trace/src/crashtracking/crashtracker.js b/packages/dd-trace/src/crashtracking/crashtracker.js index 88d31417fda..fac59b7b16e 100644 --- a/packages/dd-trace/src/crashtracking/crashtracker.js +++ b/packages/dd-trace/src/crashtracking/crashtracker.js @@ -1,13 +1,15 @@ 'use strict' +const { URL } = require('url') + // Load binding first to not import other modules if it throws const libdatadog = require('@datadog/libdatadog') const binding = libdatadog.load('crashtracker') const log = require('../log') const defaults = require('../config_defaults') -const { URL } = require('url') const pkg = require('../../../../package.json') +const processTags = require('../process-tags') class Crashtracker { #started = false @@ -77,6 +79,13 @@ class Crashtracker { #getMetadata (config) { const tags = Object.keys(config.tags).map(key => `${key}:${config.tags[key]}`) + // Add process tags to the tags array + for (const [key, value] of processTags.tags) { + if (value !== undefined) { + tags.push(`${key}:${value}`) + } + } + return { library_name: pkg.name, library_version: pkg.version, diff --git a/packages/dd-trace/src/datastreams/checkpointer.js b/packages/dd-trace/src/datastreams/checkpointer.js index 255723560b2..1cef57c15ba 100644 --- a/packages/dd-trace/src/datastreams/checkpointer.js +++ b/packages/dd-trace/src/datastreams/checkpointer.js @@ -13,7 +13,7 @@ class DataStreamsCheckpointer { * @param {string} type - The type of the checkpoint, usually the streaming technology being used. * Examples include kafka, kinesis, sns etc. * @param {string} target - The target of data. This can be a topic, exchange or stream name. - * @param {Object} carrier - The carrier object to inject context into. + * @param {object} carrier - The carrier object to inject context into. */ setProduceCheckpoint (type, target, carrier) { if (!this.config.dsmEnabled) return @@ -33,7 +33,7 @@ class DataStreamsCheckpointer { * @param {string} type - The type of the checkpoint, usually the streaming technology being used. * 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 {object} carrier - The carrier object to extract context from. * @param {boolean} [manualCheckpoint=true] - 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 diff --git a/packages/dd-trace/src/datastreams/index.js b/packages/dd-trace/src/datastreams/index.js index 4d37b6ed95f..53d826add39 100644 --- a/packages/dd-trace/src/datastreams/index.js +++ b/packages/dd-trace/src/datastreams/index.js @@ -13,7 +13,7 @@ const { // // TODO: Remove this when DSM has been moved to dedicated plugins. /** - * @template T extends new (...args: any[]) => any + * @template T extends new (...args: unknown[]) => unknown * @param {() => T} classGetter * @param {string[]} methods * @param {string[]} staticMethods diff --git a/packages/dd-trace/src/datastreams/pathway.js b/packages/dd-trace/src/datastreams/pathway.js index eab5fd8bfd8..c1994cc3a1b 100644 --- a/packages/dd-trace/src/datastreams/pathway.js +++ b/packages/dd-trace/src/datastreams/pathway.js @@ -4,10 +4,10 @@ // other languages use FNV1 // this inconsistency is ok because hashes do not need to be consistent across services const crypto = require('crypto') -const { encodeVarint, decodeVarint } = require('./encoding') const { LRUCache } = require('../../../../vendor/dist/lru-cache') const log = require('../log') const pick = require('../../../datadog-core/src/utils/src/pick') +const { encodeVarint, decodeVarint } = require('./encoding') const cache = new LRUCache({ max: 500 }) @@ -44,7 +44,7 @@ function computeHash (service, env, edgeTags, parentHash) { } /** - * @param {Object} dataStreamsContext + * @param {object} dataStreamsContext * @param {Buffer} dataStreamsContext.hash * @param {number} dataStreamsContext.pathwayStartNs * @param {number} dataStreamsContext.edgeStartNs @@ -59,7 +59,7 @@ function encodePathwayContext (dataStreamsContext) { } /** - * @param {Object} dataStreamsContext + * @param {object} dataStreamsContext * @param {Buffer} dataStreamsContext.hash * @param {number} dataStreamsContext.pathwayStartNs * @param {number} dataStreamsContext.edgeStartNs @@ -72,7 +72,7 @@ function encodePathwayContextBase64 (dataStreamsContext) { /** * @param {Buffer} pathwayContext - * @returns {Object} + * @returns {object} */ function decodePathwayContext (pathwayContext) { if (pathwayContext == null || pathwayContext.length < 8) { @@ -111,11 +111,11 @@ const DsmPathwayCodec = { // we use a class for encoding / decoding in case we update our encoding/decoding. A class will make updates easier // instead of using individual functions. /** - * @param {Object} dataStreamsContext + * @param {object} dataStreamsContext * @param {Buffer} dataStreamsContext.hash * @param {number} dataStreamsContext.pathwayStartNs * @param {number} dataStreamsContext.edgeStartNs - * @param {Object} carrier + * @param {object} carrier */ encode (dataStreamsContext, carrier) { if (!dataStreamsContext || !dataStreamsContext.hash) { @@ -127,7 +127,7 @@ const DsmPathwayCodec = { }, /** - * @param {Object} carrier + * @param {object} carrier * @returns {ReturnType|undefined} */ decode (carrier) { diff --git a/packages/dd-trace/src/datastreams/processor.js b/packages/dd-trace/src/datastreams/processor.js index b306d022987..986f67b5355 100644 --- a/packages/dd-trace/src/datastreams/processor.js +++ b/packages/dd-trace/src/datastreams/processor.js @@ -4,14 +4,14 @@ const os = require('os') const pkg = require('../../../../package.json') const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js') +const { PATHWAY_HASH } = require('../../../../ext/tags') +const log = require('../log') const { DsmPathwayCodec } = require('./pathway') const { DataStreamsWriter } = require('./writer') const { computePathwayHash } = require('./pathway') const { getAmqpMessageSize, getHeadersSize, getMessageSize, getSizeOrZero } = require('./size') -const { PATHWAY_HASH } = require('../../../../ext/tags') const { SchemaBuilder } = require('./schemas/schema_builder') const { SchemaSampler } = require('./schemas/schema_sampler') -const log = require('../log') const ENTRY_PARENT_HASH = Buffer.from('0000000000000000', 'hex') diff --git a/packages/dd-trace/src/datastreams/writer.js b/packages/dd-trace/src/datastreams/writer.js index 8c74e52b889..be87368e6b1 100644 --- a/packages/dd-trace/src/datastreams/writer.js +++ b/packages/dd-trace/src/datastreams/writer.js @@ -1,12 +1,12 @@ 'use strict' +const { URL, format } = require('url') +const zlib = require('zlib') const pkg = require('../../../../package.json') const log = require('../log') const request = require('../exporters/common/request') -const { URL, format } = require('url') const { MsgpackEncoder } = require('../msgpack') const defaults = require('../config_defaults') -const zlib = require('zlib') const msgpack = new MsgpackEncoder() diff --git a/packages/dd-trace/src/debugger/config.js b/packages/dd-trace/src/debugger/config.js index ee468b1d28f..68f4ddfceb3 100644 --- a/packages/dd-trace/src/debugger/config.js +++ b/packages/dd-trace/src/debugger/config.js @@ -8,6 +8,7 @@ module.exports = function getDebuggerConfig (config) { hostname: config.hostname, logLevel: config.logLevel, port: config.port, + propagateProcessTags: config.propagateProcessTags, repositoryUrl: config.repositoryUrl, runtimeId: config.tags['runtime-id'], service: config.service, diff --git a/packages/dd-trace/src/debugger/devtools_client/config.js b/packages/dd-trace/src/debugger/devtools_client/config.js index eed624ae6a1..e23966d4ad5 100644 --- a/packages/dd-trace/src/debugger/devtools_client/config.js +++ b/packages/dd-trace/src/debugger/devtools_client/config.js @@ -2,8 +2,8 @@ const { workerData: { config: parentConfig, parentThreadId, configPort } } = require('node:worker_threads') const { format } = require('node:url') -const log = require('./log') const defaults = require('../../config_defaults') +const log = require('./log') const config = module.exports = { ...parentConfig, diff --git a/packages/dd-trace/src/debugger/devtools_client/index.js b/packages/dd-trace/src/debugger/devtools_client/index.js index 55f6f72b75f..060f9b932ca 100644 --- a/packages/dd-trace/src/debugger/devtools_client/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/index.js @@ -1,6 +1,9 @@ 'use strict' const { randomUUID } = require('crypto') +const { version } = require('../../../../../package.json') +const { NODE_MAJOR } = require('../../../../../version') +const processTags = require('../../process-tags') const { breakpointToProbes } = require('./state') const session = require('./session') const { getLocalStateForCallFrame } = require('./snapshot') @@ -10,8 +13,6 @@ const { ackEmitting } = require('./status') const config = require('./config') const { MAX_SNAPSHOTS_PER_SECOND_GLOBALLY } = require('./defaults') const log = require('./log') -const { version } = require('../../../../../package.json') -const { NODE_MAJOR } = require('../../../../../version') require('./remote_config') @@ -215,6 +216,10 @@ session.on('Debugger.paused', async ({ params }) => { language: 'javascript' } + if (config.propagateProcessTags.enabled) { + snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject + } + if (probe.captureSnapshot) { if (captureErrors?.length > 0) { // There was an error collecting the snapshot for this probe, let's not try again diff --git a/packages/dd-trace/src/debugger/devtools_client/send.js b/packages/dd-trace/src/debugger/devtools_client/send.js index af1352c559d..590652f344b 100644 --- a/packages/dd-trace/src/debugger/devtools_client/send.js +++ b/packages/dd-trace/src/debugger/devtools_client/send.js @@ -3,13 +3,13 @@ const { hostname: getHostname } = require('os') const { stringify } = require('querystring') -const config = require('./config') -const JSONBuffer = require('./json-buffer') const request = require('../../exporters/common/request') const { GIT_COMMIT_SHA, GIT_REPOSITORY_URL } = require('../../plugins/util/tags') -const log = require('./log') const { version } = require('../../../../../package.json') const { getEnvironmentVariable } = require('../../config-helper') +const log = require('./log') +const JSONBuffer = require('./json-buffer') +const config = require('./config') const { pruneSnapshot } = require('./snapshot-pruner') module.exports = send diff --git a/packages/dd-trace/src/debugger/devtools_client/session.js b/packages/dd-trace/src/debugger/devtools_client/session.js index 3dcca3d33c6..2ac00a00f3e 100644 --- a/packages/dd-trace/src/debugger/devtools_client/session.js +++ b/packages/dd-trace/src/debugger/devtools_client/session.js @@ -7,7 +7,7 @@ const inspector = require('./inspector_promises_polyfill') * connect: () => void, * connectToMainThread: () => void * disconnect: () => void, - * post: (method: string, params?: object) => Promise, + * post: (method: string, params?: object) => Promise, * }} CDPSession */ const session = /** @type {CDPSession} */ (new inspector.Session()) 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 ff86cea70d5..39dd5ebb4fc 100644 --- a/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js @@ -1,8 +1,8 @@ 'use strict' +const session = require('../session') const { collectionSizeSym, largeCollectionSkipThresholdSym, fieldCountSym, timeBudgetSym } = require('./symbols') const { LARGE_OBJECT_SKIP_THRESHOLD } = require('./constants') -const session = require('../session') const LEAF_SUBTYPES = new Set(['date', 'regexp']) const ITERABLE_SUBTYPES = new Set(['map', 'set', 'weakmap', 'weakset']) @@ -13,12 +13,12 @@ module.exports = { } /** - * @typedef {Object} GetObjectOptions - * @property {Object} maxReferenceDepth - The maximum depth of the object to traverse + * @typedef {object} GetObjectOptions + * @property {object} maxReferenceDepth - The maximum depth of the object to traverse * @property {number} maxCollectionSize - The maximum size of a collection to include in the snapshot * @property {number} maxFieldCount - The maximum number of properties on an object to include in the snapshot * @property {bigint} deadlineNs - The deadline in nanoseconds compared to `process.hrtime.bigint()` - * @property {Object} ctx - A context object to track the state/progress of the snapshot collection. + * @property {object} ctx - A context object to track the state/progress of the snapshot collection. * @property {boolean} ctx.deadlineReached - Will be set to `true` if the deadline has been reached. * @property {Error[]} ctx.captureErrors - An array on which errors can be pushed if an issue is detected while * collecting the snapshot. @@ -34,7 +34,7 @@ module.exports = { * 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 * track the current object type and should not be set by the caller. - * @returns {Promise} The properties of the object + * @returns {Promise} The properties of the object */ async function collectObjectProperties (objectId, opts, depth = 0, collection = false) { const { result, privateProperties } = await session.post('Runtime.getProperties', { diff --git a/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js b/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js index a05a3f6165d..3aaa43fdda0 100644 --- a/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js @@ -16,7 +16,7 @@ module.exports = { } /** - * @typedef {Object} GetLocalStateForCallFrameOptions + * @typedef {object} GetLocalStateForCallFrameOptions * @property {number} [maxReferenceDepth] - The maximum depth of the object to traverse. Defaults to * {@link DEFAULT_MAX_REFERENCE_DEPTH}. * @property {number} [maxCollectionSize] - The maximum size of a collection to include in the snapshot. Defaults to @@ -34,7 +34,7 @@ module.exports = { * * @param {import('inspector').Debugger.CallFrame} callFrame - The call frame to get the local state for * @param {GetLocalStateForCallFrameOptions} [opts] - The options for the snapshot - * @returns {Promise} The local state for the call frame + * @returns {Promise} The local state for the call frame */ async function getLocalStateForCallFrame ( callFrame, diff --git a/packages/dd-trace/src/debugger/devtools_client/state.js b/packages/dd-trace/src/debugger/devtools_client/state.js index 9e4346efe35..21543f222b8 100644 --- a/packages/dd-trace/src/debugger/devtools_client/state.js +++ b/packages/dd-trace/src/debugger/devtools_client/state.js @@ -24,7 +24,7 @@ module.exports = { * Find the script to inspect based on a partial or absolute path. Handles both Windows and POSIX paths. * * @param {string} path - Partial or absolute path to match against loaded scripts - * @returns {Object | null} - Object containing `url`, `scriptId`, `sourceMapURL`, and `source` - or null if no match + * @returns {object | null} - Object containing `url`, `scriptId`, `sourceMapURL`, and `source` - or null if no match */ findScriptFromPartialPath (path) { if (!path) return null // This shouldn't happen, but better safe than sorry diff --git a/packages/dd-trace/src/debugger/devtools_client/status.js b/packages/dd-trace/src/debugger/devtools_client/status.js index ccd176072eb..5e7a4f725f6 100644 --- a/packages/dd-trace/src/debugger/devtools_client/status.js +++ b/packages/dd-trace/src/debugger/devtools_client/status.js @@ -1,10 +1,10 @@ 'use strict' const TTLSet = require('../../../../../vendor/dist/ttl-set') -const config = require('./config') -const JSONBuffer = require('./json-buffer') const request = require('../../exporters/common/request') const FormData = require('../../exporters/common/form-data') +const config = require('./config') +const JSONBuffer = require('./json-buffer') const log = require('./log') module.exports = { diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index 347139f2dea..b5c20260efe 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -4,8 +4,8 @@ const { readFile } = require('fs') const { types } = require('util') const { join } = require('path') const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_threads') -const getDebuggerConfig = require('./config') const log = require('../log') +const getDebuggerConfig = require('./config') let worker = null let configChannel = null diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index b33fcfde271..89991e39fe7 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -1,11 +1,12 @@ 'use strict' const lookup = require('dns').lookup // cache to avoid instrumentation -const request = require('./exporters/common/request') const dgram = require('dgram') const isIP = require('net').isIP -const log = require('./log') const { URL, format } = require('url') + +const request = require('./exporters/common/request') +const log = require('./log') const Histogram = require('./histogram') const defaults = require('./config_defaults') diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index b8f40f45e48..2090cc00c4d 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -1,11 +1,11 @@ 'use strict' -const { truncateSpan, normalizeSpan } = require('./tags-processors') const { MsgpackChunk, MsgpackEncoder } = require('../msgpack') const log = require('../log') const { isTrue } = require('../util') const { memoize } = require('../log/utils') const { getEnvironmentVariable } = require('../config-helper') +const { truncateSpan, normalizeSpan } = require('./tags-processors') const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB diff --git a/packages/dd-trace/src/encode/agentless-ci-visibility.js b/packages/dd-trace/src/encode/agentless-ci-visibility.js index acc1b09d25d..a0c636a4a6b 100644 --- a/packages/dd-trace/src/encode/agentless-ci-visibility.js +++ b/packages/dd-trace/src/encode/agentless-ci-visibility.js @@ -1,6 +1,4 @@ 'use strict' -const { truncateSpan, normalizeSpan } = require('./tags-processors') -const { AgentEncoder } = require('./0.4') const { version: ddTraceVersion } = require('../../../../package.json') const { ITR_CORRELATION_ID } = require('../../src/plugins/util/test') const id = require('../../src/id') @@ -9,6 +7,8 @@ const { TELEMETRY_ENDPOINT_PAYLOAD_SERIALIZATION_MS, TELEMETRY_ENDPOINT_PAYLOAD_EVENTS_COUNT } = require('../ci-visibility/telemetry') +const { AgentEncoder } = require('./0.4') +const { truncateSpan, normalizeSpan } = require('./tags-processors') const ENCODING_VERSION = 1 const ALLOWED_CONTENT_TYPES = new Set(['test_session_end', 'test_module_end', 'test_suite_end', 'test']) diff --git a/packages/dd-trace/src/encode/coverage-ci-visibility.js b/packages/dd-trace/src/encode/coverage-ci-visibility.js index e3fe6cbed86..e7d821b0324 100644 --- a/packages/dd-trace/src/encode/coverage-ci-visibility.js +++ b/packages/dd-trace/src/encode/coverage-ci-visibility.js @@ -1,5 +1,4 @@ 'use strict' -const { AgentEncoder } = require('./0.4') const { MsgpackChunk } = require('../msgpack') const { @@ -8,6 +7,7 @@ const { TELEMETRY_ENDPOINT_PAYLOAD_EVENTS_COUNT } = require('../ci-visibility/telemetry') const FormData = require('../exporters/common/form-data') +const { AgentEncoder } = require('./0.4') const COVERAGE_PAYLOAD_VERSION = 2 const COVERAGE_KEYS_LENGTH = 2 diff --git a/packages/dd-trace/src/encode/span-stats.js b/packages/dd-trace/src/encode/span-stats.js index 5739ed7be43..b4f0bbcec25 100644 --- a/packages/dd-trace/src/encode/span-stats.js +++ b/packages/dd-trace/src/encode/span-stats.js @@ -95,7 +95,7 @@ class SpanStatsEncoder extends AgentEncoder { } _encode (bytes, stats) { - this._encodeMapPrefix(bytes, 8) + this._encodeMapPrefix(bytes, stats.ProcessTags ? 9 : 8) this._encodeString(bytes, 'Hostname') this._encodeString(bytes, stats.Hostname) @@ -123,6 +123,11 @@ class SpanStatsEncoder extends AgentEncoder { this._encodeString(bytes, 'Sequence') this._encodeLong(bytes, stats.Sequence) + + if (stats.ProcessTags) { + this._encodeString(bytes, 'ProcessTags') + this._encodeString(bytes, stats.ProcessTags) + } } } diff --git a/packages/dd-trace/src/exporter.js b/packages/dd-trace/src/exporter.js index db95d85f89d..3654dcac8f7 100644 --- a/packages/dd-trace/src/exporter.js +++ b/packages/dd-trace/src/exporter.js @@ -1,9 +1,9 @@ 'use strict' -const exporters = require('../../../ext/exporters') const fs = require('fs') -const constants = require('./constants') +const exporters = require('../../../ext/exporters') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const constants = require('./constants') module.exports = function getExporter (name) { switch (name) { diff --git a/packages/dd-trace/src/exporters/agent/index.js b/packages/dd-trace/src/exporters/agent/index.js index 4e6d852c29a..29139373cca 100644 --- a/packages/dd-trace/src/exporters/agent/index.js +++ b/packages/dd-trace/src/exporters/agent/index.js @@ -2,8 +2,8 @@ const { URL, format } = require('url') const log = require('../../log') -const Writer = require('./writer') const defaults = require('../../config_defaults') +const Writer = require('./writer') class AgentExporter { #timer diff --git a/packages/dd-trace/src/exporters/common/agent-info-exporter.js b/packages/dd-trace/src/exporters/common/agent-info-exporter.js index b36a582156f..93b00dbe524 100644 --- a/packages/dd-trace/src/exporters/common/agent-info-exporter.js +++ b/packages/dd-trace/src/exporters/common/agent-info-exporter.js @@ -2,9 +2,9 @@ const { URL, format } = require('url') -const request = require('./request') const defaults = require('../../config_defaults') const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../../ci-visibility/telemetry') +const request = require('./request') function fetchAgentInfo (url, callback) { request('', { diff --git a/packages/dd-trace/src/exporters/common/request.js b/packages/dd-trace/src/exporters/common/request.js index a87170bbe57..50cf89201f2 100644 --- a/packages/dd-trace/src/exporters/common/request.js +++ b/packages/dd-trace/src/exporters/common/request.js @@ -8,11 +8,11 @@ const http = require('http') const https = require('https') const zlib = require('zlib') +const { storage } = require('../../../../datadog-core') +const log = require('../../log') const { urlToHttpOptions } = require('./url-to-http-options-polyfill') const docker = require('./docker') const { httpAgent, httpsAgent } = require('./agents') -const { storage } = require('../../../../datadog-core') -const log = require('../../log') const maxActiveRequests = 8 diff --git a/packages/dd-trace/src/exporters/common/writer.js b/packages/dd-trace/src/exporters/common/writer.js index 57f08accd32..c27f104abe0 100644 --- a/packages/dd-trace/src/exporters/common/writer.js +++ b/packages/dd-trace/src/exporters/common/writer.js @@ -1,7 +1,7 @@ 'use strict' -const request = require('./request') const log = require('../../log') +const request = require('./request') const { safeJSONStringify } = require('./util') class Writer { diff --git a/packages/dd-trace/src/exporters/span-stats/index.js b/packages/dd-trace/src/exporters/span-stats/index.js index 3f7e57d1426..d8c64c3b164 100644 --- a/packages/dd-trace/src/exporters/span-stats/index.js +++ b/packages/dd-trace/src/exporters/span-stats/index.js @@ -2,8 +2,8 @@ const { URL, format } = require('url') -const { Writer } = require('./writer') const defaults = require('../../config_defaults') +const { Writer } = require('./writer') class SpanStatsExporter { constructor (config) { diff --git a/packages/dd-trace/src/external-logger/src/index.js b/packages/dd-trace/src/external-logger/src/index.js index 77f5523648a..b3b796cf556 100644 --- a/packages/dd-trace/src/external-logger/src/index.js +++ b/packages/dd-trace/src/external-logger/src/index.js @@ -1,8 +1,7 @@ 'use strict' -const tracerLogger = require('../../log')// path to require tracer logger - const https = require('https') +const tracerLogger = require('../../log')// path to require tracer logger class ExternalLogger { // Note: these attribute names match the corresponding entry in the JSON payload. diff --git a/packages/dd-trace/src/flare/index.js b/packages/dd-trace/src/flare/index.js index a93ce931238..2c9de92e914 100644 --- a/packages/dd-trace/src/flare/index.js +++ b/packages/dd-trace/src/flare/index.js @@ -2,10 +2,10 @@ const log = require('../log') const startupLog = require('../startup-log') -const FlareFile = require('./file') const { LogChannel } = require('../log/channels') const request = require('../exporters/common/request') const FormData = require('../exporters/common/form-data') +const FlareFile = require('./file') const MAX_LOG_SIZE = 12 * 1024 * 1024 // 12MB soft limit const TIMEOUT = 20 * 1000 * 60 diff --git a/packages/dd-trace/src/guardrails/index.js b/packages/dd-trace/src/guardrails/index.js index 9379d042c0e..eb8a30ecb97 100644 --- a/packages/dd-trace/src/guardrails/index.js +++ b/packages/dd-trace/src/guardrails/index.js @@ -2,10 +2,11 @@ var path = require('path') var Module = require('module') + +var nodeVersion = require('../../../../version') var isTrue = require('./util').isTrue var log = require('./log') var telemetry = require('./telemetry') -var nodeVersion = require('../../../../version') var NODE_MAJOR = nodeVersion.NODE_MAJOR @@ -14,7 +15,9 @@ function guard (fn) { var clobberBailout = false var forced = isTrue(process.env.DD_INJECT_FORCE) var engines = require('../../../../package.json').engines - var minMajor = parseInt(engines.node.replace(/[^0-9]/g, '')) + var versions = engines.node.match(/^>=(\d+) <(\d+)$/) + var minMajor = versions[1] + var nextMajor = versions[2] var version = process.versions.node if (process.env.DD_INJECTION_ENABLED) { @@ -40,7 +43,7 @@ function guard (fn) { // If the runtime doesn't match the engines field in package.json, then we // should not initialize the tracer. - if (!clobberBailout && NODE_MAJOR < minMajor) { + if (!clobberBailout && (NODE_MAJOR < minMajor || NODE_MAJOR >= nextMajor)) { initBailout = true telemetry([ { name: 'abort', tags: ['reason:incompatible_runtime'] }, diff --git a/packages/dd-trace/src/id.js b/packages/dd-trace/src/id.js index c758917f043..4bacdcd421f 100644 --- a/packages/dd-trace/src/id.js +++ b/packages/dd-trace/src/id.js @@ -238,7 +238,7 @@ function writeUInt32BE (buffer, value, offset) { } /** - * @param {string} value + * @param {string} [value] * @param {number} [radix] * @returns {Identifier} */ diff --git a/packages/dd-trace/src/index.js b/packages/dd-trace/src/index.js index f83890f241c..0096ebecc7f 100644 --- a/packages/dd-trace/src/index.js +++ b/packages/dd-trace/src/index.js @@ -1,7 +1,7 @@ 'use strict' -const { isFalse } = require('./util') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const { isFalse } = require('./util') // Global `jest` is only present in Jest workers. const inJestWorker = typeof jest !== 'undefined' diff --git a/packages/dd-trace/src/lambda/handler.js b/packages/dd-trace/src/lambda/handler.js index 4a4f9fa653e..db9c70d5264 100644 --- a/packages/dd-trace/src/lambda/handler.js +++ b/packages/dd-trace/src/lambda/handler.js @@ -3,8 +3,8 @@ const log = require('../log') const { channel } = require('../../../datadog-instrumentations/src/helpers/instrument') const { ERROR_MESSAGE, ERROR_TYPE } = require('../constants') -const { ImpendingTimeout } = require('./runtime/errors') const { getEnvironmentVariable } = require('../config-helper') +const { ImpendingTimeout } = require('./runtime/errors') const globalTracer = global._ddtrace const tracer = globalTracer._tracer @@ -21,7 +21,7 @@ let __lambdaTimeout * Publishes to the `apm:aws:lambda:timeout` channel when * the AWS Lambda run time is about to end. * - * @param {*} context AWS Lambda context object. + * @param {object} context AWS Lambda context object. */ function checkTimeout (context) { const remainingTimeInMillis = context.getRemainingTimeInMillis() @@ -63,7 +63,7 @@ function crashFlush () { /** * Extracts the context from the given Lambda handler arguments. * - * @param {*} args any amount of arguments + * @param {unknown[]} args any amount of arguments * @returns the context, if extraction was succesful. */ function extractContext (args) { @@ -80,7 +80,7 @@ function extractContext (args) { /** * Patches your AWS Lambda handler function to add some tracing support. * - * @param {*} lambdaHandler a Lambda handler function. + * @param {Function} lambdaHandler a Lambda handler function. */ exports.datadog = function datadog (lambdaHandler) { return (...args) => { diff --git a/packages/dd-trace/src/lambda/index.js b/packages/dd-trace/src/lambda/index.js index ce0a5d783c9..7705b71d420 100644 --- a/packages/dd-trace/src/lambda/index.js +++ b/packages/dd-trace/src/lambda/index.js @@ -1,7 +1,7 @@ 'use strict' -const { registerLambdaHook } = require('./runtime/ritm') const { getEnvironmentVariable } = require('../config-helper') +const { registerLambdaHook } = require('./runtime/ritm') /** * It is safe to do it this way, since customers will never be expected to disable diff --git a/packages/dd-trace/src/lambda/runtime/patch.js b/packages/dd-trace/src/lambda/runtime/patch.js index f9af356dba6..010132ef112 100644 --- a/packages/dd-trace/src/lambda/runtime/patch.js +++ b/packages/dd-trace/src/lambda/runtime/patch.js @@ -2,17 +2,17 @@ const path = require('path') -const { _extractModuleNameAndHandlerPath, _extractModuleRootAndHandler, _getLambdaFilePaths } = require('./ritm') const { datadog } = require('../handler') const { addHook } = require('../../../../datadog-instrumentations/src/helpers/instrument') const shimmer = require('../../../../datadog-shimmer') const { getEnvironmentVariable } = require('../../config-helper') +const { _extractModuleNameAndHandlerPath, _extractModuleRootAndHandler, _getLambdaFilePaths } = require('./ritm') /** * Patches a Datadog Lambda module by calling `patchDatadogLambdaHandler` * with the handler name `datadog`. * - * @param {*} datadogLambdaModule node module to be patched. + * @param {object} datadogLambdaModule node module to be patched. * @returns a Datadog Lambda module with the `datadog` function from * `datadog-lambda-js` patched. */ @@ -27,7 +27,7 @@ const patchDatadogLambdaModule = (datadogLambdaModule) => { * Datadog instrumentation by getting the Lambda handler from its * arguments. * - * @param {*} datadogHandler the Datadog Lambda handler to destructure. + * @param {Function} datadogHandler the Datadog Lambda handler to destructure. * @returns the datadogHandler with its arguments patched. */ function patchDatadogLambdaHandler (datadogHandler) { @@ -51,7 +51,7 @@ const patchLambdaModule = (handlerPath) => (lambdaModule) => { /** * Patches a Lambda handler in order to do Datadog instrumentation. * - * @param {*} lambdaHandler the Lambda handler to be patched. + * @param {Function} lambdaHandler the Lambda handler to be patched. * @returns a function which patches the given Lambda handler. */ function patchLambdaHandler (lambdaHandler) { diff --git a/packages/dd-trace/src/lambda/runtime/ritm.js b/packages/dd-trace/src/lambda/runtime/ritm.js index f3d5a8e8d12..ad7edcc08df 100644 --- a/packages/dd-trace/src/lambda/runtime/ritm.js +++ b/packages/dd-trace/src/lambda/runtime/ritm.js @@ -61,7 +61,7 @@ function _extractModuleNameAndHandlerPath (handler) { /** * Returns all possible paths of the files to be patched when required. * - * @param {*} lambdaStylePath the path comprised of the `LAMBDA_TASK_ROOT`, + * @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. */ diff --git a/packages/dd-trace/src/llmobs/constants/tags.js b/packages/dd-trace/src/llmobs/constants/tags.js index d8f50d952b7..a2f904ffa84 100644 --- a/packages/dd-trace/src/llmobs/constants/tags.js +++ b/packages/dd-trace/src/llmobs/constants/tags.js @@ -36,5 +36,11 @@ module.exports = { CACHE_WRITE_INPUT_TOKENS_METRIC_KEY: 'cache_write_input_tokens', REASONING_OUTPUT_TOKENS_METRIC_KEY: 'reasoning_output_tokens', - DROPPED_IO_COLLECTION_ERROR: 'dropped_io' + DROPPED_IO_COLLECTION_ERROR: 'dropped_io', + + PROMPT_TRACKING_INSTRUMENTATION_METHOD: 'prompt_tracking_instrumentation_method', + PROMPT_MULTIMODAL: 'prompt_multimodal', + INSTRUMENTATION_METHOD_AUTO: 'auto', + INSTRUMENTATION_METHOD_ANNOTATED: 'annotated', + INSTRUMENTATION_METHOD_UNKNOWN: 'unknown' } diff --git a/packages/dd-trace/src/llmobs/index.js b/packages/dd-trace/src/llmobs/index.js index 93e5e3c4494..1fe01dcbbba 100644 --- a/packages/dd-trace/src/llmobs/index.js +++ b/packages/dd-trace/src/llmobs/index.js @@ -1,5 +1,9 @@ 'use strict' +const util = require('node:util') + +const { channel } = require('dc-polyfill') + const log = require('../log') const { ML_APP, @@ -7,24 +11,19 @@ const { PROPAGATED_PARENT_ID_KEY } = require('./constants/tags') const { storage } = require('./storage') - const telemetry = require('./telemetry') const LLMObsSpanProcessor = require('./span_processor') +const LLMObsEvalMetricsWriter = require('./writers/evaluations') +const LLMObsTagger = require('./tagger') +const LLMObsSpanWriter = require('./writers/spans') +const { setAgentStrategy } = require('./writers/util') -const { channel } = require('dc-polyfill') const spanFinishCh = channel('dd-trace:span:finish') const evalMetricAppendCh = channel('llmobs:eval-metric:append') const flushCh = channel('llmobs:writers:flush') const injectCh = channel('dd-trace:span:inject') const registerUserSpanProcessorCh = channel('llmobs:register-processor') -const LLMObsEvalMetricsWriter = require('./writers/evaluations') -const LLMObsTagger = require('./tagger') -const LLMObsSpanWriter = require('./writers/spans') -const { setAgentStrategy } = require('./writers/util') - -const util = require('node:util') - /** * Setting writers and processor globally when LLMObs is enabled * We're setting these in this module instead of on the SDK. diff --git a/packages/dd-trace/src/llmobs/plugins/ai/index.js b/packages/dd-trace/src/llmobs/plugins/ai/index.js index a49e1fc4b97..d502830bbb2 100644 --- a/packages/dd-trace/src/llmobs/plugins/ai/index.js +++ b/packages/dd-trace/src/llmobs/plugins/ai/index.js @@ -1,10 +1,9 @@ 'use strict' +const { channel } = require('dc-polyfill') const BaseLLMObsPlugin = require('../base') const { getModelProvider } = require('../../../../../datadog-plugin-ai/src/utils') -const { channel } = require('dc-polyfill') - const toolCreationCh = channel('dd-trace:vercel-ai:tool') const setAttributesCh = channel('dd-trace:vercel-ai:span:setAttributes') @@ -21,6 +20,38 @@ const { getLlmObsSpanName } = require('./util') +/** + * @typedef {Record & { description?: string, id?: string }} AvailableToolArgs + */ + +/** + * @typedef {string | number | boolean | null | undefined | string[] | number[] | boolean[]} TagValue + * @typedef {Record} SpanTags + * + * @typedef {{ name?: string, description?: string }} ToolForModel + * + * @typedef {{ type: 'text' | 'reasoning' | 'redacted-reasoning', text?: string, data?: string }} TextPart + * @typedef {{ type: 'tool-call', toolName: string, toolCallId: string, args?: unknown, input?: unknown }} ToolCallPart + * @typedef {( + * { type: 'tool-result', toolCallId: string, output?: { type: string, value?: unknown }, result?: unknown } & + * Record + * )} ToolResultPart + * + * @typedef {{ + * role: 'system', + * content: string + * } | { + * role: 'user', + * content: TextPart[] + * } | { + * role: 'assistant', + * content: Array + * } | { + * role: 'tool', + * content: ToolResultPart[] + * }} AiSdkMessage + */ + const SPAN_NAME_TO_KIND_MAPPING = { // embeddings embed: 'workflow', @@ -47,7 +78,7 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin { /** * The available tools within the runtime scope of this integration. * This essentially acts as a global registry for all tools made through the Vercel AI SDK. - * @type {Set>} + * @type {Set} */ #availableTools @@ -80,7 +111,7 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin { * We use the tool description as the next best identifier for a tool. * * @param {string} toolName - * @param {string} toolDescription + * @param {string | undefined} toolDescription * @returns {string | undefined} */ findToolName (toolName, toolDescription) { @@ -223,7 +254,7 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin { /** * @param {import('../../../opentracing/span')} span - * @param {Record} tags + * @param {SpanTags} tags */ setLLMOperationTags (span, tags) { const toolsForModel = tags['ai.prompt.tools']?.map(getJsonStringValue) @@ -290,8 +321,8 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin { * it is possible to have multiple tool call results in a single message that we * need to split into multiple messages. * - * @param {*} message - * @param {*} toolsForModel + * @param {AiSdkMessage} message + * @param {ToolForModel[] | null | undefined} toolsForModel * @returns {Array<{role: string, content: string, toolId?: string, * toolCalls?: Array<{arguments: string, name: string, toolId: string, type: string}>}>} */ diff --git a/packages/dd-trace/src/llmobs/plugins/ai/util.js b/packages/dd-trace/src/llmobs/plugins/ai/util.js index f788e2d514b..426885007e3 100644 --- a/packages/dd-trace/src/llmobs/plugins/ai/util.js +++ b/packages/dd-trace/src/llmobs/plugins/ai/util.js @@ -10,16 +10,35 @@ const MODEL_METADATA_KEYS = new Set([ 'stop_sequences' ]) +/** + * @typedef {import('../../../opentracing/span')} Span + * + * @typedef {string | number | boolean | null | undefined | string[] | number[] | boolean[]} TagValue + * @typedef {Record} SpanTags + * + * @typedef {{ span?: Span }} CurrentStore + * @typedef {{ currentStore?: CurrentStore, attributes?: SpanTags }} AiPluginContext + */ + +/** + * @typedef {{ + * type: string, + * value?: unknown + * }} ToolCallOutput + * + * @typedef {{ output?: ToolCallOutput, result?: unknown } & Record} ToolCallResultContent + */ + /** * Get the span tags from the context (either the attributes or the span tags). * - * @param {Record} ctx - * @returns {Record} + * @param {AiPluginContext} ctx + * @returns {SpanTags} */ function getSpanTags (ctx) { const span = ctx.currentStore?.span const carrier = ctx.attributes ?? span?.context()._tags ?? {} - return carrier + return /** @type {SpanTags} */ (carrier) } /** @@ -88,10 +107,11 @@ function getJsonStringValue (str, defaultValue) { /** * Get the model metadata from the span tags (top_p, top_k, temperature, etc.) - * @param {Record} tags - * @returns {Record | null} + * @param {SpanTags} tags + * @returns {Record | null} */ function getModelMetadata (tags) { + /** @type {Record} */ const modelMetadata = {} for (const metadata of MODEL_METADATA_KEYS) { const metadataTagKey = `gen_ai.request.${metadata}` @@ -106,10 +126,11 @@ function getModelMetadata (tags) { /** * Get the generation metadata from the span tags (maxSteps, maxRetries, etc.) - * @param {Record} tags - * @returns {Record | null} + * @param {SpanTags} tags + * @returns {Record | null} */ function getGenerationMetadata (tags) { + /** @type {Record} */ const metadata = {} for (const tag of Object.keys(tags)) { @@ -131,7 +152,7 @@ function getGenerationMetadata (tags) { * If the tool name is a parsable number, or is not found, null is returned. * Older versions of the ai sdk would tag the tool name as its index in the tools array. * - * @param {Record} tags + * @param {SpanTags} tags * @returns {string | null} */ function getToolNameFromTags (tags) { @@ -147,7 +168,7 @@ function getToolNameFromTags (tags) { /** * Get the content of a tool call result. * Version 5 of the ai sdk sets this tag as `content.output`, with a ` - * @param {Record} content + * @param {{ output?: { type: string, value: unknown }, result?: unknown }} content * @returns {string} */ function getToolCallResultContent (content) { diff --git a/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js b/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js index 9e8ef938019..b69f9b46280 100644 --- a/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +++ b/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js @@ -1,16 +1,16 @@ 'use strict' -const BaseLLMObsPlugin = require('./base') const { storage } = require('../../../../datadog-core') -const llmobsStore = storage('llmobs') const telemetry = require('../telemetry') - const { extractRequestParams, extractTextAndResponseReason, parseModelId, extractTextAndResponseReasonFromStream } = require('../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils') +const BaseLLMObsPlugin = require('./base') + +const llmobsStore = storage('llmobs') const ENABLED_OPERATIONS = new Set(['invokeModel', 'invokeModelWithResponseStream']) diff --git a/packages/dd-trace/src/llmobs/plugins/genai/util.js b/packages/dd-trace/src/llmobs/plugins/genai/util.js index 47484e12b2b..fcb88380a73 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai/util.js +++ b/packages/dd-trace/src/llmobs/plugins/genai/util.js @@ -258,7 +258,7 @@ function formatContentObject (content) { /** * Format input messages from contents - * @param {*} contents + * @param {[string|{text: string}|{parts: Array}]} [contents] * @returns {Array} */ function formatInputMessages (contents) { @@ -285,7 +285,7 @@ function formatInputMessages (contents) { /** * Format embedding input from contents - * @param {*} contents + * @param {[string|{text: string}|{parts: Array}]} [contents] * @returns {Array} */ function formatEmbeddingInput (contents) { diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js index 985ee018502..139fa4dec2e 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js @@ -1,7 +1,7 @@ 'use strict' -const LangChainLLMObsHandler = require('.') const { spanHasError } = require('../../../util') +const LangChainLLMObsHandler = require('.') class LangChainLLMObsChainHandler extends LangChainLLMObsHandler { setMetaTags ({ span, inputs, results }) { diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js index 1b7a2344067..86c7ca4642f 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js @@ -1,8 +1,8 @@ 'use strict' -const LangChainLLMObsHandler = require('.') const LLMObsTagger = require('../../../tagger') const { spanHasError } = require('../../../util') +const LangChainLLMObsHandler = require('.') const LLM = 'llm' diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js index 285fb1f0a96..68c8dd06b3c 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js @@ -1,8 +1,8 @@ 'use strict' -const LangChainLLMObsHandler = require('.') const LLMObsTagger = require('../../../tagger') const { spanHasError } = require('../../../util') +const LangChainLLMObsHandler = require('.') class LangChainLLMObsEmbeddingHandler extends LangChainLLMObsHandler { setMetaTags ({ span, inputs, results }) { diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js index 24f8db5c7c7..5c2c77141da 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js @@ -1,8 +1,8 @@ 'use strict' -const LangChainLLMObsHandler = require('.') const LLMObsTagger = require('../../../tagger') const { spanHasError } = require('../../../util') +const LangChainLLMObsHandler = require('.') class LangChainLLMObsLlmHandler extends LangChainLLMObsHandler { setMetaTags ({ span, inputs, results }) { diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/vectorstore.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/vectorstore.js index 02500a724da..c8c66f36765 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/vectorstore.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/vectorstore.js @@ -1,7 +1,7 @@ 'use strict' -const LangChainLLMObsHandler = require('.') const { spanHasError } = require('../../../util') +const LangChainLLMObsHandler = require('.') class LangChainLLMObsVectorStoreHandler extends LangChainLLMObsHandler { setMetaTags ({ span, inputs, results }) { diff --git a/packages/dd-trace/src/llmobs/plugins/openai/constants.js b/packages/dd-trace/src/llmobs/plugins/openai/constants.js new file mode 100644 index 00000000000..386e9532667 --- /dev/null +++ b/packages/dd-trace/src/llmobs/plugins/openai/constants.js @@ -0,0 +1,16 @@ +'use strict' + +const INPUT_TYPE_IMAGE = 'input_image' +const INPUT_TYPE_FILE = 'input_file' +const INPUT_TYPE_TEXT = 'input_text' + +const IMAGE_FALLBACK = '[image]' +const FILE_FALLBACK = '[file]' + +module.exports = { + INPUT_TYPE_IMAGE, + INPUT_TYPE_FILE, + INPUT_TYPE_TEXT, + IMAGE_FALLBACK, + FILE_FALLBACK +} diff --git a/packages/dd-trace/src/llmobs/plugins/openai/index.js b/packages/dd-trace/src/llmobs/plugins/openai/index.js index 59955dbe307..084fe3c8f6f 100644 --- a/packages/dd-trace/src/llmobs/plugins/openai/index.js +++ b/packages/dd-trace/src/llmobs/plugins/openai/index.js @@ -1,7 +1,17 @@ 'use strict' const LLMObsPlugin = require('../base') -const { extractChatTemplateFromInstructions, normalizePromptVariables, extractTextFromContentItem } = require('./utils') +const { + PROMPT_TRACKING_INSTRUMENTATION_METHOD, + PROMPT_MULTIMODAL, + INSTRUMENTATION_METHOD_AUTO +} = require('../../constants/tags') +const { + extractChatTemplateFromInstructions, + normalizePromptVariables, + extractTextFromContentItem, + hasMultimodalInputs +} = require('./utils') const allowedParamKeys = new Set([ 'max_output_tokens', @@ -422,6 +432,11 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin { variables: normalizedVariables, chat_template: chatTemplate }) + const tags = { [PROMPT_TRACKING_INSTRUMENTATION_METHOD]: INSTRUMENTATION_METHOD_AUTO } + if (hasMultimodalInputs(inputs.prompt.variables)) { + tags[PROMPT_MULTIMODAL] = 'true' + } + this._tagger.tagSpanTags(span, tags) } } diff --git a/packages/dd-trace/src/llmobs/plugins/openai/utils.js b/packages/dd-trace/src/llmobs/plugins/openai/utils.js index 1474ac9e068..fcf217e6f2e 100644 --- a/packages/dd-trace/src/llmobs/plugins/openai/utils.js +++ b/packages/dd-trace/src/llmobs/plugins/openai/utils.js @@ -1,7 +1,11 @@ 'use strict' -const IMAGE_FALLBACK = '[image]' -const FILE_FALLBACK = '[file]' +const { + INPUT_TYPE_IMAGE, + INPUT_TYPE_FILE, + IMAGE_FALLBACK, + FILE_FALLBACK +} = require('./constants') const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g @@ -11,8 +15,8 @@ const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g * Performs reverse templating: reconstructs the template by replacing actual values with {{variable_name}}. * For images/files: uses {{variable_name}} when values are available, falls back to [image]/[file] when stripped. * - * @param {Array} instructions - From Response.instructions (array of ResponseInputMessageItem) - * @param {Object} variables - Normalized variables (output of normalizePromptVariables) + * @param {Array} instructions - From Response.instructions (array of ResponseInputMessageItem) + * @param {Record} variables - Normalized variables (output of normalizePromptVariables) * @returns {Array<{role: string, content: string}>} Chat template with placeholders */ function extractChatTemplateFromInstructions (instructions, variables) { @@ -66,7 +70,7 @@ function extractChatTemplateFromInstructions (instructions, variables) { * Used for both input messages and chat template extraction. Falls back to [image]/[file] markers * when the actual values are stripped (e.g., by OpenAI's default URL stripping behavior). * - * @param {Object} contentItem - Content item from Response.instructions[].content (ResponseInputContentItem) + * @param {object} contentItem - Content item from Response.instructions[].content (ResponseInputContentItem) * @returns {string|null} Text content, URL/file reference, or [image]/[file] fallback marker */ function extractTextFromContentItem (contentItem) { @@ -77,11 +81,11 @@ function extractTextFromContentItem (contentItem) { } // For image/file items, extract the actual reference value - if (contentItem.type === 'input_image') { + if (contentItem.type === INPUT_TYPE_IMAGE) { return contentItem.image_url || contentItem.file_id || IMAGE_FALLBACK } - if (contentItem.type === 'input_file') { + if (contentItem.type === INPUT_TYPE_FILE) { return contentItem.file_id || contentItem.file_url || contentItem.filename || FILE_FALLBACK } @@ -93,8 +97,8 @@ function extractTextFromContentItem (contentItem) { * * Converts ResponseInputText, ResponseInputImage, and ResponseInputFile objects to simple string values. * - * @param {Object} variables - From ResponsePrompt.variables - * @returns {Object} Normalized variables with simple string values + * @param {Record} variables - From ResponsePrompt.variables + * @returns {Record} Normalized variables with simple string values */ function normalizePromptVariables (variables) { if (!variables) return {} @@ -107,8 +111,16 @@ function normalizePromptVariables (variables) { ) } +function hasMultimodalInputs (variables) { + if (!variables) return false + return Object.values(variables).some(value => + value?.type === INPUT_TYPE_IMAGE || value?.type === INPUT_TYPE_FILE + ) +} + module.exports = { extractChatTemplateFromInstructions, normalizePromptVariables, - extractTextFromContentItem + extractTextFromContentItem, + hasMultimodalInputs } diff --git a/packages/dd-trace/src/llmobs/plugins/vertexai.js b/packages/dd-trace/src/llmobs/plugins/vertexai.js index 45b9fc217b6..10d7a17fef6 100644 --- a/packages/dd-trace/src/llmobs/plugins/vertexai.js +++ b/packages/dd-trace/src/llmobs/plugins/vertexai.js @@ -1,10 +1,10 @@ 'use strict' -const LLMObsPlugin = require('./base') const { extractModel, extractSystemInstructions } = require('../../../../datadog-plugin-google-cloud-vertexai/src/utils') +const LLMObsPlugin = require('./base') class VertexAILLMObsPlugin extends LLMObsPlugin { static integration = 'vertexai' // used for llmobs telemetry diff --git a/packages/dd-trace/src/llmobs/sdk.js b/packages/dd-trace/src/llmobs/sdk.js index b058569c90e..5bde2d37434 100644 --- a/packages/dd-trace/src/llmobs/sdk.js +++ b/packages/dd-trace/src/llmobs/sdk.js @@ -1,26 +1,22 @@ 'use strict' -const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags') +const { channel } = require('dc-polyfill') +const { isTrue, isError } = require('../util') +const tracerVersion = require('../../../../package.json').version +const logger = require('../log') +const { getEnvironmentVariable } = require('../config-helper') +const Span = require('../opentracing/span') +const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags') const { getFunctionArguments, validateKind } = require('./util') -const { isTrue, isError } = require('../util') - const { storage } = require('./storage') - -const Span = require('../opentracing/span') - -const tracerVersion = require('../../../../package.json').version -const logger = require('../log') -const { getEnvironmentVariable } = require('../config-helper') const telemetry = require('./telemetry') - const LLMObsTagger = require('./tagger') // communicating with writer -const { channel } = require('dc-polyfill') const evalMetricAppendCh = channel('llmobs:eval-metric:append') const flushCh = channel('llmobs:writers:flush') const registerUserSpanProcessorCh = channel('llmobs:register-processor') @@ -53,23 +49,20 @@ class LLMObs extends NoopLLMObs { logger.debug('Enabling LLMObs') - const { mlApp, agentlessEnabled } = options - const DD_LLMOBS_ENABLED = getEnvironmentVariable('DD_LLMOBS_ENABLED') - const llmobsConfig = { - mlApp, - agentlessEnabled - } - - const enabled = DD_LLMOBS_ENABLED == null || isTrue(DD_LLMOBS_ENABLED) - if (!enabled) { + if (DD_LLMOBS_ENABLED != null && !isTrue(DD_LLMOBS_ENABLED)) { logger.debug('LLMObs.enable() called when DD_LLMOBS_ENABLED is false. No action taken.') return } - this._config.llmobs.enabled = true - this._config.configure({ ...this._config, llmobs: llmobsConfig }) + const llmobs = { + mlApp: options.mlApp, + agentlessEnabled: options.agentlessEnabled + } + // TODO: This will update config telemetry with the origin 'code', which is not ideal when `enable()` is called + // based on `APM_TRACING` RC product updates. + this._config.configure({ llmobs }) // configure writers and channel subscribers this._llmobsModule.enable(this._config) diff --git a/packages/dd-trace/src/llmobs/span_processor.js b/packages/dd-trace/src/llmobs/span_processor.js index b0069860a2e..74d74e4e52b 100644 --- a/packages/dd-trace/src/llmobs/span_processor.js +++ b/packages/dd-trace/src/llmobs/span_processor.js @@ -1,5 +1,14 @@ 'use strict' +const util = require('node:util') + +const tracerVersion = require('../../../../package.json').version +const logger = require('../log') +const { + ERROR_MESSAGE, + ERROR_TYPE, + ERROR_STACK +} = require('../constants') const { SPAN_KIND, MODEL_NAME, @@ -20,22 +29,9 @@ const { NAME } = require('./constants/tags') const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text') - -const { - ERROR_MESSAGE, - ERROR_TYPE, - ERROR_STACK -} = require('../constants') - const telemetry = require('./telemetry') - const LLMObsTagger = require('./tagger') -const tracerVersion = require('../../../../package.json').version -const logger = require('../log') - -const util = require('node:util') - class LLMObservabilitySpan { constructor () { this.input = [] diff --git a/packages/dd-trace/src/llmobs/telemetry.js b/packages/dd-trace/src/llmobs/telemetry.js index f188a385cb6..61235911fb5 100644 --- a/packages/dd-trace/src/llmobs/telemetry.js +++ b/packages/dd-trace/src/llmobs/telemetry.js @@ -1,5 +1,8 @@ 'use strict' +const ERROR_TYPE = require('../constants') + +const telemetryMetrics = require('../telemetry/metrics') const { SPAN_KIND, MODEL_PROVIDER, @@ -10,10 +13,6 @@ const { DECORATOR } = require('./constants/tags') -const ERROR_TYPE = require('../constants') - -const telemetryMetrics = require('../telemetry/metrics') - const LLMObsTagger = require('./tagger') const llmobsMetrics = telemetryMetrics.manager.namespace('mlobs') diff --git a/packages/dd-trace/src/llmobs/writers/base.js b/packages/dd-trace/src/llmobs/writers/base.js index 8f4e84b6e61..e910e418ccc 100644 --- a/packages/dd-trace/src/llmobs/writers/base.js +++ b/packages/dd-trace/src/llmobs/writers/base.js @@ -1,9 +1,9 @@ 'use strict' -const request = require('../../exporters/common/request') -const { getEnvironmentVariable } = require('../../config-helper') const { URL, format } = require('node:url') const path = require('node:path') +const request = require('../../exporters/common/request') +const { getEnvironmentVariable } = require('../../config-helper') const logger = require('../../log') diff --git a/packages/dd-trace/src/llmobs/writers/spans.js b/packages/dd-trace/src/llmobs/writers/spans.js index 8bf01c46980..7bcfb07fc4c 100644 --- a/packages/dd-trace/src/llmobs/writers/spans.js +++ b/packages/dd-trace/src/llmobs/writers/spans.js @@ -9,11 +9,10 @@ const { } = require('../constants/writers') const { DROPPED_VALUE_TEXT } = require('../constants/text') const { DROPPED_IO_COLLECTION_ERROR } = require('../constants/tags') -const BaseWriter = require('./base') const telemetry = require('../telemetry') const logger = require('../../log') - const tracerVersion = require('../../../../../package.json').version +const BaseWriter = require('./base') class LLMObsSpanWriter extends BaseWriter { constructor (config) { diff --git a/packages/dd-trace/src/log/index.js b/packages/dd-trace/src/log/index.js index 023b2d0e137..8fbfca1590a 100644 --- a/packages/dd-trace/src/log/index.js +++ b/packages/dd-trace/src/log/index.js @@ -1,11 +1,11 @@ 'use strict' const { inspect } = require('util') const { isTrue } = require('../util') +const { getEnvironmentVariable } = require('../config-helper') const { traceChannel, debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels') const logWriter = require('./writer') const { Log, LogConfig, NoTransmitError } = require('./log') const { memoize } = require('./utils') -const { getEnvironmentVariable } = require('../config-helper') const config = { enabled: false, diff --git a/packages/dd-trace/src/noop/proxy.js b/packages/dd-trace/src/noop/proxy.js index ac14bc246ea..d927b4ac4fd 100644 --- a/packages/dd-trace/src/noop/proxy.js +++ b/packages/dd-trace/src/noop/proxy.js @@ -1,11 +1,11 @@ 'use strict' -const NoopTracer = require('./tracer') const NoopAppsecSdk = require('../appsec/sdk/noop') -const NoopDogStatsDClient = require('./dogstatsd') const NoopLLMObsSDK = require('../llmobs/noop') const NoopFlaggingProvider = require('../openfeature/noop') const NoopAIGuardSDK = require('../aiguard/noop') +const NoopDogStatsDClient = require('./dogstatsd') +const NoopTracer = require('./tracer') const noop = new NoopTracer() const noopAppsec = new NoopAppsecSdk() diff --git a/packages/dd-trace/src/noop/span.js b/packages/dd-trace/src/noop/span.js index 28d2cc724e8..353fd24809f 100644 --- a/packages/dd-trace/src/noop/span.js +++ b/packages/dd-trace/src/noop/span.js @@ -1,8 +1,8 @@ 'use strict' -const NoopSpanContext = require('./span_context') const id = require('../id') const { storage } = require('../../../datadog-core') // TODO: noop storage? +const NoopSpanContext = require('./span_context') class NoopSpan { constructor (tracer, parent) { diff --git a/packages/dd-trace/src/openfeature/index.js b/packages/dd-trace/src/openfeature/index.js index 30480225839..df77b6b61b0 100644 --- a/packages/dd-trace/src/openfeature/index.js +++ b/packages/dd-trace/src/openfeature/index.js @@ -1,9 +1,9 @@ 'use strict' +const { channel } = require('dc-polyfill') const log = require('../log') const ExposuresWriter = require('./writers/exposures') const { setAgentStrategy } = require('./writers/util') -const { channel } = require('dc-polyfill') const exposureSubmitCh = channel('ffe:exposure:submit') const flushCh = channel('ffe:writers:flush') @@ -12,7 +12,7 @@ let exposuresWriter = null /** * @private - * @param {Object|Array} exposureEvents - Exposure events channel subscriber + * @param {object | Array} exposureEvents - Exposure events channel subscriber * @returns {void} */ function _handleExposureSubmit (exposureEvents) { diff --git a/packages/dd-trace/src/openfeature/noop.js b/packages/dd-trace/src/openfeature/noop.js index c41a11ca050..7d45602c6de 100644 --- a/packages/dd-trace/src/openfeature/noop.js +++ b/packages/dd-trace/src/openfeature/noop.js @@ -9,7 +9,7 @@ const { NOOP_REASON } = require('./constants/constants') */ class NoopFlaggingProvider { /** - * @param {Object} [noopTracer] - Optional noop tracer instance + * @param {object} [noopTracer] - Optional noop tracer instance */ constructor (noopTracer) { this._tracer = noopTracer @@ -22,8 +22,8 @@ class NoopFlaggingProvider { /** * @param {string} flagKey - Flag key * @param {boolean} defaultValue - Default value to return - * @param {Object} context - Evaluation context - * @param {Object} logger - Logger instance + * @param {object} context - Evaluation context + * @param {object} logger - Logger instance * @returns {Promise<{value: boolean, reason: string}>} Resolution details */ resolveBooleanEvaluation (flagKey, defaultValue, context, logger) { @@ -36,8 +36,8 @@ class NoopFlaggingProvider { /** * @param {string} flagKey - Flag key * @param {string} defaultValue - Default value to return - * @param {Object} context - Evaluation context - * @param {Object} logger - Logger instance + * @param {object} context - Evaluation context + * @param {object} logger - Logger instance * @returns {Promise<{value: string, reason: string}>} Resolution details */ resolveStringEvaluation (flagKey, defaultValue, context, logger) { @@ -50,8 +50,8 @@ class NoopFlaggingProvider { /** * @param {string} flagKey - Flag key * @param {number} defaultValue - Default value to return - * @param {Object} context - Evaluation context - * @param {Object} logger - Logger instance + * @param {object} context - Evaluation context + * @param {object} logger - Logger instance * @returns {Promise<{value: number, reason: string}>} Resolution details */ resolveNumberEvaluation (flagKey, defaultValue, context, logger) { @@ -63,10 +63,10 @@ class NoopFlaggingProvider { /** * @param {string} flagKey - Flag key - * @param {Object} defaultValue - Default value to return - * @param {Object} context - Evaluation context - * @param {Object} logger - Logger instance - * @returns {Promise<{value: Object, reason: string}>} Resolution details + * @param {object} defaultValue - Default value to return + * @param {object} context - Evaluation context + * @param {object} logger - Logger instance + * @returns {Promise<{value: object, reason: string}>} Resolution details */ resolveObjectEvaluation (flagKey, defaultValue, context, logger) { return Promise.resolve({ @@ -76,14 +76,14 @@ class NoopFlaggingProvider { } /** - * @returns {Object} Current configuration + * @returns {object} Current configuration */ getConfiguration () { return this._config } /** - * @param {Object} config - Configuration to set + * @param {object} config - Configuration to set */ setConfiguration (config) { this._config = config @@ -91,7 +91,7 @@ class NoopFlaggingProvider { /** * @internal - * @param {Object} ufc - Universal Flag Configuration object + * @param {object} ufc - Universal Flag Configuration object */ _setConfiguration (ufc) { this.setConfiguration(ufc) diff --git a/packages/dd-trace/src/openfeature/remote_config.js b/packages/dd-trace/src/openfeature/remote_config.js new file mode 100644 index 00000000000..279176353aa --- /dev/null +++ b/packages/dd-trace/src/openfeature/remote_config.js @@ -0,0 +1,31 @@ +'use strict' + +const RemoteConfigCapabilities = require('../remote_config/capabilities') + +/** + * Configures remote config handlers for openfeature feature flagging + * + * @param {object} rc - RemoteConfig instance + * @param {object} config - Tracer config + * @param {Function} getOpenfeatureProxy - Function that returns the OpenFeature proxy from tracer + */ +function enable (rc, config, getOpenfeatureProxy) { + // Always enable capability for feature flag configuration + // This indicates the library supports this capability via remote config + rc.updateCapabilities(RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true) + + // Only register product handler if the experimental feature is enabled + if (!config.experimental.flaggingProvider.enabled) return + + // Set product handler for FFE_FLAGS + rc.setProductHandler('FFE_FLAGS', (action, conf) => { + // Feed UFC config directly to OpenFeature provider + if (action === 'apply' || action === 'modify') { + getOpenfeatureProxy()._setConfiguration(conf) + } + }) +} + +module.exports = { + enable +} diff --git a/packages/dd-trace/src/openfeature/writers/base.js b/packages/dd-trace/src/openfeature/writers/base.js index 462280bf5d8..43d44c04d72 100644 --- a/packages/dd-trace/src/openfeature/writers/base.js +++ b/packages/dd-trace/src/openfeature/writers/base.js @@ -1,21 +1,21 @@ 'use strict' +const { URL, format } = require('node:url') const request = require('../../exporters/common/request') const { safeJSONStringify } = require('../../exporters/common/util') -const { URL, format } = require('node:url') const log = require('../../log') /** - * @typedef {Object} BaseFFEWriterOptions + * @typedef {object} BaseFFEWriterOptions * @property {number} [interval] - Flush interval in milliseconds * @property {number} [timeout] - Request timeout in milliseconds - * @property {Object} config - Tracer configuration object + * @property {object} config - Tracer configuration object * @property {string} endpoint - API endpoint path * @property {URL} [agentUrl] - Base URL for the agent * @property {number} [payloadSizeLimit] - Maximum payload size in bytes * @property {number} [eventSizeLimit] - Maximum individual event size in bytes - * @property {Object} [headers] - Additional HTTP headers + * @property {object} [headers] - Additional HTTP headers */ /** @@ -67,7 +67,7 @@ class BaseFFEWriter { /** * Appends an event array to the buffer - * @param {Array|Object} events - Event object(s) to append to buffer + * @param {Array | object} events - Event object(s) to append to buffer */ append (events) { const eventArray = Array.isArray(events) ? events : [events] diff --git a/packages/dd-trace/src/openfeature/writers/exposures.js b/packages/dd-trace/src/openfeature/writers/exposures.js index 97e59e81ca0..c111b61b7a8 100644 --- a/packages/dd-trace/src/openfeature/writers/exposures.js +++ b/packages/dd-trace/src/openfeature/writers/exposures.js @@ -1,6 +1,5 @@ 'use strict' -const BaseFFEWriter = require('./base') const { EXPOSURES_ENDPOINT, EVP_PROXY_AGENT_BASE_PATH, @@ -9,31 +8,32 @@ const { EVP_PAYLOAD_SIZE_LIMIT, EVP_EVENT_SIZE_LIMIT } = require('../constants/constants') +const BaseFFEWriter = require('./base') /** - * @typedef {Object} ExposureEvent + * @typedef {object} ExposureEvent * @property {number} timestamp - Unix timestamp in milliseconds - * @property {Object} allocation - Allocation information + * @property {object} allocation - Allocation information * @property {string} allocation.key - Allocation key - * @property {Object} flag - Flag information + * @property {object} flag - Flag information * @property {string} flag.key - Flag key - * @property {Object} variant - Variant information + * @property {object} variant - Variant information * @property {string} variant.key - Variant key - * @property {Object} subject - Subject (user/entity) information + * @property {object} subject - Subject (user/entity) information * @property {string} subject.id - Subject identifier * @property {string} [subject.type] - Subject type - * @property {Object} [subject.attributes] - Additional subject attributes + * @property {object} [subject.attributes] - Additional subject attributes */ /** - * @typedef {Object} ExposureContext + * @typedef {object} ExposureContext * @property {string} service - Service name * @property {string} [version] - Service version * @property {string} [env] - Service environment */ /** - * @typedef {Object} ExposureEventPayload + * @typedef {object} ExposureEventPayload * @property {ExposureContext} context - Service context metadata * @property {ExposureEvent[]} exposures - Formatted exposure events */ diff --git a/packages/dd-trace/src/opentelemetry/context_manager.js b/packages/dd-trace/src/opentelemetry/context_manager.js index 15d5a7b09ef..c963f687d84 100644 --- a/packages/dd-trace/src/opentelemetry/context_manager.js +++ b/packages/dd-trace/src/opentelemetry/context_manager.js @@ -1,11 +1,11 @@ 'use strict' -const { storage } = require('../../../datadog-core') const { trace, ROOT_CONTEXT, propagation } = require('@opentelemetry/api') +const { storage } = require('../../../datadog-core') const DataDogSpanContext = require('../opentracing/span_context') -const SpanContext = require('./span_context') const tracer = require('../../') +const SpanContext = require('./span_context') class ContextManager { constructor () { diff --git a/packages/dd-trace/src/opentelemetry/logs/logger.js b/packages/dd-trace/src/opentelemetry/logs/logger.js index 7982654d7f2..81416302a02 100644 --- a/packages/dd-trace/src/opentelemetry/logs/logger.js +++ b/packages/dd-trace/src/opentelemetry/logs/logger.js @@ -1,7 +1,7 @@ 'use strict' -const { sanitizeAttributes } = require('../../../../../vendor/dist/@opentelemetry/core') const { context } = require('@opentelemetry/api') +const { sanitizeAttributes } = require('../../../../../vendor/dist/@opentelemetry/core') const { VERSION: packageVersion } = require('../../../../../version') /** diff --git a/packages/dd-trace/src/opentelemetry/logs/logger_provider.js b/packages/dd-trace/src/opentelemetry/logs/logger_provider.js index 83a85006033..53f1293700d 100644 --- a/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +++ b/packages/dd-trace/src/opentelemetry/logs/logger_provider.js @@ -1,9 +1,9 @@ 'use strict' const { logs } = require('@opentelemetry/api-logs') const { context } = require('@opentelemetry/api') -const Logger = require('./logger') const log = require('../../log') const ContextManager = require('../context_manager') +const Logger = require('./logger') /** * @typedef {import('@opentelemetry/api-logs').Logger} Logger @@ -26,7 +26,7 @@ class LoggerProvider { /** * Creates a new LoggerProvider instance with a single processor for Datadog Agent export. * - * @param {Object} [options] - LoggerProvider options + * @param {object} [options] - LoggerProvider options * @param {BatchLogRecordProcessor} [options.processor] - Single LogRecordProcessor instance for * exporting logs to Datadog Agent */ @@ -40,9 +40,9 @@ class LoggerProvider { /** * Gets or creates a logger instance. * - * @param {string|Object} nameOrOptions - Logger name or options object + * @param {string | object} nameOrOptions - Logger name or options object * @param {string} [version] - Logger version (when nameOrOptions is a string) - * @param {Object} [options] - Additional options (when nameOrOptions is a string) + * @param {object} [options] - Additional options (when nameOrOptions is a string) * @returns {Logger} Logger instance */ getLogger (nameOrOptions, version, options = {}) { diff --git a/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js index 6267fb63187..078dd2d0ca6 100644 --- a/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +++ b/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js @@ -1,10 +1,11 @@ 'use strict' -const OtlpTransformerBase = require('../otlp/otlp_transformer_base') const { SeverityNumber } = require('@opentelemetry/api-logs') -const { getProtobufTypes } = require('../otlp/protobuf_loader') const { trace } = require('@opentelemetry/api') +const OtlpTransformerBase = require('../otlp/otlp_transformer_base') +const { getProtobufTypes } = require('../otlp/protobuf_loader') + /** * @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord */ @@ -105,7 +106,7 @@ class OtlpTransformer extends OtlpTransformerBase { /** * Creates scope logs grouped by instrumentation library. * @param {LogRecord[]} logRecords - Array of log records to transform - * @returns {Object[]} Array of scope log objects + * @returns {object[]} Array of scope log objects */ #transformScope (logRecords) { const groupedRecords = this.groupByInstrumentationScope(logRecords) @@ -131,7 +132,7 @@ class OtlpTransformer extends OtlpTransformerBase { /** * Transforms a single log record to OTLP format. * @param {LogRecord} logRecord - Log record to transform - * @returns {Object} OTLP log record object + * @returns {object} OTLP log record object */ #transformLogRecord (logRecord) { const spanContext = this.#extractSpanContext(logRecord.context) @@ -176,8 +177,8 @@ class OtlpTransformer extends OtlpTransformerBase { /** * Extracts span context from the log record's context. - * @param {Object} logContext - The log record's context - * @returns {Object|null} Span context or null if not available + * @param {object} logContext - The log record's context + * @returns {object | null} Span context or null if not available */ #extractSpanContext (logContext) { if (!logContext) return null @@ -214,8 +215,8 @@ class OtlpTransformer extends OtlpTransformerBase { /** * Transforms log body to OTLP AnyValue format. - * @param {any} body - Log body to transform - * @returns {Object} OTLP AnyValue object + * @param {import('@opentelemetry/api-logs').LogBody} body - Log body to transform + * @returns {object} OTLP AnyValue object */ #transformBody (body) { if (typeof body === 'string') { diff --git a/packages/dd-trace/src/opentelemetry/metrics/instruments.js b/packages/dd-trace/src/opentelemetry/metrics/instruments.js index 57c55e835bd..ee34db8cdd3 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/instruments.js +++ b/packages/dd-trace/src/opentelemetry/metrics/instruments.js @@ -9,7 +9,7 @@ const { METRIC_TYPES } = require('./constants') */ /** - * @typedef {Object} Measurement + * @typedef {object} Measurement * @property {string} name - Instrument name * @property {string} description - Instrument description * @property {string} unit - Measurement unit @@ -30,11 +30,11 @@ class Instrument { * Creates a new instrument instance. * * @param {string} name - Instrument name (e.g., 'http.request.duration') - * @param {Object} options - Instrument configuration options + * @param {object} options - Instrument configuration options * @param {string} [options.description] - Human-readable description of the instrument * @param {string} [options.unit] - Unit of measurement (e.g., 'ms', 'bytes', '1') * @param {InstrumentationScope} instrumentationScope - Instrumentation scope for this instrument - * @param {Object} reader - Metric reader for recording measurements + * @param {object} reader - Metric reader for recording measurements */ constructor (name, options, instrumentationScope, reader) { this.name = name diff --git a/packages/dd-trace/src/opentelemetry/metrics/meter.js b/packages/dd-trace/src/opentelemetry/metrics/meter.js index 5e03b39e62a..89a50c03f0d 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/meter.js +++ b/packages/dd-trace/src/opentelemetry/metrics/meter.js @@ -1,10 +1,10 @@ 'use strict' const { VERSION: packageVersion } = require('../../../../../version') +const log = require('../../log') const { Counter, UpDownCounter, Histogram, Gauge, ObservableGauge, ObservableCounter, ObservableUpDownCounter } = require('./instruments') -const log = require('../../log') const { METRIC_TYPES } = require('./constants') /** @@ -31,7 +31,7 @@ class Meter { * @param {string} [instrumentationScope.name] - Meter name (defaults to 'dd-trace-js') * @param {string} [instrumentationScope.version] - Meter version (defaults to tracer version) * @param {string} [instrumentationScope.schemaUrl] - Schema URL - * @param {Object} [instrumentationScope.attributes] - Attributes for the instrumentation scope + * @param {object} [instrumentationScope.attributes] - Attributes for the instrumentation scope */ constructor ( meterProvider, diff --git a/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js index 1d200ab3720..65599de2d91 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +++ b/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js @@ -85,7 +85,7 @@ class OtlpTransformer extends OtlpTransformerBase { * Creates scope metrics grouped by instrumentation scope. * @param {Iterable} metrics - Iterable of metrics to transform * @param {boolean} isJson - Whether to format for JSON output - * @returns {Array} Array of scope metric objects + * @returns {Array} Array of scope metric objects * */ #transformScope (metrics, isJson = false) { @@ -125,7 +125,7 @@ class OtlpTransformer extends OtlpTransformerBase { * * @param {AggregatedMetric} metric - The metric to transform * @param {boolean} isJson - Whether to output JSON format (vs protobuf) - * @returns {Object} - The metric transformed to OTLP protobuf or JSON format + * @returns {object} - The metric transformed to OTLP protobuf or JSON format */ #transformMetric (metric, isJson = false) { const result = { @@ -176,7 +176,7 @@ class OtlpTransformer extends OtlpTransformerBase { * * @param {HistogramDataPoint} dp - The histogram data point to transform * @param {boolean} isJson - Whether to output JSON format (vs protobuf) - * @returns {Object} The histogram data point transformed to OTLP protobuf format + * @returns {object} The histogram data point transformed to OTLP protobuf format */ #transformHistogramDataPoint (dp, isJson) { const attributes = isJson @@ -209,7 +209,7 @@ class OtlpTransformer extends OtlpTransformerBase { * * @param {NumberDataPoint} dataPoint - The number data point to transform * @param {boolean} isJson - Whether to output JSON format (vs protobuf) - * @returns {Object} The number data point transformed to OTLP protobuf format + * @returns {object} The number data point transformed to OTLP protobuf format */ #transformNumberDataPoint (dataPoint, isJson) { const attributes = isJson 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 8abea441ad5..c871d960bc9 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +++ b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js @@ -1,10 +1,10 @@ 'use strict' +const log = require('../../log') +const { stableStringify } = require('../otlp/otlp_transformer_base') const { METRIC_TYPES, TEMPORALITY, DEFAULT_HISTOGRAM_BUCKETS, DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE } = require('./constants') -const log = require('../../log') -const { stableStringify } = require('../otlp/otlp_transformer_base') /** * @typedef {import('@opentelemetry/api').Attributes} Attributes @@ -13,7 +13,32 @@ const { stableStringify } = require('../otlp/otlp_transformer_base') */ /** - * @typedef {Object} NumberDataPoint + * @typedef {{ value: number, startTime: number }} SumCumulativeState + * + * @typedef {{ + * count: number, + * sum: number, + * min: number, + * max: number, + * bucketCounts: number[], + * startTime: number + * }} HistogramCumulativeState + * + * @typedef {SumCumulativeState | HistogramCumulativeState} CumulativeStateValue + * + * @typedef {{ + * count: number, + * sum: number, + * min?: number, + * max?: number, + * bucketCounts: number[] + * }} HistogramLastExportedState + * + * @typedef {number | HistogramLastExportedState} LastExportedStateValue + */ + +/** + * @typedef {object} NumberDataPoint * @property {Attributes} attributes - Number data point metric attributes * @property {string} attrKey - Stable stringified key for attributes * @property {number} timeUnixNano - Timestamp in nanoseconds @@ -22,7 +47,7 @@ const { stableStringify } = require('../otlp/otlp_transformer_base') */ /** - * @typedef {Object} HistogramDataPoint + * @typedef {object} HistogramDataPoint * @property {Attributes} attributes - Histogram data point metric attributes * @property {string} attrKey - Stable stringified key for attributes * @property {number} timeUnixNano - Timestamp in nanoseconds @@ -36,7 +61,7 @@ const { stableStringify } = require('../otlp/otlp_transformer_base') */ /** - * @typedef {Object} AggregatedMetricDataPoint + * @typedef {object} AggregatedMetricDataPoint * @property {Attributes} attributes - Aggregated metric data point metric attributes * @property {string} attrKey - Stable stringified key for attributes * @property {number} timeUnixNano - Timestamp in nanoseconds @@ -50,7 +75,7 @@ const { stableStringify } = require('../otlp/otlp_transformer_base') */ /** - * @typedef {Object} AggregatedMetric + * @typedef {object} AggregatedMetric * @property {string} name - Metric name * @property {string} description - Metric description * @property {string} unit - Metric unit @@ -255,8 +280,8 @@ class MetricAggregator { * Aggregates measurements into metrics. * * @param {Measurement[]} measurements - The measurements to aggregate - * @param {Map} cumulativeState - The cumulative state of the metrics - * @param {Map} lastExportedState - The last exported state of the metrics + * @param {Map} cumulativeState - The cumulative state of the metrics + * @param {Map} lastExportedState - The last exported state of the metrics * @returns {Iterable} The aggregated metrics */ aggregate (measurements, cumulativeState, lastExportedState) { @@ -353,7 +378,7 @@ class MetricAggregator { * Applies delta temporality to the metrics. * * @param {Iterable} metrics - The metrics to apply delta temporality to - * @param {Map} lastExportedState - The last exported state of the metrics + * @param {Map} lastExportedState - The last exported state of the metrics * @returns {void} */ #applyDeltaTemporality (metrics, lastExportedState) { @@ -424,7 +449,7 @@ class MetricAggregator { * @param {string} attrKey - The attribute key * @param {number} timestamp - The timestamp of the measurement * @param {string} stateKey - The state key - * @param {Map} cumulativeState - The cumulative state of the metrics + * @param {Map} cumulativeState - The cumulative state of the metrics */ #aggregateSum (metric, value, attributes, attrKey, timestamp, stateKey, cumulativeState) { if (!cumulativeState.has(stateKey)) { @@ -478,7 +503,7 @@ class MetricAggregator { * @param {string} attrKey - The attribute key * @param {number} timestamp - The timestamp of the measurement * @param {string} stateKey - The state key - * @param {Map} cumulativeState - The cumulative state of the metrics + * @param {Map} cumulativeState - The cumulative state of the metrics * @returns {void} */ #aggregateHistogram (metric, value, attributes, attrKey, timestamp, stateKey, cumulativeState) { diff --git a/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js b/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js index 4428494571d..3aea6d23b89 100644 --- a/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +++ b/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js @@ -4,6 +4,7 @@ const log = require('../../log') /** * @typedef {import('@opentelemetry/api').Attributes} Attributes + * @typedef {import('@opentelemetry/api').AttributeValue} AttributeValue */ /** @@ -61,7 +62,7 @@ class OtlpTransformerBase { /** * Transforms resource attributes to OTLP resource format. - * @returns {Object} OTLP resource object + * @returns {object} OTLP resource object * @protected */ transformResource () { @@ -73,8 +74,8 @@ class OtlpTransformerBase { /** * Transforms attributes to OTLP KeyValue format. - * @param {Object} attributes - Attributes to transform - * @returns {Object[]} Array of OTLP KeyValue objects + * @param {Attributes} attributes - Attributes to transform + * @returns {object[]} Array of OTLP KeyValue objects * @protected */ transformAttributes (attributes) { @@ -86,8 +87,8 @@ class OtlpTransformerBase { /** * Transforms attributes to JSON format (simplified). - * @param {Object} attributes - Attributes to transform - * @returns {Object[]} Array of OTLP KeyValue objects with string values + * @param {object} attributes - Attributes to transform + * @returns {object[]} Array of OTLP KeyValue objects with string values * @protected */ attributesToJson (attributes) { @@ -103,8 +104,8 @@ class OtlpTransformerBase { * Transforms any value to OTLP AnyValue format. * Supports: strings, numbers (int/double), booleans, arrays. * Objects are filtered out by sanitizeAttributes before reaching this method. - * @param {any} value - Value to transform - * @returns {Object} OTLP AnyValue object + * @param {AttributeValue | null | undefined} value - Value to transform + * @returns {object} OTLP AnyValue object * @protected */ transformAnyValue (value) { @@ -130,8 +131,8 @@ class OtlpTransformerBase { /** * Serializes data to protobuf format. - * @param {Object} protoType - Protobuf type from protobuf_loader - * @param {Object} data - Data to serialize + * @param {object} protoType - Protobuf type from protobuf_loader + * @param {object} data - Data to serialize * @returns {Buffer} Protobuf-encoded data * @protected */ @@ -143,7 +144,7 @@ class OtlpTransformerBase { /** * Serializes data to JSON format. - * @param {Object} data - Data to serialize + * @param {object} data - Data to serialize * @returns {Buffer} JSON-encoded data * @protected */ diff --git a/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js b/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js index 0d59b5eaf48..82e9ef88e30 100644 --- a/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js +++ b/packages/dd-trace/src/opentelemetry/otlp/protobuf_loader.js @@ -14,8 +14,8 @@ * - https://github.com/open-telemetry/opentelemetry-proto (v1.7.0) */ -const protobuf = require('../../../../../vendor/dist/protobufjs') const path = require('path') +const protobuf = require('../../../../../vendor/dist/protobufjs') let _root = null let protoLogsService = null diff --git a/packages/dd-trace/src/opentelemetry/span.js b/packages/dd-trace/src/opentelemetry/span.js index 47bc2aeacd5..3b8bc481658 100644 --- a/packages/dd-trace/src/opentelemetry/span.js +++ b/packages/dd-trace/src/opentelemetry/span.js @@ -1,8 +1,8 @@ 'use strict' +const { performance } = require('perf_hooks') const api = require('@opentelemetry/api') -const { performance } = require('perf_hooks') const { timeOrigin } = performance const { timeInputToHrTime } = require('../../../../vendor/dist/@opentelemetry/core') @@ -13,8 +13,8 @@ const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK, IGNORE_OTEL_ERROR } = require('. const { SERVICE_NAME, RESOURCE_NAME, SPAN_KIND } = require('../../../../ext/tags') const kinds = require('../../../../ext/kinds') -const SpanContext = require('./span_context') const id = require('../id') +const SpanContext = require('./span_context') // The one built into OTel rounds so we lose sub-millisecond precision. function hrTimeToMilliseconds (time) { diff --git a/packages/dd-trace/src/opentelemetry/tracer.js b/packages/dd-trace/src/opentelemetry/tracer.js index 11579d0582b..ec6f66cbce7 100644 --- a/packages/dd-trace/src/opentelemetry/tracer.js +++ b/packages/dd-trace/src/opentelemetry/tracer.js @@ -3,13 +3,13 @@ const api = require('@opentelemetry/api') const { sanitizeAttributes } = require('../../../../vendor/dist/@opentelemetry/core') -const Sampler = require('./sampler') -const Span = require('./span') const id = require('../id') const log = require('../log') -const SpanContext = require('./span_context') const TextMapPropagator = require('../opentracing/propagation/text_map') const TraceState = require('../opentracing/propagation/tracestate') +const SpanContext = require('./span_context') +const Span = require('./span') +const Sampler = require('./sampler') class Tracer { constructor (library, config, tracerProvider) { diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index 3d7668dfcd3..3f5297ed18d 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -1,16 +1,16 @@ 'use strict' +const { channel } = require('dc-polyfill') const pick = require('../../../../datadog-core/src/utils/src/pick') const id = require('../../id') const DatadogSpanContext = require('../span_context') const log = require('../../log') -const TraceState = require('./tracestate') const tags = require('../../../../../ext/tags') -const { channel } = require('dc-polyfill') const { setBaggageItem, getAllBaggageItems, removeAllBaggageItems } = require('../../baggage') const telemetryMetrics = require('../../telemetry/metrics') const { AUTO_KEEP, AUTO_REJECT, USER_KEEP } = require('../../../../../ext/priority') +const TraceState = require('./tracestate') const tracerMetrics = telemetryMetrics.manager.namespace('tracers') @@ -36,6 +36,9 @@ const b3HeaderExpr = /^(([0-9a-f]{16}){1,2}-[0-9a-f]{16}(-[01d](-[0-9a-f]{16})?) const baggageExpr = new RegExp(`^${baggagePrefix}(.+)$`) const tagKeyExpr = /^_dd\.p\.[\x21-\x2B\x2D-\x7E]+$/ // ASCII minus spaces and commas const tagValueExpr = /^[\x20-\x2B\x2D-\x7E]*$/ // ASCII minus commas +// RFC7230 token (used by HTTP header field-name) and compatible with Node's header name validation. +// See https://www.rfc-editor.org/rfc/rfc7230#section-3.2.6 +const httpHeaderNameExpr = /^[0-9A-Za-z!#$%&'*+\-.^_`|~]+$/ const traceparentExpr = /^([a-f0-9]{2})-([a-f0-9]{32})-([a-f0-9]{16})-([a-f0-9]{2})(-.*)?$/i const traceparentKey = 'traceparent' const tracestateKey = 'tracestate' @@ -130,9 +133,21 @@ class TextMapPropagator { _injectBaggageItems (spanContext, carrier) { if (this._config.legacyBaggageEnabled) { - spanContext?._baggageItems && Object.keys(spanContext._baggageItems).forEach(key => { - carrier[baggagePrefix + key] = String(spanContext._baggageItems[key]) - }) + const baggageItems = spanContext?._baggageItems + if (baggageItems) { + for (const key of Object.keys(baggageItems)) { + const headerName = baggagePrefix + key + + // Legacy OpenTracing baggage is propagated as individual headers (ot-baggage-*), + // so it must always be representable as a valid HTTP header name. + if (!httpHeaderNameExpr.test(headerName)) { + tracerMetrics.count('context_header_style.malformed', ['header_style:baggage']).inc() + continue + } + + carrier[headerName] = String(baggageItems[key]) + } + } } if (this._hasPropagationStyle('inject', 'baggage')) { let baggage = '' @@ -646,9 +661,10 @@ class TextMapPropagator { _extractBaggageItems (carrier, spanContext) { if (!this._hasPropagationStyle('extract', 'baggage')) return - if (!carrier || !carrier.baggage) return + if (!carrier?.baggage) return const baggages = carrier.baggage.split(',') - const keysToSpanTag = this._config.baggageTagKeys === '*' + const tagAllKeys = this._config.baggageTagKeys === '*' + const keysToSpanTag = tagAllKeys ? undefined : new Set(this._config.baggageTagKeys.split(',')) for (const keyValue of baggages) { @@ -665,7 +681,7 @@ class TextMapPropagator { removeAllBaggageItems() return } - if (spanContext && (this._config.baggageTagKeys === '*' || keysToSpanTag.has(key))) { + if (spanContext && (tagAllKeys || keysToSpanTag?.has(key))) { spanContext._trace.tags['baggage.' + key] = value } setBaggageItem(key, value) diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index 41c0988c82f..94f494470c5 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -4,16 +4,16 @@ const { performance } = require('perf_hooks') const now = performance.now.bind(performance) const dateNow = Date.now -const SpanContext = require('./span_context') +const util = require('util') +const { channel } = require('dc-polyfill') const id = require('../id') const tagger = require('../tagger') const runtimeMetrics = require('../runtime_metrics') const log = require('../log') const { storage } = require('../../../datadog-core') const telemetryMetrics = require('../telemetry/metrics') -const { channel } = require('dc-polyfill') -const util = require('util') const { getEnvironmentVariable } = require('../config-helper') +const SpanContext = require('./span_context') const tracerMetrics = telemetryMetrics.manager.namespace('tracers') diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index 9fb8a467a80..6e6024a3e25 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -1,19 +1,19 @@ 'use strict' const os = require('os') -const Span = require('./span') const SpanProcessor = require('../span_processor') const PrioritySampler = require('../priority_sampler') +const formats = require('../../../../ext/formats') +const log = require('../log') +const runtimeMetrics = require('../runtime_metrics') +const getExporter = require('../exporter') +const Span = require('./span') const TextMapPropagator = require('./propagation/text_map') const DSMTextMapPropagator = require('./propagation/text_map_dsm') const HttpPropagator = require('./propagation/http') const BinaryPropagator = require('./propagation/binary') const LogPropagator = require('./propagation/log') -const formats = require('../../../../ext/formats') -const log = require('../log') -const runtimeMetrics = require('../runtime_metrics') -const getExporter = require('../exporter') const SpanContext = require('./span_context') const REFERENCE_CHILD_OF = 'child_of' diff --git a/packages/dd-trace/src/payload-tagging/index.js b/packages/dd-trace/src/payload-tagging/index.js index 4ef77963601..7d4dcd2676c 100644 --- a/packages/dd-trace/src/payload-tagging/index.js +++ b/packages/dd-trace/src/payload-tagging/index.js @@ -38,7 +38,9 @@ function maybeJSONParseValue (value) { function expand (object, expansionRules) { for (const rule of expansionRules) { jsonpath(rule, object, (value, _type, desc) => { - desc.parent[desc.parentProperty] = maybeJSONParseValue(value) + if (desc.parent && desc.parentProperty) { + desc.parent[desc.parentProperty] = maybeJSONParseValue(value) + } }) } } @@ -52,7 +54,9 @@ function expand (object, expansionRules) { function redact (object, redactionRules) { for (const rule of redactionRules) { jsonpath(rule, object, (_value, _type, desc) => { - desc.parent[desc.parentProperty] = 'redacted' + if (desc.parent && desc.parentProperty) { + desc.parent[desc.parentProperty] = 'redacted' + } }) } } diff --git a/packages/dd-trace/src/plugin_manager.js b/packages/dd-trace/src/plugin_manager.js index 337330d4ba5..15db904d2d3 100644 --- a/packages/dd-trace/src/plugin_manager.js +++ b/packages/dd-trace/src/plugin_manager.js @@ -1,10 +1,10 @@ 'use strict' const { channel } = require('dc-polyfill') +const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const { isFalse, isTrue, normalizePluginEnvName } = require('./util') const plugins = require('./plugins') const log = require('./log') -const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') // Test optimization plugins that should only be enabled when isCiVisibility is true const TEST_OPTIMIZATION_PLUGINS = new Set([ diff --git a/packages/dd-trace/src/plugins/apollo.js b/packages/dd-trace/src/plugins/apollo.js index 936e1c36d3b..45cd417e169 100644 --- a/packages/dd-trace/src/plugins/apollo.js +++ b/packages/dd-trace/src/plugins/apollo.js @@ -1,7 +1,7 @@ 'use strict' -const TracingPlugin = require('./tracing') const { storage } = require('../../../datadog-core') +const TracingPlugin = require('./tracing') class ApolloBasePlugin extends TracingPlugin { static id = 'apollo.gateway' diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index 87da10128c3..b5e4357074b 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -1,6 +1,33 @@ 'use strict' const { storage } = require('../../../datadog-core') +const { COMPONENT } = require('../constants') +const log = require('../log') +const { + incrementCountMetric, + distributionMetric, + TELEMETRY_EVENT_CREATED, + TELEMETRY_ITR_SKIPPED +} = require('../ci-visibility/telemetry') +const getDiClient = require('../ci-visibility/dynamic-instrumentation') +const { DD_MAJOR } = require('../../../../version') +const id = require('../id') +const { OS_VERSION, OS_PLATFORM, OS_ARCHITECTURE, RUNTIME_NAME, RUNTIME_VERSION } = require('./util/env') +const { + CI_PROVIDER_NAME, + GIT_REPOSITORY_URL, + GIT_COMMIT_SHA, + GIT_BRANCH, + CI_WORKSPACE_PATH, + GIT_COMMIT_MESSAGE, + GIT_TAG, + GIT_PULL_REQUEST_BASE_BRANCH_SHA, + GIT_COMMIT_HEAD_SHA, + GIT_PULL_REQUEST_BASE_BRANCH, + GIT_COMMIT_HEAD_MESSAGE +} = require('./util/tags') +const Plugin = require('./plugin') +const { getRepositoryRoot } = require('./util/git') const { getTestEnvironmentMetadata, getTestSessionName, @@ -37,33 +64,6 @@ const { getPullRequestBaseBranch, TEST_IS_TEST_FRAMEWORK_WORKER } = require('./util/test') -const { getRepositoryRoot } = require('./util/git') -const Plugin = require('./plugin') -const { COMPONENT } = require('../constants') -const log = require('../log') -const { - incrementCountMetric, - distributionMetric, - TELEMETRY_EVENT_CREATED, - TELEMETRY_ITR_SKIPPED -} = require('../ci-visibility/telemetry') -const { - CI_PROVIDER_NAME, - GIT_REPOSITORY_URL, - GIT_COMMIT_SHA, - GIT_BRANCH, - CI_WORKSPACE_PATH, - GIT_COMMIT_MESSAGE, - GIT_TAG, - GIT_PULL_REQUEST_BASE_BRANCH_SHA, - GIT_COMMIT_HEAD_SHA, - GIT_PULL_REQUEST_BASE_BRANCH, - GIT_COMMIT_HEAD_MESSAGE -} = require('./util/tags') -const { OS_VERSION, OS_PLATFORM, OS_ARCHITECTURE, RUNTIME_NAME, RUNTIME_VERSION } = require('./util/env') -const getDiClient = require('../ci-visibility/dynamic-instrumentation') -const { DD_MAJOR } = require('../../../../version') -const id = require('../id') const FRAMEWORK_TO_TRIMMED_COMMAND = { vitest: 'vitest run', diff --git a/packages/dd-trace/src/plugins/database.js b/packages/dd-trace/src/plugins/database.js index b26ae904dac..fe34b2423f6 100644 --- a/packages/dd-trace/src/plugins/database.js +++ b/packages/dd-trace/src/plugins/database.js @@ -1,7 +1,7 @@ 'use strict' -const StoragePlugin = require('./storage') const { PEER_SERVICE_KEY, PEER_SERVICE_SOURCE_KEY } = require('../constants') +const StoragePlugin = require('./storage') class DatabasePlugin extends StoragePlugin { static operation = 'query' diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index d30e31a68d4..e83c05ed6f1 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -1,6 +1,6 @@ 'use strict' -module.exports = { +const plugins = { get '@anthropic-ai/sdk' () { return require('../../../datadog-plugin-anthropic/src') }, get '@apollo/gateway' () { return require('../../../datadog-plugin-apollo/src') }, get '@aws-sdk/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') }, @@ -26,6 +26,7 @@ module.exports = { get '@node-redis/client' () { return require('../../../datadog-plugin-redis/src') }, get '@opensearch-project/opensearch' () { return require('../../../datadog-plugin-opensearch/src') }, get '@prisma/client' () { return require('../../../datadog-plugin-prisma/src') }, + get './runtime/library.js' () { return require('../../../datadog-plugin-prisma/src') }, get '@redis/client' () { return require('../../../datadog-plugin-redis/src') }, get '@smithy/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') }, get '@vitest/runner' () { return require('../../../datadog-plugin-vitest/src') }, @@ -35,6 +36,7 @@ module.exports = { get amqplib () { return require('../../../datadog-plugin-amqplib/src') }, get avsc () { return require('../../../datadog-plugin-avsc/src') }, get 'aws-sdk' () { return require('../../../datadog-plugin-aws-sdk/src') }, + get bullmq () { return require('../../../datadog-plugin-bullmq/src') }, get bunyan () { return require('../../../datadog-plugin-bunyan/src') }, get 'cassandra-driver' () { return require('../../../datadog-plugin-cassandra-driver/src') }, get child_process () { return require('../../../datadog-plugin-child_process/src') }, @@ -110,3 +112,5 @@ module.exports = { get winston () { return require('../../../datadog-plugin-winston/src') }, get ws () { return require('../../../datadog-plugin-ws/src') } } + +module.exports = plugins diff --git a/packages/dd-trace/src/plugins/log_plugin.js b/packages/dd-trace/src/plugins/log_plugin.js index 6e181f47f8d..8b7940c5b1f 100644 --- a/packages/dd-trace/src/plugins/log_plugin.js +++ b/packages/dd-trace/src/plugins/log_plugin.js @@ -1,8 +1,8 @@ 'use strict' const { LOG } = require('../../../../ext/formats') -const Plugin = require('./plugin') const { storage } = require('../../../datadog-core') +const Plugin = require('./plugin') function messageProxy (message, holder) { return new Proxy(message, { diff --git a/packages/dd-trace/src/plugins/outbound.js b/packages/dd-trace/src/plugins/outbound.js index 8f9332d68ab..fb3aebd6cfa 100644 --- a/packages/dd-trace/src/plugins/outbound.js +++ b/packages/dd-trace/src/plugins/outbound.js @@ -6,9 +6,9 @@ const { PEER_SERVICE_SOURCE_KEY, PEER_SERVICE_REMAP_KEY } = require('../constants') -const TracingPlugin = require('./tracing') const { exitTags } = require('../../../datadog-code-origin') const { storage } = require('../../../datadog-core') +const TracingPlugin = require('./tracing') const COMMON_PEER_SVC_SOURCE_TAGS = [ 'net.peer.name', diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index f012f5546c8..24f9a2d5ac1 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -1,9 +1,9 @@ 'use strict' -const Plugin = require('./plugin') const { storage } = require('../../../datadog-core') const analyticsSampler = require('../analytics_sampler') const { COMPONENT } = require('../constants') +const Plugin = require('./plugin') class TracingPlugin extends Plugin { constructor (...args) { diff --git a/packages/dd-trace/src/plugins/util/ci.js b/packages/dd-trace/src/plugins/util/ci.js index 814bd4fd4d9..5312145834b 100644 --- a/packages/dd-trace/src/plugins/util/ci.js +++ b/packages/dd-trace/src/plugins/util/ci.js @@ -1,6 +1,7 @@ 'use strict' const { readFileSync } = require('fs') +const { getEnvironmentVariable, getEnvironmentVariables } = require('../../config-helper') const { GIT_BRANCH, GIT_COMMIT_SHA, @@ -32,7 +33,6 @@ const { GIT_PULL_REQUEST_BASE_BRANCH_HEAD_SHA } = require('./tags') const { filterSensitiveInfoFromRepository } = require('./url') -const { getEnvironmentVariable, getEnvironmentVariables } = require('../../config-helper') // Receives a string with the form 'John Doe ' // and returns { name: 'John Doe', email: 'john.doe@gmail.com' } diff --git a/packages/dd-trace/src/plugins/util/git.js b/packages/dd-trace/src/plugins/util/git.js index 37cd2bf891d..4a4c508999e 100644 --- a/packages/dd-trace/src/plugins/util/git.js +++ b/packages/dd-trace/src/plugins/util/git.js @@ -5,6 +5,14 @@ const path = require('path') const fs = require('fs') const log = require('../../log') +const { + incrementCountMetric, + distributionMetric, + TELEMETRY_GIT_COMMAND, + TELEMETRY_GIT_COMMAND_MS, + TELEMETRY_GIT_COMMAND_ERRORS +} = require('../../ci-visibility/telemetry') +const { storage } = require('../../../../datadog-core') const { GIT_COMMIT_SHA, GIT_BRANCH, @@ -26,15 +34,7 @@ const { GIT_COMMIT_HEAD_COMMITTER_NAME, GIT_COMMIT_HEAD_MESSAGE } = require('./tags') -const { - incrementCountMetric, - distributionMetric, - TELEMETRY_GIT_COMMAND, - TELEMETRY_GIT_COMMAND_MS, - TELEMETRY_GIT_COMMAND_ERRORS -} = require('../../ci-visibility/telemetry') const { filterSensitiveInfoFromRepository } = require('./url') -const { storage } = require('../../../../datadog-core') const { cachedExec } = require('./git-cache') const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB diff --git a/packages/dd-trace/src/plugins/util/stacktrace.js b/packages/dd-trace/src/plugins/util/stacktrace.js index 937d48be39d..eefc5cd9b85 100644 --- a/packages/dd-trace/src/plugins/util/stacktrace.js +++ b/packages/dd-trace/src/plugins/util/stacktrace.js @@ -72,7 +72,7 @@ function parseUserLandFrames (stack, limit = Infinity) { * @param {number} end - The end index of the line to parse within the stack trace. * @returns {StackFrame|undefined} The parsed frame if it is a user frame, `undefined` otherwise. * - * @typedef {Object} StackFrame + * @typedef {object} StackFrame * @property {string} fileName - The file name of the frame. * @property {string} lineNumber - The line number of the frame. * @property {string} columnNumber - The column number of the frame. diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 121ee0fff6a..804701c8c8d 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -10,20 +10,17 @@ const satisfies = require('../../../../../vendor/dist/semifies') const istanbul = require('../../../../../vendor/dist/istanbul-lib-coverage') const ignore = require('../../../../../vendor/dist/ignore') +const id = require('../../id') const { - getGitMetadata, - getGitInformationDiscrepancy, - getGitDiff, - getGitRemoteName, - getSourceBranch, - checkAndFetchBranch, - getLocalBranches, - getMergeBase, - getCounts -} = require('./git') -const { getUserProviderGitMetadata, validateGitRepositoryUrl, validateGitCommitSha } = require('./user-provided-git') -const { getCIMetadata } = require('./ci') -const { getRuntimeAndOSMetadata } = require('./env') + incrementCountMetric, + TELEMETRY_GIT_COMMIT_SHA_DISCREPANCY, + TELEMETRY_GIT_SHA_MATCH +} = require('../../ci-visibility/telemetry') + +const { SPAN_TYPE, RESOURCE_NAME, SAMPLING_PRIORITY } = require('../../../../../ext/tags') +const { SAMPLING_RULE_DECISION } = require('../../constants') +const { AUTO_KEEP } = require('../../../../../ext/priority') +const { version: ddTraceVersion } = require('../../../../../package.json') const { GIT_BRANCH, GIT_COMMIT_SHA, @@ -37,17 +34,20 @@ const { CI_JOB_NAME, GIT_COMMIT_HEAD_SHA } = require('./tags') -const id = require('../../id') +const { getRuntimeAndOSMetadata } = require('./env') +const { getCIMetadata } = require('./ci') +const { getUserProviderGitMetadata, validateGitRepositoryUrl, validateGitCommitSha } = require('./user-provided-git') const { - incrementCountMetric, - TELEMETRY_GIT_COMMIT_SHA_DISCREPANCY, - TELEMETRY_GIT_SHA_MATCH -} = require('../../ci-visibility/telemetry') - -const { SPAN_TYPE, RESOURCE_NAME, SAMPLING_PRIORITY } = require('../../../../../ext/tags') -const { SAMPLING_RULE_DECISION } = require('../../constants') -const { AUTO_KEEP } = require('../../../../../ext/priority') -const { version: ddTraceVersion } = require('../../../../../package.json') + getGitMetadata, + getGitInformationDiscrepancy, + getGitDiff, + getGitRemoteName, + getSourceBranch, + checkAndFetchBranch, + getLocalBranches, + getMergeBase, + getCounts +} = require('./git') /** * JSDoc types for test environment metadata helpers. @@ -813,7 +813,7 @@ function getTestEndLine (testFn, startLine = 0) { /** * Gets an object of test tags from an Playwright annotations array. - * @param {Object[]} annotations - Annotations from a Playwright test. + * @param {object[]} annotations - Annotations from a Playwright test. * @param {string} annotations[].type - Type of annotation. A string of the shape DD_TAGS[$tag_name]. * @param {string} annotations[].description - Value of the tag. */ 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 e35a03b0108..87b87b49f2e 100644 --- a/packages/dd-trace/src/plugins/util/user-provided-git.js +++ b/packages/dd-trace/src/plugins/util/user-provided-git.js @@ -1,5 +1,6 @@ 'use strict' +const { getEnvironmentVariables } = require('../../config-helper') const { GIT_COMMIT_SHA, GIT_BRANCH, @@ -19,7 +20,6 @@ const { const { normalizeRef } = require('./ci') const { filterSensitiveInfoFromRepository } = require('./url') -const { getEnvironmentVariables } = require('../../config-helper') function removeEmptyValues (tags) { return Object.keys(tags).reduce((filteredTags, tag) => { diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index a1af749bb2a..ddffb9ce1bd 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -7,10 +7,10 @@ const log = require('../../log') const tags = require('../../../../../ext/tags') const types = require('../../../../../ext/types') const kinds = require('../../../../../ext/kinds') -const urlFilter = require('./urlfilter') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../constants') -const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy') const TracingPlugin = require('../tracing') +const urlFilter = require('./urlfilter') +const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy') const { extractURL, obfuscateQs, calculateHttpEndpoint } = require('./url') let extractIp @@ -274,11 +274,14 @@ const web = { return context.middleware.at(-1) }, - // Extract the parent span from the headers and start a new span as its child startChildSpan (tracer, config, name, req, traceCtx) { const headers = req.headers const reqCtx = contexts.get(req) - let childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers) + const { storage } = require('../../../../datadog-core') + const store = storage('legacy').getStore() + const pubsubSpan = store?.span?._name === 'pubsub.push.receive' ? store.span : null + + let childOf = pubsubSpan || tracer.extract(FORMAT_HTTP_HEADERS, headers) // we may have headers signaling a router proxy span should be created (such as for AWS API Gateway) if (tracer._config?.inferredProxyServicesEnabled) { @@ -503,7 +506,7 @@ function addResourceTag (context) { const { req, span } = context const tags = span.context()._tags - if (tags['resource.name']) return + if (tags[RESOURCE_NAME]) return const resource = [req.method, tags[HTTP_ROUTE]] .filter(Boolean) diff --git a/packages/dd-trace/src/priority_sampler.js b/packages/dd-trace/src/priority_sampler.js index 9247bff3d1d..0cd61cb8933 100644 --- a/packages/dd-trace/src/priority_sampler.js +++ b/packages/dd-trace/src/priority_sampler.js @@ -1,5 +1,19 @@ 'use strict' +const { + tags: { + MANUAL_KEEP, + MANUAL_DROP, + SAMPLING_PRIORITY, + SERVICE_NAME + }, + priority: { + AUTO_REJECT, + AUTO_KEEP, + USER_REJECT, + USER_KEEP + } +} = require('../../../ext') const log = require('./log') const RateLimiter = require('./rate_limiter') const Sampler = require('./sampler') @@ -19,21 +33,6 @@ const { DECISION_MAKER_KEY } = require('./constants') -const { - tags: { - MANUAL_KEEP, - MANUAL_DROP, - SAMPLING_PRIORITY, - SERVICE_NAME - }, - priority: { - AUTO_REJECT, - AUTO_KEEP, - USER_REJECT, - USER_KEEP - } -} = require('../../../ext') - const DEFAULT_KEY = 'service:,env:' const defaultSampler = new Sampler(AUTO_KEEP) @@ -54,7 +53,7 @@ class PrioritySampler { /** * Creates an instance of PrioritySampler. * - * @typedef {Object} SamplingConfig + * @typedef {object} SamplingConfig * @property {number} [sampleRate] - The default sample rate for traces. * @property {string} [provenance] - Optional rule provenance ("customer" or "dynamic"). * @property {number} [rateLimit=100] - The maximum number of traces to sample per second. diff --git a/packages/dd-trace/src/process-tags/index.js b/packages/dd-trace/src/process-tags/index.js index 843dd389528..b245f41377e 100644 --- a/packages/dd-trace/src/process-tags/index.js +++ b/packages/dd-trace/src/process-tags/index.js @@ -1,25 +1,10 @@ 'use strict' const path = require('node:path') -const pkg = require('../pkg') const CURRENT_WORKING_DIRECTORY = process.cwd() const ENTRYPOINT_PATH = require.main?.filename || '' -const TRACING_FIELD_NAME = '_dd.tags.process' -const DSM_FIELD_NAME = 'ProcessTags' -const PROFILING_FIELD_NAME = 'process_tags' - -module.exports.TRACING_FIELD_NAME = TRACING_FIELD_NAME -module.exports.DSM_FIELD_NAME = DSM_FIELD_NAME -module.exports.PROFILING_FIELD_NAME = PROFILING_FIELD_NAME - -// TODO CRASH_TRACKING_FIELD_NAME /process_tags /application/process_tags -// TODO: TELEMETRY_FIELD_NAME /application/process_tags -// TODO: DYNAMIC_INSTRUMENTATION_FIELD_NAME process_tags -// TODO: CLIENT_TRACE_STATISTICS_FIELD_NAME process_tags -// TODO: REMOTE_CONFIG_FIELD_NAME process_tags - // $ cd /foo/bar && node baz/banana.js // entrypoint.workdir = bar // entrypoint.name = banana @@ -27,7 +12,11 @@ module.exports.PROFILING_FIELD_NAME = PROFILING_FIELD_NAME // entrypoint.basedir = baz // package.json.name = -module.exports = function getProcessTags () { +// process tags are constant throughout the lifetime of a process +function getProcessTags () { + // Lazy load pkg to avoid issues with require.main during test initialization + const pkg = require('../pkg') + // this list is sorted alphabetically for consistent serialization const tags = [ // the parent directory name of the entrypoint script, e.g. /foo/bar/baz/banana.js -> baz @@ -46,25 +35,38 @@ module.exports = function getProcessTags () { ['package.json.name', pkg.name || undefined] ] - const serialized = serialize(tags) + const tagsArray = [] + const tagsObject = {} - return { - tags, - serialized + for (const [key, value] of tags) { + if (value !== undefined) { + const sanitizedValue = sanitize(value) + tagsArray.push(`${key}:${sanitizedValue}`) + tagsObject[key] = sanitizedValue + } } -} -function serialize (tags) { - const intermediary = [] - for (const [name, value] of tags) { - // if we don't have a value we should send nothing at all - if (value === undefined) continue - intermediary.push(`${name}:${sanitize(value)}`) + const serialized = tagsArray.join(',') + + return { + tags, + serialized, + tagsObject, + tagsArray } - return intermediary.join(',') } -module.exports.serialize = serialize +// Export the singleton +module.exports = getProcessTags() + +module.exports.TRACING_FIELD_NAME = '_dd.tags.process' +module.exports.DSM_FIELD_NAME = 'ProcessTags' +module.exports.PROFILING_FIELD_NAME = 'process_tags' +module.exports.DYNAMIC_INSTRUMENTATION_FIELD_NAME = 'process_tags' +module.exports.TELEMETRY_FIELD_NAME = 'process_tags' +module.exports.REMOTE_CONFIG_FIELD_NAME = 'process_tags' +module.exports.CRASH_TRACKING_FIELD_NAME = 'process_tags' +module.exports.CLIENT_TRACE_STATISTICS_FIELD_NAME = 'ProcessTags' /** * Sanitize a process tag value diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 8d32fc15a7f..5c11a54c68a 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -4,6 +4,12 @@ const os = require('os') const path = require('path') const { URL, format, pathToFileURL } = require('url') const satisfies = require('../../../../vendor/dist/semifies') +const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags') +const { getIsAzureFunction } = require('../serverless') +const { isFalse, isTrue } = require('../util') +const { getAzureTagsFromMetadata, getAzureAppMetadata, getAzureFunctionMetadata } = require('../azure_metadata') +const { getEnvironmentVariables } = require('../config-helper') +const defaults = require('../config_defaults') const { AgentExporter } = require('./exporters/agent') const { FileExporter } = require('./exporters/file') const { ConsoleLogger } = require('./loggers/console') @@ -11,12 +17,7 @@ const WallProfiler = require('./profilers/wall') const SpaceProfiler = require('./profilers/space') const EventsProfiler = require('./profilers/events') const { oomExportStrategies, snapshotKinds } = require('./constants') -const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags') const { tagger } = require('./tagger') -const { isFalse, isTrue } = require('../util') -const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata') -const { getEnvironmentVariables } = require('../config-helper') -const defaults = require('../config_defaults') class Config { constructor (options = {}) { @@ -73,7 +74,7 @@ class Config { tagger.parse(DD_TAGS), tagger.parse(options.tags), tagger.parse({ env, host, service, version, functionname }), - getAzureTagsFromMetadata(getAzureAppMetadata()) + getAzureTagsFromMetadata(getIsAzureFunction() ? getAzureFunctionMetadata() : getAzureAppMetadata()) ) // Add source code integration tags if available @@ -178,6 +179,9 @@ class Config { this.heapSamplingInterval = options.heapSamplingInterval ?? (Number(DD_PROFILING_HEAP_SAMPLING_INTERVAL) || 512 * 1024) + + const isAtLeast24 = satisfies(process.versions.node, '>=24.0.0') + const uploadCompression0 = options.uploadCompression ?? DD_PROFILING_DEBUG_UPLOAD_COMPRESSION ?? 'on' let [uploadCompression, level0] = uploadCompression0.split('-') if (!['on', 'off', 'gzip', 'zstd'].includes(uploadCompression)) { @@ -205,9 +209,12 @@ class Config { } } - // Default to gzip + // Default to either zstd (on Node.js 24+) or gzip (earlier Node.js). We could default to ztsd + // everywhere as we ship a Rust zstd compressor for older Node.js versions, but on 24+ we use + // the built-in one that runs asynchronously on libuv worker threads, just as gzip does. This is + // the least disruptive choice. if (uploadCompression === 'on') { - uploadCompression = 'gzip' + uploadCompression = isAtLeast24 ? 'zstd' : 'gzip' } this.uploadCompression = { method: uploadCompression, level } @@ -221,20 +228,24 @@ class Config { const hasExecArg = (arg) => process.execArgv.includes(arg) || String(NODE_OPTIONS).includes(arg) - this.asyncContextFrameEnabled = isTrue(options.useAsyncContextFrame ?? DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED) - if (this.asyncContextFrameEnabled) { - if (satisfies(process.versions.node, '>=24.0.0')) { - if (hasExecArg('--no-async-context-frame')) { - turnOffAsyncContextFrame('with --no-async-context-frame') - } - } else if (satisfies(process.versions.node, '>=23.0.0')) { - if (!hasExecArg('--experimental-async-context-frame')) { - turnOffAsyncContextFrame('without --experimental-async-context-frame') - } + let canUseAsyncContextFrame = false + if (samplingContextsAvailable) { + if (isAtLeast24) { + canUseAsyncContextFrame = !hasExecArg('--no-async-context-frame') + } else if (satisfies(process.versions.node, '>=22.9.0')) { + canUseAsyncContextFrame = hasExecArg('--experimental-async-context-frame') + } + } + this.asyncContextFrameEnabled = isTrue(DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED ?? canUseAsyncContextFrame) + if (this.asyncContextFrameEnabled && !canUseAsyncContextFrame) { + if (!samplingContextsAvailable) { + turnOffAsyncContextFrame(`on ${process.platform}`) + } else if (isAtLeast24) { + turnOffAsyncContextFrame('with --no-async-context-frame') + } else if (satisfies(process.versions.node, '>=22.9.0')) { + turnOffAsyncContextFrame('without --experimental-async-context-frame') } else { - // NOTE: technically, this should work starting with 22.7.0 which is when - // AsyncContextFrame debuted, but it would require a change in pprof-nodejs too. - turnOffAsyncContextFrame('but it requires at least Node.js 23') + turnOffAsyncContextFrame('but it requires at least Node.js 22.9.0') } } diff --git a/packages/dd-trace/src/profiling/exporter_cli.js b/packages/dd-trace/src/profiling/exporter_cli.js index 564877aa319..b8191440d5d 100644 --- a/packages/dd-trace/src/profiling/exporter_cli.js +++ b/packages/dd-trace/src/profiling/exporter_cli.js @@ -1,14 +1,14 @@ 'use strict' +const fs = require('fs') +const { fileURLToPath } = require('url') +const { SourceMapper, heap, encode } = require('@datadog/pprof') +const { getEnvironmentVariable } = require('../config-helper') const { AgentExporter } = require('./exporters/agent') const { FileExporter } = require('./exporters/file') -const { SourceMapper, heap, encode } = require('@datadog/pprof') const { ConsoleLogger } = require('./loggers/console') const { tagger } = require('./tagger') -const fs = require('fs') -const { fileURLToPath } = require('url') -const { getEnvironmentVariable } = require('../config-helper') const logger = new ConsoleLogger() const timeoutMs = 15 * 1000 diff --git a/packages/dd-trace/src/profiling/exporters/agent.js b/packages/dd-trace/src/profiling/exporters/agent.js index 9fc85931800..4310109e92a 100644 --- a/packages/dd-trace/src/profiling/exporters/agent.js +++ b/packages/dd-trace/src/profiling/exporters/agent.js @@ -1,19 +1,19 @@ 'use strict' -const retry = require('../../../../../vendor/dist/retry') const { request: httpRequest } = require('http') const { request: httpsRequest } = require('https') -const { EventSerializer } = require('./event_serializer') +const perf = require('perf_hooks').performance +const { urlToHttpOptions } = require('url') +const retry = require('../../../../../vendor/dist/retry') // TODO: avoid using dd-trace internals. Make this a separate module? const docker = require('../../exporters/common/docker') const FormData = require('../../exporters/common/form-data') const { storage } = require('../../../../datadog-core') const version = require('../../../../../package.json').version -const { urlToHttpOptions } = require('url') -const perf = require('perf_hooks').performance - const telemetryMetrics = require('../../telemetry/metrics') +const { EventSerializer } = require('./event_serializer') + const profilersNamespace = telemetryMetrics.manager.namespace('profilers') const statusCodeCounters = [] diff --git a/packages/dd-trace/src/profiling/index.js b/packages/dd-trace/src/profiling/index.js index 7a35871b705..33fd857ffe3 100644 --- a/packages/dd-trace/src/profiling/index.js +++ b/packages/dd-trace/src/profiling/index.js @@ -1,12 +1,12 @@ 'use strict' +const { getEnvironmentVariable } = require('../config-helper') const { Profiler, ServerlessProfiler } = require('./profiler') const WallProfiler = require('./profilers/wall') const SpaceProfiler = require('./profilers/space') const { AgentExporter } = require('./exporters/agent') const { FileExporter } = require('./exporters/file') const { ConsoleLogger } = require('./loggers/console') -const { getEnvironmentVariable } = require('../config-helper') const profiler = getEnvironmentVariable('AWS_LAMBDA_FUNCTION_NAME') ? new ServerlessProfiler() : new Profiler() diff --git a/packages/dd-trace/src/profiling/libuv-size.js b/packages/dd-trace/src/profiling/libuv-size.js index d4b42ee2110..6e382b9c165 100644 --- a/packages/dd-trace/src/profiling/libuv-size.js +++ b/packages/dd-trace/src/profiling/libuv-size.js @@ -1,7 +1,7 @@ 'use strict' -const { getEnvironmentVariable } = require('../config-helper') const os = require('node:os') +const { getEnvironmentVariable } = require('../config-helper') function getLibuvThreadPoolSize (envVar) { if (envVar === undefined) { diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 8cdb55556be..fb372f00974 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -1,15 +1,14 @@ 'use strict' const { EventEmitter } = require('events') +const { promisify } = require('util') +const zlib = require('zlib') +const dc = require('dc-polyfill') +const crashtracker = require('../crashtracking') const { Config } = require('./config') const { snapshotKinds } = require('./constants') const { threadNamePrefix } = require('./profilers/shared') const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('./webspan-utils') -const dc = require('dc-polyfill') -const crashtracker = require('../crashtracking') - -const { promisify } = require('util') -const zlib = require('zlib') const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted') const spanFinishedChannel = dc.channel('dd-trace:span:finish') diff --git a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js index b3eba5b892e..40802bc84b0 100644 --- a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js @@ -1,7 +1,7 @@ 'use strict' -const TracingPlugin = require('../../../plugins/tracing') const { performance } = require('perf_hooks') +const TracingPlugin = require('../../../plugins/tracing') // We are leveraging the TracingPlugin class for its functionality to bind // start/error/finish methods to the appropriate diagnostic channels. diff --git a/packages/dd-trace/src/profiling/profilers/events.js b/packages/dd-trace/src/profiling/profilers/events.js index e660d79f90c..f316062f670 100644 --- a/packages/dd-trace/src/profiling/profilers/events.js +++ b/packages/dd-trace/src/profiling/profilers/events.js @@ -1,7 +1,6 @@ 'use strict' const { performance, constants, PerformanceObserver } = require('perf_hooks') -const { END_TIMESTAMP_LABEL, SPAN_ID_LABEL, LOCAL_ROOT_SPAN_ID_LABEL, encodeProfileAsync } = require('./shared') const { Function, Label, @@ -12,8 +11,9 @@ const { StringTable, ValueType } = require('../../../../../vendor/dist/pprof-format') -const PoissonProcessSamplingFilter = require('./poisson') const { availableParallelism, effectiveLibuvThreadCount } = require('../libuv-size') +const { END_TIMESTAMP_LABEL, SPAN_ID_LABEL, LOCAL_ROOT_SPAN_ID_LABEL, encodeProfileAsync } = require('./shared') +const PoissonProcessSamplingFilter = require('./poisson') // perf_hooks uses millis, with fractional part representing nanos. We emit nanos into the pprof file. const MS_TO_NS = 1_000_000 // The number of sampling intervals that need to pass before we reset the Poisson process sampling instant. diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index 859d5686867..b01a6275d7d 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -1,10 +1,12 @@ 'use strict' -const { storage } = require('../../../../datadog-core') - const dc = require('dc-polyfill') + +const { storage } = require('../../../../datadog-core') const runtimeMetrics = require('../../runtime_metrics') const telemetryMetrics = require('../../telemetry/metrics') +const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils') + const { END_TIMESTAMP_LABEL, SPAN_ID_LABEL, @@ -15,8 +17,6 @@ const { } = require('./shared') const TRACE_ENDPOINT_LABEL = 'trace endpoint' -const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils') - let beforeCh const enterCh = dc.channel('dd-trace:storage:enter') const spanFinishCh = dc.channel('dd-trace:span:finish') diff --git a/packages/dd-trace/src/proxy.js b/packages/dd-trace/src/proxy.js index 321117f9fa7..0eb6d33b72b 100644 --- a/packages/dd-trace/src/proxy.js +++ b/packages/dd-trace/src/proxy.js @@ -1,4 +1,5 @@ 'use strict' +const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const NoopProxy = require('./noop/proxy') const DatadogTracer = require('./tracer') const getConfig = require('./config') @@ -10,7 +11,6 @@ const telemetry = require('./telemetry') const nomenclature = require('./service-naming') const PluginManager = require('./plugin_manager') const NoopDogStatsDClient = require('./noop/dogstatsd') -const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') const { setBaggageItem, getBaggageItem, @@ -126,16 +126,11 @@ class Tracer extends NoopProxy { } if (config.remoteConfig.enabled && !config.isCiVisibility) { - const rc = require('./remote_config').enable(config, this._modules.appsec) + const RemoteConfig = require('./remote_config') + const rc = new RemoteConfig(config) - rc.setProductHandler('APM_TRACING', (action, conf) => { - if (action === 'unapply') { - config.configure({}, true) - } else { - config.configure(conf.lib_config, true) - } - this._enableOrDisableTracing(config) - }) + const tracingRemoteConfig = require('./config/remote_config') + tracingRemoteConfig.enable(rc, config, this._enableOrDisableTracing.bind(this)) rc.setProductHandler('AGENT_CONFIG', (action, conf) => { if (!conf?.name?.startsWith('flare-log-level.')) return @@ -156,18 +151,17 @@ class Tracer extends NoopProxy { this._flare.module.send(conf.args) }) + if (this._modules.appsec) { + const appsecRemoteConfig = require('./appsec/remote_config') + appsecRemoteConfig.enable(rc, config, this._modules.appsec) + } + if (config.dynamicInstrumentation.enabled) { DynamicInstrumentation.start(config, rc) } - if (config.experimental.flaggingProvider.enabled) { - rc.setProductHandler('FFE_FLAGS', (action, conf) => { - // Feed UFC config directly to OpenFeature provider - if (action === 'apply' || action === 'modify') { - this.openfeature._setConfiguration(conf) - } - }) - } + const openfeatureRemoteConfig = require('./openfeature/remote_config') + openfeatureRemoteConfig.enable(rc, config, () => this.openfeature) } if (config.profiling.enabled === 'true') { diff --git a/packages/dd-trace/src/remote_config/index.js b/packages/dd-trace/src/remote_config/index.js index 34612f36d04..2673d60ab58 100644 --- a/packages/dd-trace/src/remote_config/index.js +++ b/packages/dd-trace/src/remote_config/index.js @@ -1,175 +1,579 @@ 'use strict' -const Activation = require('../appsec/activation') - -const RemoteConfigManager = require('./manager') -const RemoteConfigCapabilities = require('./capabilities') -const { setCollectionMode } = require('../appsec/user_tracking') +const { URL, format } = require('url') +const uuid = require('../../../../vendor/dist/crypto-randomuuid') +const tracerVersion = require('../../../../package.json').version +const request = require('../exporters/common/request') const log = require('../log') -const { updateConfig } = require('../telemetry') +const { getExtraServices } = require('../service-naming/extra-services') +const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags') +const tagger = require('../tagger') +const defaults = require('../config_defaults') +const processTags = require('../process-tags') +const Scheduler = require('./scheduler') +const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states') + +const clientId = uuid() + +const DEFAULT_CAPABILITY = Buffer.alloc(1).toString('base64') // 0x00 + +const kSupportsAckCallback = Symbol('kSupportsAckCallback') -let rc +// There MUST NOT exist separate instances of RC clients in a tracer making separate ClientGetConfigsRequest +// with their own separated Client.ClientState. +class RemoteConfig { + #handlers = new Map() + #products = new Set() + #batchHandlers = new Map() -function enable (config, appsec) { - rc = new RemoteConfigManager(config) - rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true) - rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true) - rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_LOGS_INJECTION, true) - rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RATE, true) - rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_ENABLED, true) - rc.updateCapabilities(RemoteConfigCapabilities.APM_TRACING_SAMPLE_RULES, true) - rc.updateCapabilities(RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true) + constructor (config) { + const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000) - const activation = Activation.fromConfig(config) + this.url = config.url || new URL(format({ + protocol: 'http:', + hostname: config.hostname || defaults.hostname, + port: config.port + })) + + tagger.add(config.tags, { + '_dd.rc.client_id': clientId + }) + + const tags = config.repositoryUrl + ? { + ...config.tags, + [GIT_REPOSITORY_URL]: config.repositoryUrl, + [GIT_COMMIT_SHA]: config.commitSHA + } + : config.tags - if (activation !== Activation.DISABLED) { - if (activation === Activation.ONECLICK) { - rc.updateCapabilities(RemoteConfigCapabilities.ASM_ACTIVATION, true) + const appliedConfigs = this.appliedConfigs = new Map() + + this.scheduler = new Scheduler((cb) => this.poll(cb), pollInterval) + + this.state = { + client: { + state: { // updated by `parseConfig()` and `poll()` + root_version: 1, + targets_version: 0, + // Use getter so `apply_*` can be updated async and still affect the content of `config_states` + get config_states () { + const configs = [] + for (const conf of appliedConfigs.values()) { + configs.push({ + id: conf.id, + version: conf.version, + product: conf.product, + apply_state: conf.apply_state, + apply_error: conf.apply_error + }) + } + return configs + }, + has_error: false, + error: '', + backend_client_state: '' + }, + id: clientId, + products: /** @type {string[]} */ ([]), // updated by `updateProducts()` + is_tracer: true, + client_tracer: { + runtime_id: config.tags['runtime-id'], + language: 'node', + tracer_version: tracerVersion, + service: config.service, + env: config.env, + app_version: config.version, + extra_services: /** @type {string[]} */ ([]), + tags: Object.entries(tags).map((pair) => pair.join(':')), + [processTags.REMOTE_CONFIG_FIELD_NAME]: processTags.tagsArray + }, + capabilities: DEFAULT_CAPABILITY // updated by `updateCapabilities()` + }, + cached_target_files: /** @type {RcCachedTargetFile[]} */ ([]) // updated by `parseConfig()` } + } - rc.updateCapabilities(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + /** + * @param {bigint} mask + * @param {boolean} value + */ + updateCapabilities (mask, value) { + const hex = Buffer.from(this.state.client.capabilities, 'base64').toString('hex') - let autoUserInstrumModeId + let num = BigInt(`0x${hex}`) - rc.setProductHandler('ASM_FEATURES', (action, rcConfig, configId) => { - if (!rcConfig) return + if (value) { + num |= mask + } else { + num &= ~mask + } - // this is put before other handlers because it can reject the config - if (typeof rcConfig.auto_user_instrum?.mode === 'string') { - if (action === 'apply' || action === 'modify') { - // check if there is already a config applied with this field - if (autoUserInstrumModeId && configId !== autoUserInstrumModeId) { - log.error('[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config') - // eslint-disable-next-line no-throw-literal - throw 'Multiple auto_user_instrum.mode received in ASM_FEATURES' - } + let str = num.toString(16) - setCollectionMode(rcConfig.auto_user_instrum.mode) - autoUserInstrumModeId = configId - } else if (configId === autoUserInstrumModeId) { - setCollectionMode(config.appsec.eventTracking.mode) - autoUserInstrumModeId = null - } + if (str.length % 2) str = `0${str}` + + this.state.client.capabilities = Buffer.from(str, 'hex').toString('base64') + } + + /** + * Subscribe to a product and register a per-config handler. + * + * This is the common API for products that can be handled one config at a time. + * It **implies subscription** (equivalent to calling `subscribeProducts(product)`). + * + * @param {string} product + * @param {Function} handler + */ + setProductHandler (product, handler) { + this.#handlers.set(product, handler) + this.subscribeProducts(product) + } + + /** + * Remove the per-config handler for a product and unsubscribe from it. + * + * If you only want to stop receiving configs (but keep the handler attached for later), + * call `unsubscribeProducts(product)` instead. + * + * @param {string} product + */ + removeProductHandler (product) { + this.#handlers.delete(product) + this.unsubscribeProducts(product) + } + + /** + * Subscribe to one or more products with Remote Config (receive configs for them). + * + * This only affects subscription/polling and does **not** register any handler. + * + * @param {...string} products + */ + subscribeProducts (...products) { + const hadProducts = this.#products.size > 0 + for (const product of products) { + this.#products.add(product) + } + this.updateProducts() + if (!hadProducts && this.#products.size > 0) { + this.scheduler.start() + } + } + + /** + * Unsubscribe from one or more products (stop receiving configs for them). + * + * This does **not** remove registered handlers; use `removeProductHandler(product)` + * if you want to detach a handler as well. + * + * @param {...string} products + */ + unsubscribeProducts (...products) { + const hadProducts = this.#products.size > 0 + for (const product of products) { + this.#products.delete(product) + } + this.updateProducts() + if (hadProducts && this.#products.size === 0) { + this.scheduler.stop() + } + } + + updateProducts () { + this.state.client.products = [...this.#products] + } + + /** + * Register a handler that will be invoked once per RC update, with the update batch filtered + * down to the specified products. This is useful for consumers that need to process multiple + * configs at once (e.g. WAF updates spanning ASM/ASM_DD/ASM_DATA) and then do one-time reconciliation. + * + * This does **not** implicitly subscribe to the products; call `subscribeProducts()` separately. + * + * @param {string[]} products + * @param {(transaction: RcBatchUpdateTransaction) => void} handler + */ + setBatchHandler (products, handler) { + this.#batchHandlers.set(handler, new Set(products)) + } + + /** + * Remove a previously-registered batch handler. + * + * @param {Function} handler + */ + removeBatchHandler (handler) { + this.#batchHandlers.delete(handler) + } + + // TODO: Cache the return value of this method, to avoid recomputing it on every poll + getPayload () { + this.state.client.client_tracer.extra_services = getExtraServices() + + return JSON.stringify(this.state) + } + + poll (cb) { + const options = { + url: this.url, + method: 'POST', + path: '/v0.7/config', + headers: { + 'Content-Type': 'application/json; charset=utf-8' } + } + + request(this.getPayload(), options, (err, data, statusCode) => { + // 404 means RC is disabled, ignore it + if (statusCode === 404) return cb() - if (activation === Activation.ONECLICK) { - enableOrDisableAppsec(action, rcConfig, config, appsec) + if (err) { + log.errorWithoutTelemetry('[RC] Error in request', err) + return cb() } + + // if error was just sent, reset the state + if (this.state.client.state.has_error) { + this.state.client.state.has_error = false + this.state.client.state.error = '' + } + + if (data && data !== '{}') { // '{}' means the tracer is up to date + try { + this.parseConfig(JSON.parse(data)) + } catch (err) { + log.error('[RC] Could not parse remote config response', err) + + this.state.client.state.has_error = true + this.state.client.state.error = err.toString() + } + } + + cb() }) } - return rc -} + // `client_configs` is the list of config paths to have applied + // `targets` is the signed index with metadata for config files + // `target_files` is the list of config files containing the actual config data + parseConfig ({ + client_configs: clientConfigs = [], + targets, + target_files: targetFiles = [] + }) { + const toUnapply = /** @type {RcConfigState[]} */ ([]) + const toApply = /** @type {RcConfigState[]} */ ([]) + const toModify = /** @type {RcConfigState[]} */ ([]) + const transactionByPath = new Map() + const transactionHandledPaths = new Set() + const transactionOutcomes = new Map() + + for (const appliedConfig of this.appliedConfigs.values()) { + if (!clientConfigs.includes(appliedConfig.path)) { + toUnapply.push(appliedConfig) + transactionByPath.set(appliedConfig.path, appliedConfig) + } + } -function enableOrDisableAppsec (action, rcConfig, config, appsec) { - if (typeof rcConfig.asm?.enabled === 'boolean') { - const isRemoteConfigControlling = action === 'apply' || action === 'modify' - const shouldEnable = isRemoteConfigControlling - ? rcConfig.asm.enabled // take control - : config.appsec.enabled // give back control to local config + targets = fromBase64JSON(targets) - if (shouldEnable) { - appsec.enable(config) - } else { - appsec.disable() + if (targets) { + for (const path of clientConfigs) { + const meta = targets.signed.targets[path] + if (!meta) throw new Error(`Unable to find target for path ${path}`) + + const current = this.appliedConfigs.get(path) + + const newConf = /** @type {RcConfigState} */ ({}) + + if (current) { + if (current.hashes.sha256 === meta.hashes.sha256) continue + + toModify.push(newConf) + } else { + toApply.push(newConf) + } + + const file = targetFiles.find(file => file.path === path) + if (!file) throw new Error(`Unable to find file for path ${path}`) + + // TODO: verify signatures + // verify length + // verify hash + // verify _type + // TODO: new Date(meta.signed.expires) ignore the Targets data if it has expired ? + + const { product, id } = parseConfigPath(path) + + Object.assign(newConf, { + path, + product, + id, + version: meta.custom.v, + apply_state: UNACKNOWLEDGED, + apply_error: '', + length: meta.length, + hashes: meta.hashes, + file: fromBase64JSON(file.raw) + }) + transactionByPath.set(path, newConf) + } + + this.state.client.state.targets_version = targets.signed.version + this.state.client.state.backend_client_state = targets.signed.custom.opaque_backend_state } - updateConfig([ - { - name: 'appsec.enabled', - origin: isRemoteConfigControlling ? 'remote_config' : config.getOrigin('appsec.enabled'), - value: shouldEnable + if (toUnapply.length || toApply.length || toModify.length) { + const transaction = createUpdateTransaction( + { toUnapply, toApply, toModify }, + transactionHandledPaths, + transactionOutcomes + ) + + if (this.#batchHandlers.size) { + for (const [handler, products] of this.#batchHandlers) { + const transactionView = filterTransactionByProducts(transaction, products) + if (transactionView.toUnapply.length || transactionView.toApply.length || transactionView.toModify.length) { + handler(transactionView) + } + } + } + + applyOutcomes(transactionByPath, transactionOutcomes) + + this.dispatch(toUnapply, 'unapply', transactionHandledPaths) + this.dispatch(toApply, 'apply', transactionHandledPaths) + this.dispatch(toModify, 'modify', transactionHandledPaths) + + this.state.cached_target_files = /** @type {RcCachedTargetFile[]} */ ([]) + + for (const conf of this.appliedConfigs.values()) { + const hashes = [] + for (const hash of Object.entries(conf.hashes)) { + hashes.push({ algorithm: hash[0], hash: hash[1] }) + } + this.state.cached_target_files.push({ + path: conf.path, + length: conf.length, + hashes + }) + } + } + } + + /** + * Dispatch a list of config changes to per-product handlers, skipping any paths + * marked as handled by a batch handler. + * + * @param {RcConfigState[]} list + * @param {'apply' | 'modify' | 'unapply'} action + * @param {Set} handledPaths + */ + dispatch (list, action, handledPaths) { + for (const item of list) { + if (!handledPaths.has(item.path)) { + this.#callHandlerFor(action, item) + } + + if (action === 'unapply') { + this.appliedConfigs.delete(item.path) + } else { + this.appliedConfigs.set(item.path, item) + } + } + } + + /** + * @param {'apply' | 'modify' | 'unapply'} action + * @param {RcConfigState} item + */ + #callHandlerFor (action, item) { + // in case the item was already handled by a batch hook + if (item.apply_state !== UNACKNOWLEDGED && action !== 'unapply') return + + const handler = this.#handlers.get(item.product) + + if (!handler) return + + try { + if (supportsAckCallback(handler)) { + // If the handler accepts an `ack` callback, expect that to be called and set `apply_state` accordingly + // TODO: do we want to pass old and new config ? + handler(action, item.file, item.id, (err) => { + if (err) { + item.apply_state = ERROR + item.apply_error = err.toString() + } else if (item.apply_state !== ERROR) { + item.apply_state = ACKNOWLEDGED + } + }) + } else { + // If the handler doesn't accept an `ack` callback, assume `apply_state` is `ACKNOWLEDGED`, + // unless it returns a promise, in which case we wait for the promise to be resolved or rejected. + // TODO: do we want to pass old and new config ? + const result = handler(action, item.file, item.id) + if (result instanceof Promise) { + result.then( + () => { item.apply_state = ACKNOWLEDGED }, + (err) => { + item.apply_state = ERROR + item.apply_error = err.toString() + } + ) + } else { + item.apply_state = ACKNOWLEDGED + } } - ], config) + } catch (err) { + item.apply_state = ERROR + item.apply_error = err.toString() + } } } -// TODO: all appsec specific stuff should be moved back in the appsec folder -function enableWafUpdate (appsecConfig) { - if (rc && appsecConfig && !appsecConfig.rules) { - // dirty require to make startup faster for serverless - const RuleManager = require('../appsec/rule_manager') - - rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_USER_BLOCKING, true) - // TODO: we should have a different capability for rule override - rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, true) - - if (appsecConfig.rasp?.enabled) { - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, true) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, true) +/** + * Remote Config “applied config” state tracked by the RC manager. + * This is the mutable shape stored in `this.appliedConfigs` and passed to per-product handlers. + * + * @typedef {object} RcConfigState + * @property {string} path + * @property {string} product + * @property {string} id + * @property {number} version + * @property {unknown} file + * @property {number} apply_state + * @property {string} apply_error + * @property {number} length + * @property {Record} hashes + */ + +/** + * Target file metadata cached in `state.cached_target_files` and sent back to the agent. + * + * @typedef {object} RcCachedTargetFile + * @property {string} path + * @property {number} length + * @property {Array<{algorithm: string, hash: string}>} hashes + */ + +/** + * Remote Config batch update transaction passed to batch handlers registered via + * `RemoteConfig.setBatchHandler()`. + * + * @typedef {object} RcBatchUpdateTransaction + * @property {RcConfigState[]} toUnapply + * @property {RcConfigState[]} toApply + * @property {RcConfigState[]} toModify + * @property {(path: string) => void} ack + * @property {(path: string, err: unknown) => void} error + */ + +/** + * Create an immutable "view" of the batch changes and attach explicit outcome reporting. + * + * @param {{toUnapply: RcConfigState[], toApply: RcConfigState[], toModify: RcConfigState[]}} changes + * @param {Set} handledPaths + * @param {Map} outcomes + * @returns {RcBatchUpdateTransaction} + */ +function createUpdateTransaction ({ toUnapply, toApply, toModify }, handledPaths, outcomes) { + return { + toUnapply, + toApply, + toModify, + ack (path) { + outcomes.set(path, { state: ACKNOWLEDGED, error: '' }) + handledPaths.add(path) + }, + error (path, err) { + outcomes.set(path, { state: ERROR, error: err ? err.toString() : 'Error' }) + handledPaths.add(path) } + } +} - // TODO: delete noop handlers and kPreUpdate and replace with batched handlers - rc.setProductHandler('ASM_DATA', noop) - rc.setProductHandler('ASM_DD', noop) - rc.setProductHandler('ASM', noop) +/** + * Create a filtered "view" of the transaction for a given product set, while preserving + * the outcome methods (ack/error). + * + * @param {RcBatchUpdateTransaction} transaction + * @param {Set} products + * @returns {RcBatchUpdateTransaction} + */ +function filterTransactionByProducts (transaction, products) { + const toUnapply = [] + const toApply = [] + const toModify = [] - rc.on(RemoteConfigManager.kPreUpdate, RuleManager.updateWafFromRC) + for (const item of transaction.toUnapply) { + if (products.has(item.product)) toUnapply.push(item) + } + + for (const item of transaction.toApply) { + if (products.has(item.product)) toApply.push(item) + } + + for (const item of transaction.toModify) { + if (products.has(item.product)) toModify.push(item) + } + + return { + toUnapply, + toApply, + toModify, + ack: transaction.ack, + error: transaction.error } } -function disableWafUpdate () { - if (rc) { - const RuleManager = require('../appsec/rule_manager') - - rc.updateCapabilities(RemoteConfigCapabilities.ASM_IP_BLOCKING, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_USER_BLOCKING, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSION_DATA, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_MULTICONFIG, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXTENDED_DATA_COLLECTION, false) - - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SQLI, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SSRF, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_LFI, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_SHI, false) - rc.updateCapabilities(RemoteConfigCapabilities.ASM_RASP_CMDI, false) - - rc.removeProductHandler('ASM_DATA') - rc.removeProductHandler('ASM_DD') - rc.removeProductHandler('ASM') - - rc.off(RemoteConfigManager.kPreUpdate, RuleManager.updateWafFromRC) +function applyOutcomes (byPath, outcomes) { + for (const [path, outcome] of outcomes) { + const item = byPath.get(path) + if (item) { + item.apply_state = outcome.state + item.apply_error = outcome.error + } } } -function noop () {} +function fromBase64JSON (str) { + if (!str) return null -module.exports = { - enable, - enableWafUpdate, - disableWafUpdate + return JSON.parse(Buffer.from(str, 'base64').toString()) } + +const configPathRegex = /^(?:datadog\/\d+|employee)\/([^/]+)\/([^/]+)\/[^/]+$/ + +function parseConfigPath (configPath) { + const match = configPathRegex.exec(configPath) + + if (!match || !match[1] || !match[2]) { + throw new Error(`Unable to parse path ${configPath}`) + } + + return { + product: match[1], + id: match[2] + } +} + +function supportsAckCallback (handler) { + if (kSupportsAckCallback in handler) return handler[kSupportsAckCallback] + + const numOfArgs = handler.length + let result = false + + if (numOfArgs >= 4) { + result = true + } else if (numOfArgs !== 0) { + const source = handler.toString() + result = source.slice(0, source.indexOf(')')).includes('...') + } + + handler[kSupportsAckCallback] = result + + return result +} + +module.exports = RemoteConfig diff --git a/packages/dd-trace/src/remote_config/manager.js b/packages/dd-trace/src/remote_config/manager.js deleted file mode 100644 index ecd5e84672f..00000000000 --- a/packages/dd-trace/src/remote_config/manager.js +++ /dev/null @@ -1,368 +0,0 @@ -'use strict' - -const { URL, format } = require('url') -const uuid = require('../../../../vendor/dist/crypto-randomuuid') -const { EventEmitter } = require('events') -const tracerVersion = require('../../../../package.json').version -const request = require('../exporters/common/request') -const log = require('../log') -const { getExtraServices } = require('../service-naming/extra-services') -const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states') -const Scheduler = require('./scheduler') -const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags') -const tagger = require('../tagger') -const defaults = require('../config_defaults') - -const clientId = uuid() - -const DEFAULT_CAPABILITY = Buffer.alloc(1).toString('base64') // 0x00 - -const kPreUpdate = Symbol('kPreUpdate') -const kSupportsAckCallback = Symbol('kSupportsAckCallback') - -// There MUST NOT exist separate instances of RC clients in a tracer making separate ClientGetConfigsRequest -// with their own separated Client.ClientState. -class RemoteConfigManager extends EventEmitter { - static kPreUpdate = kPreUpdate - - constructor (config) { - super() - - const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000) - - this.url = config.url || new URL(format({ - protocol: 'http:', - hostname: config.hostname || defaults.hostname, - port: config.port - })) - - tagger.add(config.tags, { - '_dd.rc.client_id': clientId - }) - - const tags = config.repositoryUrl - ? { - ...config.tags, - [GIT_REPOSITORY_URL]: config.repositoryUrl, - [GIT_COMMIT_SHA]: config.commitSHA - } - : config.tags - - this._handlers = new Map() - const appliedConfigs = this.appliedConfigs = new Map() - - this.scheduler = new Scheduler((cb) => this.poll(cb), pollInterval) - - this.state = { - client: { - state: { // updated by `parseConfig()` and `poll()` - root_version: 1, - targets_version: 0, - // Use getter so `apply_*` can be updated async and still affect the content of `config_states` - get config_states () { - const configs = [] - for (const conf of appliedConfigs.values()) { - configs.push({ - id: conf.id, - version: conf.version, - product: conf.product, - apply_state: conf.apply_state, - apply_error: conf.apply_error - }) - } - return configs - }, - has_error: false, - error: '', - backend_client_state: '' - }, - id: clientId, - products: [], // updated by `updateProducts()` - is_tracer: true, - client_tracer: { - runtime_id: config.tags['runtime-id'], - language: 'node', - tracer_version: tracerVersion, - service: config.service, - env: config.env, - app_version: config.version, - extra_services: [], - tags: Object.entries(tags).map((pair) => pair.join(':')) - }, - capabilities: DEFAULT_CAPABILITY // updated by `updateCapabilities()` - }, - cached_target_files: [] // updated by `parseConfig()` - } - } - - updateCapabilities (mask, value) { - const hex = Buffer.from(this.state.client.capabilities, 'base64').toString('hex') - - let num = BigInt(`0x${hex}`) - - if (value) { - num |= mask - } else { - num &= ~mask - } - - let str = num.toString(16) - - if (str.length % 2) str = `0${str}` - - this.state.client.capabilities = Buffer.from(str, 'hex').toString('base64') - } - - setProductHandler (product, handler) { - this._handlers.set(product, handler) - this.updateProducts() - if (this.state.client.products.length === 1) { - this.scheduler.start() - } - } - - removeProductHandler (product) { - this._handlers.delete(product) - this.updateProducts() - if (this.state.client.products.length === 0) { - this.scheduler.stop() - } - } - - updateProducts () { - this.state.client.products = [...this._handlers.keys()] - } - - getPayload () { - this.state.client.client_tracer.extra_services = getExtraServices() - - return JSON.stringify(this.state) - } - - poll (cb) { - const options = { - url: this.url, - method: 'POST', - path: '/v0.7/config', - headers: { - 'Content-Type': 'application/json; charset=utf-8' - } - } - - request(this.getPayload(), options, (err, data, statusCode) => { - // 404 means RC is disabled, ignore it - if (statusCode === 404) return cb() - - if (err) { - log.errorWithoutTelemetry('[RC] Error in request', err) - return cb() - } - - // if error was just sent, reset the state - if (this.state.client.state.has_error) { - this.state.client.state.has_error = false - this.state.client.state.error = '' - } - - if (data && data !== '{}') { // '{}' means the tracer is up to date - try { - this.parseConfig(JSON.parse(data)) - } catch (err) { - log.error('[RC] Could not parse remote config response', err) - - this.state.client.state.has_error = true - this.state.client.state.error = err.toString() - } - } - - cb() - }) - } - - // `client_configs` is the list of config paths to have applied - // `targets` is the signed index with metadata for config files - // `target_files` is the list of config files containing the actual config data - parseConfig ({ - client_configs: clientConfigs = [], - targets, - target_files: targetFiles = [] - }) { - const toUnapply = [] - const toApply = [] - const toModify = [] - - for (const appliedConfig of this.appliedConfigs.values()) { - if (!clientConfigs.includes(appliedConfig.path)) { - toUnapply.push(appliedConfig) - } - } - - targets = fromBase64JSON(targets) - - if (targets) { - for (const path of clientConfigs) { - const meta = targets.signed.targets[path] - if (!meta) throw new Error(`Unable to find target for path ${path}`) - - const current = this.appliedConfigs.get(path) - - const newConf = {} - - if (current) { - if (current.hashes.sha256 === meta.hashes.sha256) continue - - toModify.push(newConf) - } else { - toApply.push(newConf) - } - - const file = targetFiles.find(file => file.path === path) - if (!file) throw new Error(`Unable to find file for path ${path}`) - - // TODO: verify signatures - // verify length - // verify hash - // verify _type - // TODO: new Date(meta.signed.expires) ignore the Targets data if it has expired ? - - const { product, id } = parseConfigPath(path) - - Object.assign(newConf, { - path, - product, - id, - version: meta.custom.v, - apply_state: UNACKNOWLEDGED, - apply_error: '', - length: meta.length, - hashes: meta.hashes, - file: fromBase64JSON(file.raw) - }) - } - - this.state.client.state.targets_version = targets.signed.version - this.state.client.state.backend_client_state = targets.signed.custom.opaque_backend_state - } - - if (toUnapply.length || toApply.length || toModify.length) { - this.emit(RemoteConfigManager.kPreUpdate, { toUnapply, toApply, toModify }) - - this.dispatch(toUnapply, 'unapply') - this.dispatch(toApply, 'apply') - this.dispatch(toModify, 'modify') - - this.state.cached_target_files = [] - - for (const conf of this.appliedConfigs.values()) { - const hashes = [] - for (const hash of Object.entries(conf.hashes)) { - hashes.push({ algorithm: hash[0], hash: hash[1] }) - } - this.state.cached_target_files.push({ - path: conf.path, - length: conf.length, - hashes - }) - } - } - } - - dispatch (list, action) { - for (const item of list) { - // TODO: we need a way to tell if unapply configs were handled by kPreUpdate or not, because they're always - // emitted unlike the apply and modify configs - - this._callHandlerFor(action, item) - - if (action === 'unapply') { - this.appliedConfigs.delete(item.path) - } else { - this.appliedConfigs.set(item.path, item) - } - } - } - - _callHandlerFor (action, item) { - // in case the item was already handled by kPreUpdate - if (item.apply_state !== UNACKNOWLEDGED && action !== 'unapply') return - - const handler = this._handlers.get(item.product) - - if (!handler) return - - try { - if (supportsAckCallback(handler)) { - // If the handler accepts an `ack` callback, expect that to be called and set `apply_state` accordinly - // TODO: do we want to pass old and new config ? - handler(action, item.file, item.id, (err) => { - if (err) { - item.apply_state = ERROR - item.apply_error = err.toString() - } else if (item.apply_state !== ERROR) { - item.apply_state = ACKNOWLEDGED - } - }) - } else { - // If the handler doesn't accept an `ack` callback, assume `apply_state` is `ACKNOWLEDGED`, - // unless it returns a promise, in which case we wait for the promise to be resolved or rejected. - // TODO: do we want to pass old and new config ? - const result = handler(action, item.file, item.id) - if (result instanceof Promise) { - result.then( - () => { item.apply_state = ACKNOWLEDGED }, - (err) => { - item.apply_state = ERROR - item.apply_error = err.toString() - } - ) - } else { - item.apply_state = ACKNOWLEDGED - } - } - } catch (err) { - item.apply_state = ERROR - item.apply_error = err.toString() - } - } -} - -function fromBase64JSON (str) { - if (!str) return null - - return JSON.parse(Buffer.from(str, 'base64').toString()) -} - -const configPathRegex = /^(?:datadog\/\d+|employee)\/([^/]+)\/([^/]+)\/[^/]+$/ - -function parseConfigPath (configPath) { - const match = configPathRegex.exec(configPath) - - if (!match || !match[1] || !match[2]) { - throw new Error(`Unable to parse path ${configPath}`) - } - - return { - product: match[1], - id: match[2] - } -} - -function supportsAckCallback (handler) { - if (kSupportsAckCallback in handler) return handler[kSupportsAckCallback] - - const numOfArgs = handler.length - let result = false - - if (numOfArgs >= 4) { - result = true - } else if (numOfArgs !== 0) { - const source = handler.toString() - result = source.slice(0, source.indexOf(')')).includes('...') - } - - handler[kSupportsAckCallback] = result - - return result -} - -module.exports = RemoteConfigManager diff --git a/packages/dd-trace/src/require-package-json.js b/packages/dd-trace/src/require-package-json.js index 5d0187016c4..3f685993fdf 100644 --- a/packages/dd-trace/src/require-package-json.js +++ b/packages/dd-trace/src/require-package-json.js @@ -12,7 +12,7 @@ const fs = require('fs') * @typedef { import('module').Module } Module * @param {string} name * @param {Module} module - * @returns {Object} The parsed package.json + * @returns {object} The parsed package.json */ function requirePackageJson (name, module) { if (path.isAbsolute(name)) { diff --git a/packages/dd-trace/src/ritm.js b/packages/dd-trace/src/ritm.js index 65815097b13..66a2d340028 100644 --- a/packages/dd-trace/src/ritm.js +++ b/packages/dd-trace/src/ritm.js @@ -1,13 +1,16 @@ 'use strict' const path = require('path') +const fs = require('fs') const Module = require('module') -const parse = require('../../../vendor/dist/module-details-from-path') + const dc = require('dc-polyfill') + +const parse = require('../../../vendor/dist/module-details-from-path') const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper') +const { isRelativeRequire } = require('../../datadog-instrumentations/src/helpers/shared-utils') const origRequire = Module.prototype.require - // derived from require-in-the-middle@3 with tweaks module.exports = Hook @@ -64,7 +67,6 @@ function Hook (modules, options, onrequire) { } catch { return _origRequire.apply(this, arguments) } - const core = !filename.includes(path.sep) let name, basedir, hooks // return known patched modules immediately @@ -117,32 +119,41 @@ function Hook (modules, options, onrequire) { // decide how to assign the stat // first case will only happen when patching an AWS Lambda Handler const stat = inAWSLambda && hasLambdaHandler && !filenameFromNodeModule ? { name: filename } : parse(filename) - if (!stat) return exports // abort if filename could not be parsed - name = stat.name - basedir = stat.basedir - - hooks = moduleHooks[name] - if (!hooks) return exports // abort if module name isn't on whitelist - // figure out if this is the main module file, or a file inside the module - const paths = Module._resolveLookupPaths(name, this, true) - if (!paths) { - // abort if _resolveLookupPaths return null - return exports - } - - let res - try { - res = Module._findPath(name, [basedir, ...paths]) - } catch { - // case where the file specified in package.json "main" doesn't exist - // in this case, the file is treated as module-internal - } + if (stat) { + name = stat.name + basedir = stat.basedir + + hooks = moduleHooks[name] + if (!hooks) return exports // abort if module name isn't on whitelist + + const paths = Module._resolveLookupPaths(name, this, true) + if (!paths) { + // abort if _resolveLookupPaths return null + return exports + } + + let res + try { + res = Module._findPath(name, [basedir, ...paths]) + } catch { + // case where the file specified in package.json "main" doesn't exist + // in this case, the file is treated as module-internal + } + + if (!res || res !== filename) { + // this is a module-internal file + // use the module-relative path to the file, prefixed by original module name + name = name + path.sep + path.relative(basedir, filename) + } + } else { + if (isRelativeRequire(request) && moduleHooks[request]) { + hooks = moduleHooks[request] + name = request + basedir = findProjectRoot(filename) + } - if (!res || res !== filename) { - // this is a module-internal file - // use the module-relative path to the file, prefixed by original module name - name = name + path.sep + path.relative(basedir, filename) + if (!hooks) return exports } } @@ -167,6 +178,18 @@ Hook.reset = function () { moduleHooks = Object.create(null) } +function findProjectRoot (startDir) { + let dir = startDir + + while (!fs.existsSync(path.join(dir, 'package.json'))) { + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent + } + + return dir +} + Hook.prototype.unhook = function () { for (const mod of this.modules) { const hooks = (moduleHooks[mod] || []).filter(hook => hook !== this.onrequire) diff --git a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js index fc9cb546adc..c24f6421246 100644 --- a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js @@ -5,9 +5,9 @@ const v8 = require('v8') const os = require('os') const process = require('process') +const { performance, PerformanceObserver, monitorEventLoopDelay } = require('perf_hooks') const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd') const log = require('../log') -const { performance, PerformanceObserver, monitorEventLoopDelay } = require('perf_hooks') const { getEnvironmentVariable } = require('../config-helper') const { NODE_MAJOR } = require('../../../../version') diff --git a/packages/dd-trace/src/serverless.js b/packages/dd-trace/src/serverless.js index 9c4039228a0..ac8c5255e96 100644 --- a/packages/dd-trace/src/serverless.js +++ b/packages/dd-trace/src/serverless.js @@ -1,6 +1,7 @@ 'use strict' const { getEnvironmentVariable } = require('./config-helper') +const { isFalse } = require('./util') function getIsGCPFunction () { const isDeprecatedGCPFunction = @@ -13,6 +14,15 @@ function getIsGCPFunction () { return isDeprecatedGCPFunction || isNewerGCPFunction } +/** + * 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. + */ +function enableGCPPubSubPushSubscription () { + const isGCPPubSubPushSubscriptionEnabled = getEnvironmentVariable('DD_TRACE_GCP_PUBSUB_PUSH_ENABLED') + return getEnvironmentVariable('K_SERVICE') !== undefined && !isFalse(isGCPPubSubPushSubscriptionEnabled) +} + function getIsAzureFunction () { const isAzureFunction = getEnvironmentVariable('FUNCTIONS_EXTENSION_VERSION') !== undefined && @@ -21,6 +31,10 @@ function getIsAzureFunction () { return isAzureFunction } +function getIsFlexConsumptionAzureFunction () { + return getIsAzureFunction() && getEnvironmentVariable('WEBSITE_SKU') === 'FlexConsumption' +} + function isInServerlessEnvironment () { const inAWSLambda = getEnvironmentVariable('AWS_LAMBDA_FUNCTION_NAME') !== undefined const isGCPFunction = getIsGCPFunction() @@ -32,5 +46,7 @@ function isInServerlessEnvironment () { module.exports = { getIsGCPFunction, getIsAzureFunction, + enableGCPPubSubPushSubscription, + getIsFlexConsumptionAzureFunction, isInServerlessEnvironment } 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 37b0a437c3a..dc574d5d6fd 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/messaging.js @@ -47,6 +47,10 @@ const messaging = { sns: { opName: () => 'aws.request', serviceName: awsServiceV0 + }, + bullmq: { + opName: () => 'bullmq.add', + serviceName: ({ tracerService }) => `${tracerService}-bullmq` } }, consumer: { @@ -77,6 +81,10 @@ const messaging = { sqs: { opName: () => 'aws.request', serviceName: awsServiceV0 + }, + bullmq: { + opName: () => 'bullmq.processJob', + serviceName: ({ tracerService }) => `${tracerService}-bullmq` } }, client: { 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 7fa6fd9d72d..cb5592258c1 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js @@ -44,6 +44,10 @@ const messaging = { sns: { opName: () => 'aws.sns.send', serviceName: identityService + }, + bullmq: { + opName: () => 'bullmq.add', + serviceName: identityService } }, consumer: { @@ -65,6 +69,10 @@ const messaging = { sqs: { opName: () => 'aws.sqs.process', serviceName: identityService + }, + bullmq: { + opName: () => 'bullmq.processJob', + serviceName: identityService } }, client: { diff --git a/packages/dd-trace/src/span_format.js b/packages/dd-trace/src/span_format.js index 56e11fffd68..666869a636b 100644 --- a/packages/dd-trace/src/span_format.js +++ b/packages/dd-trace/src/span_format.js @@ -1,7 +1,7 @@ 'use strict' -const constants = require('./constants') const tags = require('../../../ext/tags') +const constants = require('./constants') const id = require('./id') const { isError } = require('./util') const { registerExtraService } = require('./service-naming/extra-services') diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index e88724902ba..7454e6ba490 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -5,7 +5,7 @@ const spanFormat = require('./span_format') const SpanSampler = require('./span_sampler') const GitMetadataTagger = require('./git_metadata_tagger') const { getEnvironmentVariable } = require('./config-helper') -const getProcessTags = require('./process-tags') +const processTags = require('./process-tags') const startedSpans = new WeakSet() const finishedSpans = new WeakSet() @@ -27,7 +27,7 @@ class SpanProcessor { this._gitMetadataTagger = new GitMetadataTagger(config) this._processTags = config.propagateProcessTags?.enabled - ? getProcessTags().serialized + ? processTags.serialized : false } diff --git a/packages/dd-trace/src/span_stats.js b/packages/dd-trace/src/span_stats.js index 240ed847a78..7ade2c047ad 100644 --- a/packages/dd-trace/src/span_stats.js +++ b/packages/dd-trace/src/span_stats.js @@ -1,11 +1,9 @@ 'use strict' -const os = require('os') -const { version } = require('./pkg') +const os = require('node:os') const pkg = require('../../../package.json') const { LogCollapsingLowestDenseDDSketch } = require('../../../vendor/dist/@datadog/sketches-js') -const { ORIGIN_KEY, TOP_LEVEL_KEY } = require('./constants') const { MEASURED, HTTP_STATUS_CODE, @@ -13,6 +11,9 @@ const { HTTP_ROUTE, HTTP_METHOD } = require('../../../ext/tags') +const { ORIGIN_KEY, TOP_LEVEL_KEY } = require('./constants') +const { version } = require('./pkg') +const processTags = require('./process-tags') const { SpanStatsExporter } = require('./exporters/span-stats') @@ -176,7 +177,8 @@ class SpanStatsProcessor { Lang: 'javascript', TracerVersion: pkg.version, RuntimeID: this.tags['runtime-id'], - Sequence: ++this.sequence + Sequence: ++this.sequence, + ProcessTags: processTags.serialized }) } diff --git a/packages/dd-trace/src/standalone/index.js b/packages/dd-trace/src/standalone/index.js index bc500f1ff17..54b5d0bfbe2 100644 --- a/packages/dd-trace/src/standalone/index.js +++ b/packages/dd-trace/src/standalone/index.js @@ -1,10 +1,10 @@ 'use strict' const { channel } = require('dc-polyfill') -const TraceSourcePrioritySampler = require('./tracesource_priority_sampler') const { USER_KEEP } = require('../../../../ext/priority') const TraceState = require('../opentracing/propagation/tracestate') const { APM_TRACING_ENABLED_KEY } = require('../constants') +const TraceSourcePrioritySampler = require('./tracesource_priority_sampler') const { hasTraceSourcePropagationTag } = require('./tracesource') const startCh = channel('dd-trace:span:start') diff --git a/packages/dd-trace/src/startup-log.js b/packages/dd-trace/src/startup-log.js index b985eef7c12..843fb7ff1f6 100644 --- a/packages/dd-trace/src/startup-log.js +++ b/packages/dd-trace/src/startup-log.js @@ -1,11 +1,11 @@ 'use strict' -const { info, warn } = require('./log/writer') - const os = require('os') const { inspect } = require('util') -const defaults = require('./config_defaults') + const tracerVersion = require('../../../package.json').version +const defaults = require('./config_defaults') +const { info, warn } = require('./log/writer') const errors = {} let config diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 7a1aae5b923..b227a297dc4 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -37,6 +37,7 @@ "DD_APPSEC_STACK_TRACE_ENABLED": ["A"], "DD_APPSEC_TRACE_RATE_LIMIT": ["A"], "DD_APPSEC_WAF_TIMEOUT": ["A"], + "DD_AZURE_RESOURCE_GROUP": ["A"], "DD_CIVISIBILITY_AGENTLESS_ENABLED": ["A"], "DD_CIVISIBILITY_AGENTLESS_URL": ["A"], "DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER": ["A"], @@ -162,6 +163,7 @@ "DD_RUNTIME_METRICS_GC_ENABLED": ["A"], "DD_RUNTIME_METRICS_FLUSH_INTERVAL": ["A"], "DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED": ["A"], + "DD_TRACE_GCP_PUBSUB_PUSH_ENABLED": ["A"], "DD_SERVICE_MAPPING": ["A"], "DD_SERVICE": ["A"], "DD_SITE": ["A"], @@ -243,6 +245,7 @@ "DD_TRACE_BAGGAGE_TAG_KEYS": ["A"], "DD_TRACE_BEAUTIFUL_LOGS": ["A"], "DD_TRACE_BLUEBIRD_ENABLED": ["A"], + "DD_TRACE_BULLMQ_ENABLED": ["A"], "DD_TRACE_BODY_PARSER_ENABLED": ["A"], "DD_TRACE_BSON_ENABLED": ["A"], "DD_TRACE_BUNYAN_ENABLED": ["A"], diff --git a/packages/dd-trace/src/telemetry/dependencies.js b/packages/dd-trace/src/telemetry/dependencies.js index 74aaa90b99c..cd37cd6a184 100644 --- a/packages/dd-trace/src/telemetry/dependencies.js +++ b/packages/dd-trace/src/telemetry/dependencies.js @@ -1,12 +1,12 @@ 'use strict' const path = require('path') +const { fileURLToPath } = require('url') +const dc = require('dc-polyfill') const parse = require('../../../../vendor/dist/module-details-from-path') const requirePackageJson = require('../require-package-json') -const { sendData } = require('./send-data') -const dc = require('dc-polyfill') -const { fileURLToPath } = require('url') const { isTrue } = require('../../src/util') +const { sendData } = require('./send-data') /** @type {Set} */ const savedDependenciesToSend = new Set() diff --git a/packages/dd-trace/src/telemetry/endpoints.js b/packages/dd-trace/src/telemetry/endpoints.js index 60db5004008..5b711dfed9e 100644 --- a/packages/dd-trace/src/telemetry/endpoints.js +++ b/packages/dd-trace/src/telemetry/endpoints.js @@ -15,7 +15,7 @@ let updateRetryData /** * Keep track of endpoints that still need to be sent. - * Map key is `${METHOD} ${PATH}`, value is { method, path } + * Map key is `${METHOD} ${PATH}`, value is { method, path, operationName } */ const pendingEndpoints = new Map() const wildcardEndpoints = new Set() @@ -32,11 +32,11 @@ function scheduleFlush () { setImmediate(flushAndSend).unref() } -function recordEndpoint (method, path) { +function recordEndpoint (method, path, operationName) { const key = endpointKey(method, path) if (pendingEndpoints.has(key)) return - pendingEndpoints.set(key, { method: method.toUpperCase(), path }) + pendingEndpoints.set(key, { method: method.toUpperCase(), path, operationName }) scheduleFlush() } @@ -46,7 +46,7 @@ function onFastifyRoute (routeData) { const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method] for (const method of methods) { - recordEndpoint(method, routeOptions.path) + recordEndpoint(method, routeOptions.path, 'fastify.request') } } @@ -56,7 +56,7 @@ function onExpressRoute ({ method, path }) { // If wildcard already recorded for this path, skip specific methods if (wildcardEndpoints.has(path)) return - recordEndpoint(method, path) + recordEndpoint(method, path, 'express.request') // If this is a wildcard event, record it and mark path as wildcarded if (method === '*') { @@ -66,17 +66,17 @@ function onExpressRoute ({ method, path }) { // Express automatically adds HEAD support for GET routes if (method.toUpperCase() === 'GET') { - recordEndpoint('HEAD', path) + recordEndpoint('HEAD', path, 'express.request') } } function buildEndpointObjects (endpoints) { - return endpoints.map(({ method, path }) => { + return endpoints.map(({ method, path, operationName }) => { return { type: 'REST', method, path, - operation_name: 'http.request', + operation_name: operationName, resource_name: endpointKey(method, path) } }) diff --git a/packages/dd-trace/src/telemetry/logs/index.js b/packages/dd-trace/src/telemetry/logs/index.js index 326518cb956..d89891830dd 100644 --- a/packages/dd-trace/src/telemetry/logs/index.js +++ b/packages/dd-trace/src/telemetry/logs/index.js @@ -1,8 +1,8 @@ 'use strict' const dc = require('dc-polyfill') -const logCollector = require('./log-collector') const { sendData } = require('../send-data') +const logCollector = require('./log-collector') const telemetryLog = dc.channel('datadog:telemetry:log') const errorLog = dc.channel('datadog:log:error') diff --git a/packages/dd-trace/src/telemetry/telemetry.js b/packages/dd-trace/src/telemetry/telemetry.js index 55e6f1d6ae6..7d7ceec5609 100644 --- a/packages/dd-trace/src/telemetry/telemetry.js +++ b/packages/dd-trace/src/telemetry/telemetry.js @@ -1,14 +1,17 @@ 'use strict' -const tracerVersion = require('../../../../package.json').version -const dc = require('dc-polyfill') + const os = require('os') +const dc = require('dc-polyfill') + +const tracerVersion = require('../../../../package.json').version +const { errors } = require('../startup-log') +const logger = require('../log') +const processTags = require('../process-tags') const dependencies = require('./dependencies') const endpoints = require('./endpoints') const { sendData } = require('./send-data') -const { errors } = require('../startup-log') const { manager: metricsManager } = require('./metrics') const telemetryLogger = require('./logs') -const logger = require('../log') const telemetryStartChannel = dc.channel('datadog:telemetry:start') const telemetryStopChannel = dc.channel('datadog:telemetry:stop') @@ -78,7 +81,8 @@ function getIntegrations () { newIntegrations.push({ name: pluginName, enabled: pluginManager._pluginsByName[pluginName]._enabled, - auto_enabled: true + auto_enabled: true, + [processTags.TELEMETRY_FIELD_NAME]: processTags.tagsObject }) sentIntegrations.add(pluginName) } @@ -155,7 +159,8 @@ function createAppObject (config) { service_version: config.version, tracer_version: tracerVersion, language_name: 'nodejs', - language_version: process.versions.node + language_version: process.versions.node, + process_tags: processTags.tagsObject } } diff --git a/packages/dd-trace/src/tracer.js b/packages/dd-trace/src/tracer.js index af73b4bcecf..791d66ace84 100644 --- a/packages/dd-trace/src/tracer.js +++ b/packages/dd-trace/src/tracer.js @@ -1,13 +1,13 @@ 'use strict' -const Tracer = require('./opentracing/tracer') const tags = require('../../../ext/tags') +const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const { flushStartupLogs } = require('../../datadog-instrumentations/src/helpers/check-require-cache') +const Tracer = require('./opentracing/tracer') const Scope = require('./scope') const { isError } = require('./util') const { setStartupLogConfig } = require('./startup-log') -const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const { DataStreamsCheckpointer, DataStreamsManager, DataStreamsProcessor } = require('./datastreams') -const { flushStartupLogs } = require('../../datadog-instrumentations/src/helpers/check-require-cache') const log = require('./log/writer') const SPAN_TYPE = tags.SPAN_TYPE diff --git a/packages/dd-trace/src/tracer_metadata.js b/packages/dd-trace/src/tracer_metadata.js index 67b427ecef1..3226f8cf590 100644 --- a/packages/dd-trace/src/tracer_metadata.js +++ b/packages/dd-trace/src/tracer_metadata.js @@ -1,25 +1,29 @@ 'use strict' -// Load binding first to not import other modules if it throws -const libdatadog = require('@datadog/libdatadog') const tracerVersion = require('../../../version').VERSION function storeConfig (config) { - const processDiscovery = libdatadog.maybeLoad('process-discovery') - if (processDiscovery === undefined) { - return - } + try { + // Load binding first to not import other modules if it throws + const libdatadog = require('@datadog/libdatadog') + const processDiscovery = libdatadog.maybeLoad('process-discovery') + if (processDiscovery === undefined) { + return + } - const metadata = new processDiscovery.TracerMetadata( - config.tags['runtime-id'], - tracerVersion, - config.hostname, - config.service || null, - config.env || null, - config.version || null - ) + const metadata = new processDiscovery.TracerMetadata( + config.tags['runtime-id'], + tracerVersion, + config.hostname, + config.service || null, + config.env || null, + config.version || null + ) - return processDiscovery.storeMetadata(metadata) + return processDiscovery.storeMetadata(metadata) + } catch { + // Either libdatadog or process-discovery is unavailable. + } } module.exports = storeConfig diff --git a/packages/dd-trace/test/analytics_sampler.spec.js b/packages/dd-trace/test/analytics_sampler.spec.js index ad296fcd085..f7ab080e232 100644 --- a/packages/dd-trace/test/analytics_sampler.spec.js +++ b/packages/dd-trace/test/analytics_sampler.spec.js @@ -1,6 +1,6 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') require('./setup/core') diff --git a/packages/dd-trace/test/appsec/api_security_sampler.spec.js b/packages/dd-trace/test/appsec/api_security_sampler.spec.js index 22fd17c03f0..e896231a9dc 100644 --- a/packages/dd-trace/test/appsec/api_security_sampler.spec.js +++ b/packages/dd-trace/test/appsec/api_security_sampler.spec.js @@ -2,10 +2,10 @@ const assert = require('node:assert/strict') +const { performance } = require('node:perf_hooks') const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') -const { performance } = require('node:perf_hooks') const { USER_KEEP, AUTO_KEEP, AUTO_REJECT, USER_REJECT } = require('../../../../ext/priority') describe('API Security Sampler', () => { diff --git a/packages/dd-trace/test/appsec/blocking.spec.js b/packages/dd-trace/test/appsec/blocking.spec.js index 62af7130d64..43b2710f7b1 100644 --- a/packages/dd-trace/test/appsec/blocking.spec.js +++ b/packages/dd-trace/test/appsec/blocking.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -75,12 +74,14 @@ describe('blocking', () => { const blocked = block(req, res, rootSpan) assert.strictEqual(blocked, false) - expect(log.warn).to.have.been - .calledOnceWithExactly('[ASM] Cannot send blocking response when headers have already been sent') + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Cannot send blocking response when headers have already been sent' + ) sinon.assert.calledOnceWithExactly(rootSpan.setTag, '_dd.appsec.block.failed', 1) sinon.assert.notCalled(res.setHeader) sinon.assert.notCalled(res.constructor.prototype.end) - expect(telemetry.updateBlockFailureMetric).to.be.calledOnceWithExactly(req) + sinon.assert.calledOnceWithExactly(telemetry.updateBlockFailureMetric, req) }) it('should send blocking response with html type if present in the headers', () => { diff --git a/packages/dd-trace/test/appsec/extended-data-collection.express.plugin.spec.js b/packages/dd-trace/test/appsec/extended-data-collection.express.plugin.spec.js index 8a3133a26c2..e9c77e75f8a 100644 --- a/packages/dd-trace/test/appsec/extended-data-collection.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/extended-data-collection.express.plugin.spec.js @@ -9,8 +9,8 @@ const msgpack = require('@msgpack/msgpack') const { withVersions } = require('../setup/mocha') const agent = require('../plugins/agent') const appsec = require('../../src/appsec') -const { createDeepObject } = require('./utils') const { getConfigFresh } = require('../helpers/config') +const { createDeepObject } = require('./utils') describe('extended data collection', () => { before(() => { diff --git a/packages/dd-trace/test/appsec/extended-data-collection.fastify.plugin.spec.js b/packages/dd-trace/test/appsec/extended-data-collection.fastify.plugin.spec.js index 03c2effdad1..a10acef1a2f 100644 --- a/packages/dd-trace/test/appsec/extended-data-collection.fastify.plugin.spec.js +++ b/packages/dd-trace/test/appsec/extended-data-collection.fastify.plugin.spec.js @@ -10,8 +10,8 @@ const { withVersions } = require('../setup/mocha') const agent = require('../plugins/agent') const appsec = require('../../src/appsec') -const { createDeepObject } = require('./utils') const { getConfigFresh } = require('../helpers/config') +const { createDeepObject } = require('./utils') describe('extended data collection', () => { before(() => { diff --git a/packages/dd-trace/test/appsec/graphql.apollo-server-express.plugin.spec.js b/packages/dd-trace/test/appsec/graphql.apollo-server-express.plugin.spec.js index 6cc95199546..2eafa8cdd0d 100644 --- a/packages/dd-trace/test/appsec/graphql.apollo-server-express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/graphql.apollo-server-express.plugin.spec.js @@ -1,12 +1,12 @@ 'use strict' const agent = require('../plugins/agent') +const { withVersions } = require('../setup/mocha') const { schema, resolvers, graphqlCommonTests } = require('./graphql.test-utils') -const { withVersions } = require('../setup/mocha') withVersions('apollo-server-core', 'express', '>=4', expressVersion => { withVersions('apollo-server-core', 'apollo-server-express', apolloServerExpressVersion => { diff --git a/packages/dd-trace/test/appsec/graphql.apollo-server-fastify.plugin.spec.js b/packages/dd-trace/test/appsec/graphql.apollo-server-fastify.plugin.spec.js index bfcf0eeee01..79e8ccea4e2 100644 --- a/packages/dd-trace/test/appsec/graphql.apollo-server-fastify.plugin.spec.js +++ b/packages/dd-trace/test/appsec/graphql.apollo-server-fastify.plugin.spec.js @@ -1,12 +1,12 @@ 'use strict' const agent = require('../plugins/agent') +const { withVersions } = require('../setup/mocha') const { schema, resolvers, graphqlCommonTests } = require('./graphql.test-utils') -const { withVersions } = require('../setup/mocha') withVersions('apollo-server-core', 'fastify', '3', fastifyVersion => { withVersions('apollo-server-core', 'apollo-server-fastify', apolloServerFastifyVersion => { diff --git a/packages/dd-trace/test/appsec/graphql.apollo-server.plugin.spec.js b/packages/dd-trace/test/appsec/graphql.apollo-server.plugin.spec.js index 59c1e07091d..ddff6cf6573 100644 --- a/packages/dd-trace/test/appsec/graphql.apollo-server.plugin.spec.js +++ b/packages/dd-trace/test/appsec/graphql.apollo-server.plugin.spec.js @@ -2,12 +2,12 @@ const path = require('path') const agent = require('../plugins/agent') +const { withVersions } = require('../setup/mocha') const { schema, resolvers, graphqlCommonTests } = require('./graphql.test-utils') -const { withVersions } = require('../setup/mocha') withVersions('apollo-server', '@apollo/server', apolloServerVersion => { const config = {} diff --git a/packages/dd-trace/test/appsec/graphql.spec.js b/packages/dd-trace/test/appsec/graphql.spec.js index 12e6e90b232..ac1cbeb4cb8 100644 --- a/packages/dd-trace/test/appsec/graphql.spec.js +++ b/packages/dd-trace/test/appsec/graphql.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -274,7 +273,7 @@ describe('GraphQL', () => { sinon.assert.calledOnceWithExactly(blocking.getBlockingData, req, 'graphql', blockParameters) sinon.assert.calledOnceWithExactly(rootSpan.setTag, '_dd.appsec.block.failed', 1) - expect(telemetry.updateBlockFailureMetric).to.be.calledOnceWithExactly(req) + sinon.assert.calledOnceWithExactly(telemetry.updateBlockFailureMetric, req) }) }) }) 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 d020d0e6cc8..705a87b6006 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 @@ -6,7 +6,6 @@ const fs = require('node:fs') const os = require('node:os') const path = require('node:path') -const { expect } = require('chai') const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') @@ -102,7 +101,12 @@ describe('Hardcoded Password Analyzer', () => { }) const evidence = { value: ident } - expect(reportEvidence).to.be.calledOnceWithExactly({ file: relFile, line, column, ident, data: ruleId }, undefined, evidence) + sinon.assert.calledOnceWithExactly( + reportEvidence, + { file: relFile, line, column, ident, data: ruleId }, + undefined, + evidence + ) }) }) 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 9a48dbd915c..78be335f8cd 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 @@ -14,9 +14,8 @@ const hardcodedSecretAnalyzer = require('../../../../src/appsec/iast/analyzers/h const vulnerabilityReporter = require('../../../../src/appsec/iast/vulnerability-reporter') const { getConfigFresh } = require('../../../helpers/config') const agent = require('../../../plugins/agent') -const { suite } = require('./resources/hardcoded-secrets-suite.json') - const { assertObjectContains } = require('../../../../../../integration-tests/helpers') +const { suite } = require('./resources/hardcoded-secrets-suite.json') describe('Hardcoded Secret Analyzer', () => { describe('unit test', () => { @@ -52,7 +51,7 @@ describe('Hardcoded Secret Analyzer', () => { }] }) - assert.ok([NameAndValue, ValueOnly].includes(testCase.type)) + assertObjectContains([NameAndValue, ValueOnly], [testCase.type]) sinon.assert.calledOnceWithExactly(report, { file: relFile, line, column, ident, data: testCase.id }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/insecure-cookie-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/insecure-cookie-analyzer.express.plugin.spec.js index 3b495e22674..eb8a27c3be5 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/insecure-cookie-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/insecure-cookie-analyzer.express.plugin.spec.js @@ -1,10 +1,10 @@ 'use strict' -const { prepareTestServerForIastInExpress } = require('../utils') -const { withVersions } = require('../../../setup/mocha') const fs = require('fs') const os = require('os') const path = require('path') +const { withVersions } = require('../../../setup/mocha') +const { prepareTestServerForIastInExpress } = require('../utils') describe('Insecure cookie vulnerability', () => { let setCookieFunctions diff --git a/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js index cf886e530db..20c96c0a54d 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/ldap-injection-analyzer.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -131,6 +130,6 @@ describe('ldap-injection-analyzer', () => { onLdapClientSearch({ base: 'base', filter: 'filter', name: 'datadog:ldapjs:client:search' }) - expect(analyzeAll.firstCall).to.be.calledWith('base', 'filter') + sinon.assert.calledWith(analyzeAll.firstCall, 'base', 'filter') }) }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/no-httponly-cookie-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/no-httponly-cookie-analyzer.express.plugin.spec.js index be5f3248d4f..7a47bcfd55b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/no-httponly-cookie-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/no-httponly-cookie-analyzer.express.plugin.spec.js @@ -1,10 +1,10 @@ 'use strict' -const { prepareTestServerForIastInExpress } = require('../utils') -const { withVersions } = require('../../../setup/mocha') const fs = require('fs') const os = require('os') const path = require('path') +const { withVersions } = require('../../../setup/mocha') +const { prepareTestServerForIastInExpress } = require('../utils') describe('no HttpOnly cookie vulnerability', () => { let setCookieFunctions diff --git a/packages/dd-trace/test/appsec/iast/analyzers/no-samesite-cookie-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/no-samesite-cookie-analyzer.express.plugin.spec.js index e0e0064a7f3..a776296f61f 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/no-samesite-cookie-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/no-samesite-cookie-analyzer.express.plugin.spec.js @@ -1,10 +1,10 @@ 'use strict' -const { prepareTestServerForIastInExpress } = require('../utils') -const { withVersions } = require('../../../setup/mocha') const fs = require('fs') const os = require('os') const path = require('path') +const { withVersions } = require('../../../setup/mocha') +const { prepareTestServerForIastInExpress } = require('../utils') describe('no SameSite cookie vulnerability', () => { let setCookieFunctions diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js index af9c0302b49..3f238fb1f33 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js @@ -1,13 +1,13 @@ 'use strict' -const { prepareTestServerForIastInExpress } = require('../utils') -const axios = require('axios') -const agent = require('../../../plugins/agent') -const { withVersions } = require('../../../setup/mocha') -const semver = require('semver') const os = require('os') const path = require('path') const fs = require('fs') +const axios = require('axios') +const semver = require('semver') +const { prepareTestServerForIastInExpress } = require('../utils') +const agent = require('../../../plugins/agent') +const { withVersions } = require('../../../setup/mocha') describe('nosql injection detection in mongodb - whole feature', () => { withVersions('mongoose', 'express', expressVersion => { diff --git a/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js index 5cf042b3398..07c4ad37307 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/path-traversal-analyzer.spec.js @@ -4,16 +4,16 @@ const assert = require('node:assert/strict') const os = require('os') const path = require('path') -const { storage } = require('../../../../../datadog-core') -const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') -const expect = require('chai').expect +const fs = require('fs') const sinon = require('sinon') const proxyquire = require('proxyquire') +const { storage } = require('../../../../../datadog-core') +const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') + const pathTraversalAnalyzer = require('../../../../src/appsec/iast/analyzers/path-traversal-analyzer') const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') const { prepareTestServerForIast } = require('../utils') -const fs = require('fs') const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') const iastContext = { @@ -183,7 +183,7 @@ describe('path-traversal-analyzer', () => { hasQuota.returns(true) proxyPathAnalyzer.analyze(['arg1']) - expect(addVulnerability).not.have.been.called + sinon.assert.notCalled(addVulnerability) }) }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js index 287631f9028..ad4764cd4f3 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/sql-injection-analyzer.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const dc = require('dc-polyfill') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') @@ -214,7 +213,7 @@ describe('sql-injection-analyzer', () => { onMysqlQueryStart({ sql: 'SELECT 1' }) - expect(analyze).to.be.calledOnceWith('SELECT 1') + sinon.assert.calledOnceWithMatch(analyze, 'SELECT 1') }) it('should call analyze on apm:mysql2:query:start', () => { @@ -222,7 +221,7 @@ describe('sql-injection-analyzer', () => { onMysql2QueryStart({ sql: 'SELECT 1' }) - expect(analyze).to.be.calledOnceWith('SELECT 1') + sinon.assert.calledOnceWithMatch(analyze, 'SELECT 1') }) it('should call analyze on apm:pg:query:start', () => { @@ -230,7 +229,7 @@ describe('sql-injection-analyzer', () => { onPgQueryStart({ originalText: 'SELECT 1', query: { text: 'modified-query SELECT 1' } }) - expect(analyze).to.be.calledOnceWith('SELECT 1') + sinon.assert.calledOnceWithMatch(analyze, 'SELECT 1') }) }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/unvalidated-redirect-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/unvalidated-redirect-analyzer.express.plugin.spec.js index 5d3b749eda5..fc1ff59d85b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/unvalidated-redirect-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/unvalidated-redirect-analyzer.express.plugin.spec.js @@ -4,10 +4,10 @@ const fs = require('fs') const os = require('os') const path = require('path') +const Axios = require('axios') const { UNVALIDATED_REDIRECT } = require('../../../../src/appsec/iast/vulnerabilities') const { prepareTestServerForIastInExpress } = require('../utils') const { withVersions } = require('../../../setup/mocha') -const Axios = require('axios') describe('Unvalidated Redirect vulnerability', () => { let redirectFunctions diff --git a/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.express.plugin.spec.js index 76a067c3f2b..958f8bd091e 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.express.plugin.spec.js @@ -4,10 +4,10 @@ const fs = require('fs') const os = require('os') const path = require('path') +const axios = require('axios') const { UNVALIDATED_REDIRECT } = require('../../../../src/appsec/iast/vulnerabilities') const { prepareTestServerForIastInExpress } = require('../utils') const { withVersions } = require('../../../setup/mocha') -const axios = require('axios') describe('Vulnerability Analyzer plugin', () => { let redirectFunctions diff --git a/packages/dd-trace/test/appsec/iast/code_injection.integration.spec.js b/packages/dd-trace/test/appsec/iast/code_injection.integration.spec.js index 7e1f4fbcc07..94af45698ed 100644 --- a/packages/dd-trace/test/appsec/iast/code_injection.integration.spec.js +++ b/packages/dd-trace/test/appsec/iast/code_injection.integration.spec.js @@ -74,7 +74,7 @@ describe('IAST - code_injection - integration', () => { DD_TRACE_AGENT_PORT: agent.port, DD_IAST_ENABLED: 'true', DD_IAST_REQUEST_SAMPLING: '100', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1' }, execArgv: ['--experimental-vm-modules'] }) @@ -94,7 +94,7 @@ describe('IAST - code_injection - integration', () => { DD_TRACE_AGENT_PORT: agent.port, DD_IAST_ENABLED: 'true', DD_IAST_REQUEST_SAMPLING: '100', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1' } }) axios = Axios.create({ baseURL: proc.url }) diff --git a/packages/dd-trace/test/appsec/iast/context/context-plugin.spec.js b/packages/dd-trace/test/appsec/iast/context/context-plugin.spec.js index b75879f9ee5..4d96b8d925f 100644 --- a/packages/dd-trace/test/appsec/iast/context/context-plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/context/context-plugin.spec.js @@ -1,6 +1,5 @@ 'use strict' -const { expect } = require('chai') const { describe, it, beforeEach, afterEach } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -68,8 +67,8 @@ describe('IastContextPlugin', () => { it('should add a subscription to the channel', () => { plugin.startCtxOn(channelName, tag) - expect(addSub).to.be.calledOnceWith(channelName) - expect(getAndRegisterSubscription).to.be.calledOnceWith({ channelName, tag, tagKey: TagKey.SOURCE_TYPE }) + sinon.assert.calledOnceWithMatch(addSub, channelName) + sinon.assert.calledOnceWithExactly(getAndRegisterSubscription, { channelName, tag, tagKey: TagKey.SOURCE_TYPE }) }) it('should call startContext when event is published', () => { @@ -90,7 +89,7 @@ describe('IastContextPlugin', () => { it('should add a subscription to the channel', () => { plugin.finishCtxOn(channelName) - expect(addSub).to.be.calledOnceWith(channelName) + sinon.assert.calledOnceWithMatch(addSub, channelName) }) it('should call finishContext when event is published', () => { @@ -135,20 +134,20 @@ describe('IastContextPlugin', () => { plugin.startContext() sinon.assert.calledOnce(plugin.getTopContext) - expect(plugin.getRootSpan).to.be.calledWith(store) + sinon.assert.calledWith(plugin.getRootSpan, store) }) it('should call overheadController before starting iast context', () => { plugin.startContext() - expect(acquireRequest).to.be.calledOnceWith(rootSpan) + sinon.assert.calledOnceWithExactly(acquireRequest, rootSpan) }) it('should add _dd.iast.enabled:0 tag in the rootSpan', () => { const addTags = sinon.stub(rootSpan, 'addTags') plugin.startContext() - expect(addTags).to.be.calledOnceWith({ [IAST_ENABLED_TAG_KEY]: 0 }) + sinon.assert.calledOnceWithExactly(addTags, { [IAST_ENABLED_TAG_KEY]: 0 }) }) it('should not fail if store does not contain span', () => { @@ -156,7 +155,7 @@ describe('IastContextPlugin', () => { plugin.startContext() - expect(acquireRequest).to.be.calledOnceWith(undefined) + sinon.assert.calledOnceWithExactly(acquireRequest, undefined) }) describe('if acquireRequest', () => { @@ -175,26 +174,26 @@ describe('IastContextPlugin', () => { const addTags = sinon.stub(rootSpan, 'addTags') plugin.startContext() - expect(addTags).to.be.calledOnceWith({ [IAST_ENABLED_TAG_KEY]: 1 }) + sinon.assert.calledOnceWithExactly(addTags, { [IAST_ENABLED_TAG_KEY]: 1 }) }) it('should create and save new IAST context and store it', () => { plugin.startContext() - expect(newIastContext).to.be.calledOnceWith(rootSpan) - expect(saveIastContext).to.be.calledOnceWith(store, topContext, context) + sinon.assert.calledOnceWithExactly(newIastContext, rootSpan) + sinon.assert.calledOnceWithExactly(saveIastContext, store, topContext, context) }) it('should create new taint-tracking transaction', () => { plugin.startContext() - expect(createTransaction).to.be.calledOnceWith('span-id', context) + sinon.assert.calledOnceWithExactly(createTransaction, 'span-id', context) }) it('should obtain needed info from data before starting iast context', () => { plugin.startContext() - expect(initializeRequestContext).to.be.calledOnceWith(context) + sinon.assert.calledOnceWithExactly(initializeRequestContext, context) }) }) }) @@ -217,7 +216,7 @@ describe('IastContextPlugin', () => { plugin.finishContext() - expect(sendVulnerabilities).to.be.calledOnceWith(vulnerabilities, rootSpan) + sinon.assert.calledOnceWithExactly(sendVulnerabilities, vulnerabilities, rootSpan) }) it('should remove the taint-tracking transaction', () => { @@ -230,7 +229,7 @@ describe('IastContextPlugin', () => { plugin.finishContext() - expect(removeTransaction).to.be.calledOnceWith(iastContext) + sinon.assert.calledOnceWithExactly(removeTransaction, iastContext) }) it('should clear iastContext and releaseRequest from OCE', () => { diff --git a/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js b/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js index e90d8a6bb3b..e7c08b57151 100644 --- a/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { channel } = require('dc-polyfill') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') @@ -113,12 +112,12 @@ describe('IAST Plugin', () => { iastPlugin._getTelemetryHandler = getTelemetryHandler iastPlugin.addSub({ channelName, tagKey: VULNERABILITY_TYPE }, handler) - expect(getTelemetryHandler).to.be.not.called + sinon.assert.notCalled(getTelemetryHandler) getTelemetryHandler.reset() iastPlugin.addSub({ channelName, tagKey: SOURCE_TYPE, tag: 'test-tag' }, handler) - expect(getTelemetryHandler).to.be.not.called + sinon.assert.notCalled(getTelemetryHandler) }) }) @@ -217,7 +216,7 @@ describe('IAST Plugin', () => { loadChannel.publish({ name: 'test' }) - expect(onInstrumentationLoadedMock).to.be.calledWith('test') + sinon.assert.calledWith(onInstrumentationLoadedMock, 'test') }) }) @@ -227,12 +226,12 @@ describe('IAST Plugin', () => { iastPlugin._getTelemetryHandler = getTelemetryHandler iastPlugin.addSub({ channelName, tagKey: VULNERABILITY_TYPE }, handler) - expect(getTelemetryHandler).to.be.calledOnceWith(iastPlugin.pluginSubs[0]) + sinon.assert.calledOnceWithExactly(getTelemetryHandler, iastPlugin.pluginSubs[0]) getTelemetryHandler.reset() iastPlugin.addSub({ channelName, tagKey: SOURCE_TYPE, tag: 'test-tag' }, handler) - expect(getTelemetryHandler).to.be.calledOnceWith(iastPlugin.pluginSubs[1]) + sinon.assert.calledOnceWithExactly(getTelemetryHandler, iastPlugin.pluginSubs[1]) }) it('should register a pluginSubscription and increment a sink metric when a sink module is loaded', () => { @@ -249,7 +248,7 @@ describe('IAST Plugin', () => { loadChannel.publish({ name: 'sink' }) - expect(metricInc).to.be.calledOnceWith(undefined, vulnTags) + sinon.assert.calledOnceWithExactly(metricInc, undefined, vulnTags) }) it('should register and increment a sink metric when a sink module is loaded using a tracingChannel', () => { @@ -265,7 +264,7 @@ describe('IAST Plugin', () => { loadChannel.publish({ name: 'sink' }) - expect(metricInc).to.be.calledOnceWith(undefined, vulnTags) + sinon.assert.calledOnceWithExactly(metricInc, undefined, vulnTags) }) it('should register an pluginSubscription and increment a source metric when a source module is loaded', () => { @@ -282,7 +281,7 @@ describe('IAST Plugin', () => { loadChannel.publish({ name: 'source' }) - expect(metricInc).to.be.calledOnceWith(undefined, sourceTags) + sinon.assert.calledOnceWithExactly(metricInc, undefined, sourceTags) }) it('should increment a sink metric when event is received', () => { @@ -300,7 +299,7 @@ describe('IAST Plugin', () => { const telemetryHandler = addSubMock.secondCall.args[1] telemetryHandler() - expect(metricInc).to.be.calledOnceWith(undefined, vulnTags) + sinon.assert.calledOnceWithExactly(metricInc, undefined, vulnTags) }) it('should increment a source metric when event is received', () => { @@ -318,7 +317,7 @@ describe('IAST Plugin', () => { const telemetryHandler = addSubMock.secondCall.args[1] telemetryHandler() - expect(metricInc).to.be.calledOnceWith(undefined, sourceTags) + sinon.assert.calledOnceWithExactly(metricInc, undefined, sourceTags) }) it('should increment a source metric when event is received for every tag', () => { @@ -336,10 +335,10 @@ describe('IAST Plugin', () => { const telemetryHandler = addSubMock.secondCall.args[1] telemetryHandler() - expect(metricInc).to.be.calledThrice - expect(metricInc.firstCall).to.be.calledWith(undefined, [`${SOURCE_TYPE}:http.source`]) - expect(metricInc.secondCall).to.be.calledWith(undefined, [`${SOURCE_TYPE}:http.source2`]) - expect(metricInc.thirdCall).to.be.calledWith(undefined, [`${SOURCE_TYPE}:http.source3`]) + sinon.assert.calledThrice(metricInc) + sinon.assert.calledWith(metricInc.firstCall, undefined, [`${SOURCE_TYPE}:http.source`]) + sinon.assert.calledWith(metricInc.secondCall, undefined, [`${SOURCE_TYPE}:http.source2`]) + sinon.assert.calledWith(metricInc.thirdCall, undefined, [`${SOURCE_TYPE}:http.source3`]) }) }) @@ -379,7 +378,7 @@ describe('IAST Plugin', () => { }) sinon.assert.calledOnce(handler) - expect(metric.inc).to.be.calledOnceWithExactly(iastContext, tags) + sinon.assert.calledOnceWithExactly(metric.inc, iastContext, tags) }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/index.spec.js b/packages/dd-trace/test/appsec/iast/index.spec.js index f56dac4a73d..b528a28a771 100644 --- a/packages/dd-trace/test/appsec/iast/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/index.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict') const axios = require('axios') -const { expect } = require('chai') + const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -15,8 +15,8 @@ const vulnerabilityReporter = require('../../../src/appsec/iast/vulnerability-re const { IAST_MODULE } = require('../../../src/appsec/rasp/fs-plugin') const { getConfigFresh } = require('../../helpers/config') const agent = require('../../plugins/agent') -const { testInRequest } = require('./utils') const { assertObjectContains } = require('../../../../../integration-tests/helpers') +const { testInRequest } = require('./utils') describe('IAST Index', () => { beforeEach(() => { @@ -170,7 +170,7 @@ describe('IAST Index', () => { it('should enable AppsecFsPlugin', () => { mockIast.enable(config) sinon.assert.calledOnceWithExactly(appsecFsPlugin.enable, IAST_MODULE) - expect(analyzers.enableAllAnalyzers).to.have.been.calledAfter(appsecFsPlugin.enable) + assert.strictEqual(analyzers.enableAllAnalyzers.calledAfter(appsecFsPlugin.enable), true) }) }) @@ -205,7 +205,7 @@ describe('IAST Index', () => { it('should not finish global context if not enabled before ', () => { mockIast.disable(config) - expect(mockOverheadController.finishGlobalContext).to.have.been.not.called + sinon.assert.notCalled(mockOverheadController.finishGlobalContext) }) }) diff --git a/packages/dd-trace/test/appsec/iast/overhead-controller.integration.spec.js b/packages/dd-trace/test/appsec/iast/overhead-controller.integration.spec.js index 1a8e3019dfb..88b90a28563 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.integration.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.integration.spec.js @@ -36,7 +36,7 @@ describe('IAST - overhead-controller - integration', () => { DD_TRACE_AGENT_PORT: agent.port, DD_IAST_ENABLED: 'true', DD_IAST_REQUEST_SAMPLING: '100', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', NODE_OPTIONS: '--require ./resources/init.js' } }) 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 dfa4f518799..625770eaddd 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -4,7 +4,7 @@ const assert = require('node:assert/strict') const { EventEmitter } = require('node:events') const axios = require('axios') -const { expect } = require('chai') + const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -50,7 +50,7 @@ describe('Overhead controller', () => { it('should populate request context', () => { const iastContext = {} overheadController.initializeRequestContext(iastContext) - expect(iastContext).to.have.nested.property(overheadController.OVERHEAD_CONTROLLER_CONTEXT_KEY) + assert.ok(hasNestedProperty(iastContext, overheadController.OVERHEAD_CONTROLLER_CONTEXT_KEY)) }) }) }) @@ -670,3 +670,16 @@ describe('Overhead controller', () => { }) }) }) + +function hasNestedProperty (obj, path) { + if (obj == null || typeof path !== 'string') return false + + const parts = path.split('.') + let cur = obj + for (const part of parts) { + if (cur == null || !Object.hasOwn(cur, part)) return false + cur = cur[part] + } + + return true +} diff --git a/packages/dd-trace/test/appsec/iast/resources/overhead-controller.js b/packages/dd-trace/test/appsec/iast/resources/overhead-controller.js index 82850afd2d5..88621a5f296 100644 --- a/packages/dd-trace/test/appsec/iast/resources/overhead-controller.js +++ b/packages/dd-trace/test/appsec/iast/resources/overhead-controller.js @@ -1,7 +1,7 @@ 'use strict' -const express = require('express') const crypto = require('crypto') +const express = require('express') const app = express() diff --git a/packages/dd-trace/test/appsec/iast/resources/vm.js b/packages/dd-trace/test/appsec/iast/resources/vm.js index 03cd5f5f765..7b67d9f7bc4 100644 --- a/packages/dd-trace/test/appsec/iast/resources/vm.js +++ b/packages/dd-trace/test/appsec/iast/resources/vm.js @@ -5,8 +5,8 @@ tracer.init({ flushInterval: 1 }) -const express = require('express') const vm = require('node:vm') +const express = require('express') const app = express() diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/index.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/index.spec.js index dbe2784bbc8..491829d0a38 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/index.spec.js @@ -1,6 +1,5 @@ 'use strict' -const { expect } = require('chai') const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -37,8 +36,7 @@ describe('IAST TaintTracking', () => { taintTracking.enableTaintTracking(config.iast) sinon.assert.calledOnce(taintTrackingOperations.enableTaintOperations) sinon.assert.calledOnce(taintTrackingPlugin.enable) - expect(taintTrackingOperations.setMaxTransactions) - .to.have.been.calledOnceWithExactly(config.iast.maxConcurrentRequests) + sinon.assert.calledOnceWithExactly(taintTrackingOperations.setMaxTransactions, config.iast.maxConcurrentRequests) }) it('Should disable both rewriter, taint tracking operations, plugin', () => { diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.express.plugin.spec.js index e8ee9063162..08f0c9766f6 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.express.plugin.spec.js @@ -1,9 +1,9 @@ 'use strict' +const { URL } = require('url') +const axios = require('axios') const { prepareTestServerForIastInExpress } = require('../utils') const { withVersions } = require('../../../setup/mocha') -const axios = require('axios') -const { URL } = require('url') function noop () {} diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.fastify.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.fastify.plugin.spec.js index ec1ca820b94..939b3bf8d76 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.fastify.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.fastify.plugin.spec.js @@ -1,9 +1,9 @@ 'use strict' +const { URL } = require('url') +const axios = require('axios') const { prepareTestServerForIastInFastify } = require('../utils') const { withVersions } = require('../../../setup/mocha') -const axios = require('axios') -const { URL } = require('url') function noop () {} diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js index 4cd955e3ec6..54d51b46770 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const dc = require('dc-polyfill') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') @@ -104,7 +103,7 @@ describe('IAST Taint tracking plugin', () => { } taintTrackingPlugin._taintTrackingHandler(originType, objToBeTainted) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith(iastContext, objToBeTainted, originType) + sinon.assert.calledOnceWithExactly(taintTrackingOperations.taintObject, iastContext, objToBeTainted, originType) }) it('Should taint property in object', () => { @@ -116,7 +115,8 @@ describe('IAST Taint tracking plugin', () => { } taintTrackingPlugin._taintTrackingHandler(originType, objToBeTainted, propertyToBeTainted) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, objToBeTainted[propertyToBeTainted], originType @@ -134,7 +134,8 @@ describe('IAST Taint tracking plugin', () => { objToBeTainted[propertyToBeTainted].self = objToBeTainted taintTrackingPlugin._taintTrackingHandler(originType, objToBeTainted, propertyToBeTainted) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, objToBeTainted[propertyToBeTainted], originType @@ -160,7 +161,8 @@ describe('IAST Taint tracking plugin', () => { queryReadFinishChannel.publish({ query: req.query }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.query, 'http.request.parameter' @@ -176,7 +178,8 @@ describe('IAST Taint tracking plugin', () => { bodyParserFinishChannel.publish({ req }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.body, 'http.request.body' @@ -192,7 +195,8 @@ describe('IAST Taint tracking plugin', () => { middlewareNextChannel.publish({ req }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.body, 'http.request.body' @@ -210,7 +214,8 @@ describe('IAST Taint tracking plugin', () => { middlewareNextChannel.publish({ req }) bodyParserFinishChannel.publish({ req }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.body, 'http.request.body' @@ -224,7 +229,8 @@ describe('IAST Taint tracking plugin', () => { cookieParseFinishCh.publish({ cookies }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, cookies, HTTP_REQUEST_COOKIE_VALUE @@ -239,7 +245,8 @@ describe('IAST Taint tracking plugin', () => { } processParamsStartCh.publish({ req }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.params, HTTP_REQUEST_PATH_PARAM @@ -254,7 +261,8 @@ describe('IAST Taint tracking plugin', () => { } routerParamStartCh.publish({ req }) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.params, HTTP_REQUEST_PATH_PARAM @@ -277,13 +285,15 @@ describe('IAST Taint tracking plugin', () => { } taintTrackingPlugin.taintRequest(req, iastContext) - expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.taintObject, iastContext, req.headers, HTTP_REQUEST_HEADER_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.newTaintedString, iastContext, req.url, HTTP_REQUEST_URI, @@ -325,7 +335,8 @@ describe('IAST Taint tracking plugin', () => { }] sequelizeFinish.publish({ result }) - expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.newTaintedString, iastContext, 'string value 1', '0.name', @@ -337,7 +348,8 @@ describe('IAST Taint tracking plugin', () => { const result = { id: 1, description: 'value' } sequelizeFinish.publish({ result }) - expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.newTaintedString, iastContext, 'value', 'description', @@ -378,14 +390,16 @@ describe('IAST Taint tracking plugin', () => { ] sequelizeFinish.publish({ result }) - expect(taintTrackingOperations.newTaintedString).to.be.calledTwice - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledTwice(taintTrackingOperations.newTaintedString) + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'value', '0.description', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'child1', '0.children.0.name', @@ -418,14 +432,16 @@ describe('IAST Taint tracking plugin', () => { }] sequelizeFinish.publish({ result }) - expect(taintTrackingOperations.newTaintedString).to.be.calledTwice - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledTwice(taintTrackingOperations.newTaintedString) + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'string value 1', '0.name', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'string value 2', '1.name', @@ -437,7 +453,8 @@ describe('IAST Taint tracking plugin', () => { const result = { id: 1, description: 'value' } sequelizeFinish.publish({ result }) - expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + sinon.assert.calledOnceWithExactly( + taintTrackingOperations.newTaintedString, iastContext, 'value', 'description', @@ -504,38 +521,44 @@ describe('IAST Taint tracking plugin', () => { ] sequelizeFinish.publish({ result }) - expect(taintTrackingOperations.newTaintedString).to.callCount(6) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.callCount(taintTrackingOperations.newTaintedString, 6) + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'value', '0.description', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'child1', '0.children.0.name', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'child2', '0.children.1.name', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'value2', '1.description', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'child4', '1.children.0.name', SQL_ROW_VALUE ) - expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + sinon.assert.calledWith( + taintTrackingOperations.newTaintedString, iastContext, 'child5', '1.children.1.name', diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/plugins/kafka.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/plugins/kafka.spec.js index 5306d30ead5..51e20e4fda9 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/plugins/kafka.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/plugins/kafka.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -46,7 +45,7 @@ describe('Kafka consumer plugin', () => { afterEach(sinon.restore) it('should subscribe to dd-trace:kafkajs:consumer:afterStart channel', () => { - expect(addSub).to.be.calledOnceWith({ + sinon.assert.calledOnceWithMatch(addSub, { channelName: 'dd-trace:kafkajs:consumer:afterStart', tag: [KAFKA_MESSAGE_KEY, KAFKA_MESSAGE_VALUE] }) @@ -60,10 +59,10 @@ describe('Kafka consumer plugin', () => { handler({ message }) - expect(newTaintedObject).to.be.calledTwice + sinon.assert.calledTwice(newTaintedObject) - expect(newTaintedObject.firstCall).to.be.calledWith(iastContext, message.key, undefined, KAFKA_MESSAGE_KEY) - expect(newTaintedObject.secondCall).to.be.calledWith(iastContext, message.value, undefined, KAFKA_MESSAGE_VALUE) + sinon.assert.calledWith(newTaintedObject.firstCall, iastContext, message.key, undefined, KAFKA_MESSAGE_KEY) + sinon.assert.calledWith(newTaintedObject.secondCall, iastContext, message.value, undefined, KAFKA_MESSAGE_VALUE) }) it('should taint key Buffer.toString method', () => { @@ -76,7 +75,7 @@ describe('Kafka consumer plugin', () => { const keyStr = message.key.toString() - expect(newTaintedString).to.be.calledOnceWith(iastContext, keyStr, undefined, KAFKA_MESSAGE_KEY) + sinon.assert.calledOnceWithExactly(newTaintedString, iastContext, keyStr, undefined, KAFKA_MESSAGE_KEY) }) it('should taint value Buffer.toString method', () => { @@ -89,7 +88,7 @@ describe('Kafka consumer plugin', () => { const valueStr = message.value.toString() - expect(newTaintedString).to.be.calledOnceWith(iastContext, valueStr, undefined, KAFKA_MESSAGE_VALUE) + sinon.assert.calledOnceWithExactly(newTaintedString, iastContext, valueStr, undefined, KAFKA_MESSAGE_VALUE) }) it('should not fail with an unknown kafka message', () => { diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter-telemetry.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter-telemetry.spec.js index 17ba5d9b00f..9d886a2972a 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter-telemetry.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter-telemetry.spec.js @@ -1,6 +1,5 @@ 'use strict' -const { expect } = require('chai') const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -45,7 +44,7 @@ describe('rewriter telemetry', () => { } incrementTelemetryIfNeeded(metrics) - expect(instrumentedPropagationInc).to.be.calledOnceWith(undefined, metrics.instrumentedPropagation) + sinon.assert.calledOnceWithExactly(instrumentedPropagationInc, undefined, metrics.instrumentedPropagation) }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js index 85c99dda5f6..10cae4437f1 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/rewriter.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const dc = require('dc-polyfill') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') @@ -277,7 +276,7 @@ describe('IAST Rewriter', () => { port.postMessage({ type: constants.REWRITTEN_MESSAGE, data }) waitUntilCheckSuccess(() => { - expect(cacheRewrittenSourceMap).to.be.calledOnceWith('file.js', content) + sinon.assert.calledOnceWithExactly(cacheRewrittenSourceMap, 'file.js', content) done() }) @@ -297,7 +296,7 @@ describe('IAST Rewriter', () => { port.postMessage({ type: constants.REWRITTEN_MESSAGE, data }) waitUntilCheckSuccess(() => { - expect(rewriterTelemetry.incrementTelemetryIfNeeded).to.be.calledOnceWith(metrics) + sinon.assert.calledOnceWithExactly(rewriterTelemetry.incrementTelemetryIfNeeded, metrics) done() }) @@ -339,13 +338,13 @@ describe('IAST Rewriter', () => { port.postMessage({ type: constants.LOG_MESSAGE, data }) waitUntilCheckSuccess(() => { - expect(log.error).to.be.calledOnceWith(...messages) + sinon.assert.calledOnceWithExactly(log.error, ...messages) done() }) }) it('should call port1.on before port1.unref', () => { - expect(port1On).to.be.calledBefore(port1Unref) + assert.strictEqual(port1On.calledBefore(port1Unref), true) }) }) }) @@ -377,7 +376,7 @@ describe('IAST Rewriter', () => { const location = { path: 'test', line: 42, column: 4 } rewriter.getOriginalPathAndLineFromSourceMap(location) - expect(getOriginalPathAndLineFromSourceMap).to.be.calledOnceWithExactly('test', 42, 4) + sinon.assert.calledOnceWithExactly(getOriginalPathAndLineFromSourceMap, 'test', 42, 4) }) it('should not call native getOriginalPathAndLineFromSourceMap if --enable-source-maps is present', () => { diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/secure-marks-generator.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/secure-marks-generator.spec.js index 0183264c8e2..bbb2156946f 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/secure-marks-generator.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/secure-marks-generator.spec.js @@ -1,8 +1,8 @@ 'use strict' -const { getNextSecureMark, reset } = require('../../../../src/appsec/iast/taint-tracking/secure-marks-generator') const assert = require('node:assert') const { describe, it, beforeEach } = require('mocha') +const { getNextSecureMark, reset } = require('../../../../src/appsec/iast/taint-tracking/secure-marks-generator') describe('test secure marks generator', () => { beforeEach(() => { diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-operations.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-operations.spec.js index 40143c6bf29..e22a79021e9 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-operations.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-operations.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -156,10 +155,20 @@ describe('IAST TaintTracking Operations', () => { const result = taintTrackingOperations.taintObject(iastContext, obj, null) sinon.assert.calledTwice(taintedUtilsMock.newTaintedString) - expect(taintedUtilsMock.newTaintedString.firstCall).to.have.been - .calledWithExactly(transactionId, 'child', 'child.value', null) - expect(taintedUtilsMock.newTaintedString.secondCall).to.have.been - .calledWithExactly(transactionId, 'parent', 'value', null) + sinon.assert.calledWithExactly( + taintedUtilsMock.newTaintedString.firstCall, + transactionId, + 'child', + 'child.value', + null + ) + sinon.assert.calledWithExactly( + taintedUtilsMock.newTaintedString.secondCall, + transactionId, + 'parent', + 'value', + null + ) assert.deepStrictEqual(result, expected) }) @@ -227,9 +236,7 @@ describe('IAST TaintTracking Operations', () => { [taintTrackingOperations.IAST_TRANSACTION_ID]: transactionId } taintTrackingOperations.removeTransaction(iastContext) - expect(taintedUtils.removeTransaction).to.be.calledWithExactly( - transactionId - ) + sinon.assert.calledWithExactly(taintedUtils.removeTransaction, transactionId) assert.strictEqual(iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], undefined) }) @@ -258,7 +265,7 @@ describe('IAST TaintTracking Operations', () => { taintTrackingOperations.enableTaintOperations(iastTelemetry.verbosity) taintTrackingOperations.removeTransaction(iastContext) - expect(requestTaintedInc).to.be.calledOnceWith(iastContext, 5) + sinon.assert.calledOnceWithExactly(requestTaintedInc, iastContext, 5) }) }) @@ -325,7 +332,7 @@ describe('IAST TaintTracking Operations', () => { global._ddiast.plusOperator('helloworld', 'hello', 'world') sinon.assert.called(taintedUtils.concat) - expect(executedPropagationIncrease).to.be.calledOnceWith(context) + sinon.assert.calledOnceWithExactly(executedPropagationIncrease, context) }) }) @@ -339,8 +346,13 @@ describe('IAST TaintTracking Operations', () => { const type = 'REQUEST' taintTrackingOperations.newTaintedString(iastContext, value, param, type) sinon.assert.called(taintedUtils.newTaintedString) - expect(taintedUtils.newTaintedString).to.be - .calledWithExactly(iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], value, param, type) + sinon.assert.calledWithExactly( + taintedUtils.newTaintedString, + iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], + value, + param, + type + ) }) it('Given iastContext with undefined IAST_TRANSACTION_ID should not call TaintedUtils.newTaintedString', () => { @@ -373,8 +385,13 @@ describe('IAST TaintTracking Operations', () => { const type = 'REQUEST' taintTrackingOperations.newTaintedObject(iastContext, value, param, type) sinon.assert.called(taintedUtils.newTaintedObject) - expect(taintedUtils.newTaintedObject).to.be - .calledWithExactly(iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], value, param, type) + sinon.assert.calledWithExactly( + taintedUtils.newTaintedObject, + iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], + value, + param, + type + ) }) it('Given iastContext with undefined IAST_TRANSACTION_ID should not call TaintedUtils.newTaintedObject', () => { @@ -405,7 +422,8 @@ describe('IAST TaintTracking Operations', () => { const value = 'value' taintTrackingOperations.isTainted(iastContext, value) sinon.assert.called(taintedUtils.isTainted) - expect(taintedUtils.isTainted).to.be.calledWithExactly( + sinon.assert.calledWithExactly( + taintedUtils.isTainted, iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], value ) @@ -432,7 +450,8 @@ describe('IAST TaintTracking Operations', () => { const value = 'value' taintTrackingOperations.getRanges(iastContext, value) sinon.assert.called(taintedUtils.getRanges) - expect(taintedUtils.getRanges).to.be.calledWithExactly( + sinon.assert.calledWithExactly( + taintedUtils.getRanges, iastContext[taintTrackingOperations.IAST_TRANSACTION_ID], value ) diff --git a/packages/dd-trace/test/appsec/iast/telemetry/iast-metric.spec.js b/packages/dd-trace/test/appsec/iast/telemetry/iast-metric.spec.js index 3db8e17ac07..0d4f489622a 100644 --- a/packages/dd-trace/test/appsec/iast/telemetry/iast-metric.spec.js +++ b/packages/dd-trace/test/appsec/iast/telemetry/iast-metric.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -50,8 +49,8 @@ describe('Metrics', () => { metric.inc(context) - expect(reqNamespace.count).to.be.calledOnceWith(metric.name) - expect(inc).to.be.calledOnceWith(1) + sinon.assert.calledOnceWithMatch(reqNamespace.count, metric.name) + sinon.assert.calledOnceWithExactly(inc, 1) }) it('should add by 42 the metric value', () => { @@ -59,8 +58,8 @@ describe('Metrics', () => { metric.inc(context, 42) - expect(reqNamespace.count).to.be.calledOnceWith(metric.name) - expect(inc).to.be.calledOnceWith(42) + sinon.assert.calledOnceWithMatch(reqNamespace.count, metric.name) + sinon.assert.calledOnceWithExactly(inc, 42) }) it('should increase by one the metric tag value', () => { @@ -68,8 +67,8 @@ describe('Metrics', () => { metric.inc(context, 'tagKey:tag1') - expect(reqNamespace.count).to.be.calledOnceWith(metric.name, 'tagKey:tag1') - expect(inc).to.be.calledOnceWith(1) + sinon.assert.calledOnceWithExactly(reqNamespace.count, metric.name, 'tagKey:tag1') + sinon.assert.calledOnceWithExactly(inc, 1) }) it('should add by 42 the metric tag value', () => { @@ -77,8 +76,8 @@ describe('Metrics', () => { metric.inc(context, 'tagKey:tag1', 42) - expect(reqNamespace.count).to.be.calledOnceWith(metric.name, 'tagKey:tag1') - expect(inc).to.be.calledOnceWith(42) + sinon.assert.calledOnceWithExactly(reqNamespace.count, metric.name, 'tagKey:tag1') + sinon.assert.calledOnceWithExactly(inc, 42) }) it('should format tags according with its tagKey', () => { @@ -86,7 +85,7 @@ describe('Metrics', () => { metric.formatTags('tag1', 'tag2').forEach(tag => metric.inc(context, tag, 42)) - expect(reqNamespace.count).to.be.calledTwice + sinon.assert.calledTwice(reqNamespace.count) assert.deepStrictEqual(reqNamespace.count.firstCall.args, [metric.name, ['tagKey:tag1']]) assert.deepStrictEqual(reqNamespace.count.secondCall.args, [metric.name, ['tagKey:tag2']]) }) @@ -122,7 +121,7 @@ describe('Metrics', () => { const noTagged = new NoTaggedIastMetric('notagged', 'scope') noTagged.inc() - expect(inc).to.be.calledOnceWith(1) + sinon.assert.calledOnceWithExactly(inc, 1) }) it('should reuse previous metric when calling add multiple times', () => { @@ -134,7 +133,7 @@ describe('Metrics', () => { noTagged.inc(undefined, 42) noTagged.inc(undefined, 42) - expect(superCount).to.be.calledOnceWith('notagged') + sinon.assert.calledOnceWithMatch(superCount, 'notagged') }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/telemetry/index.spec.js b/packages/dd-trace/test/appsec/iast/telemetry/index.spec.js index be9949710d5..fe5b5543b58 100644 --- a/packages/dd-trace/test/appsec/iast/telemetry/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/telemetry/index.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert/strict') const axios = require('axios') -const { expect } = require('chai') + const { after, afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -116,7 +116,7 @@ describe('Telemetry', () => { const iastContext = {} iastTelemetry.onRequestStart(iastContext) - expect(initRequestNamespace).to.be.calledOnceWith(iastContext) + sinon.assert.calledOnceWithExactly(initRequestNamespace, iastContext) }) it('should not call init if enabled and verbosity is Off', () => { @@ -133,7 +133,7 @@ describe('Telemetry', () => { const iastContext = {} iastTelemetry.onRequestStart(iastContext) - expect(initRequestNamespace).to.not.be.calledOnce + sinon.assert.notCalled(initRequestNamespace) }) }) @@ -144,7 +144,7 @@ describe('Telemetry', () => { const iastContext = {} iastTelemetry.onRequestEnd(iastContext) - expect(finalizeRequestNamespace).to.be.calledOnceWith(iastContext) + sinon.assert.calledOnceWithMatch(finalizeRequestNamespace, iastContext) }) it('should not call finalizeRequestNamespace if enabled and verbosity is Off', () => { @@ -161,7 +161,7 @@ describe('Telemetry', () => { const iastContext = {} iastTelemetry.onRequestEnd(iastContext) - expect(finalizeRequestNamespace).to.not.be.calledOnce + sinon.assert.notCalled(finalizeRequestNamespace) }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js b/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js index 1317d1d4975..a70e6721ec6 100644 --- a/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js +++ b/packages/dd-trace/test/appsec/iast/telemetry/logs.spec.js @@ -1,6 +1,5 @@ 'use strict' -const { expect } = require('chai') const { describe, it, before, after } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -59,7 +58,7 @@ describe('Telemetry logs', () => { clock.tick(3000) - expect(start).to.be.calledOnceWith(config) - expect(send).to.be.calledOnceWith(config) + sinon.assert.calledOnceWithExactly(start, config) + sinon.assert.calledOnceWithMatch(send, config) }) }) diff --git a/packages/dd-trace/test/appsec/iast/telemetry/namespaces.spec.js b/packages/dd-trace/test/appsec/iast/telemetry/namespaces.spec.js index dce859d3d2c..7bf3530d5c6 100644 --- a/packages/dd-trace/test/appsec/iast/telemetry/namespaces.spec.js +++ b/packages/dd-trace/test/appsec/iast/telemetry/namespaces.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') const { @@ -87,9 +86,9 @@ describe('IAST metric namespaces', () => { finalizeRequestNamespace(context, rootSpan) - expect(count).to.be.calledTwice + sinon.assert.calledTwice(count) assert.deepStrictEqual(count.firstCall.args, [REQUEST_TAINTED, ['tag1:test']]) - expect(metric.inc).to.be.calledTwice + sinon.assert.calledTwice(metric.inc) assert.strictEqual(metric.inc.firstCall.args[0], 10) assert.deepStrictEqual(count.secondCall.args, [EXECUTED_SINK, undefined]) @@ -201,7 +200,7 @@ describe('IastNamespace', () => { namespace.getMetric('metric.name', tags) namespace.getMetric('metric.name', tags) - expect(count).to.be.calledOnceWith('metric.name', tags) + sinon.assert.calledOnceWithExactly(count, 'metric.name', tags) }) it('should reuse a previously created metric', () => { diff --git a/packages/dd-trace/test/appsec/iast/telemetry/span-tags.spec.js b/packages/dd-trace/test/appsec/iast/telemetry/span-tags.spec.js index 51103445ce2..c6e5ad59fb0 100644 --- a/packages/dd-trace/test/appsec/iast/telemetry/span-tags.spec.js +++ b/packages/dd-trace/test/appsec/iast/telemetry/span-tags.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') @@ -35,7 +34,7 @@ describe('Telemetry Span tags', () => { addMetricsToSpan(rootSpan, metrics.series, tagPrefix) - expect(rootSpan.addTags).to.be.calledTwice + sinon.assert.calledTwice(rootSpan.addTags) assert.deepStrictEqual(rootSpan.addTags.firstCall.args[0], { '_dd.test.executed.source.source_type_1': 42 }) assert.deepStrictEqual(rootSpan.addTags.secondCall.args[0], { '_dd.test.executed.sink.sink_type_1': 3 }) }) @@ -49,7 +48,7 @@ describe('Telemetry Span tags', () => { addMetricsToSpan(rootSpan, metrics.series, tagPrefix) - expect(rootSpan.addTags).to.be.calledOnceWithExactly({ '_dd.test.executed.source.source_type_1': 74 }) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { '_dd.test.executed.source.source_type_1': 74 }) }) it('should add span tags with tag name like \'tagPrefix.metricName.tagKey\' for different tagged metrics', () => { @@ -63,7 +62,7 @@ describe('Telemetry Span tags', () => { addMetricsToSpan(rootSpan, metrics.series, tagPrefix) - expect(rootSpan.addTags).to.be.calledTwice + sinon.assert.calledTwice(rootSpan.addTags) assert.deepStrictEqual(rootSpan.addTags.firstCall.args[0], { '_dd.test.executed.source.source_type_1': 74 }) assert.deepStrictEqual(rootSpan.addTags.secondCall.args[0], { '_dd.test.executed.source.source_type_2': 2 }) }) @@ -75,6 +74,6 @@ describe('Telemetry Span tags', () => { addMetricsToSpan(rootSpan, metrics.series, tagPrefix) - expect(rootSpan.addTags).to.be.calledOnceWithExactly({ '_dd.test.request.tainted': 42 }) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { '_dd.test.request.tainted': 42 }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/utils.js b/packages/dd-trace/test/appsec/iast/utils.js index 93b376b1c5d..66e179a5af4 100644 --- a/packages/dd-trace/test/appsec/iast/utils.js +++ b/packages/dd-trace/test/appsec/iast/utils.js @@ -7,7 +7,7 @@ const path = require('node:path') const msgpack = require('@msgpack/msgpack') const axios = require('axios') -const { expect } = require('chai') + const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const iast = require('../../../src/appsec/iast') @@ -163,7 +163,7 @@ function checkNoVulnerabilityInRequest (vulnerability, config, done, makeRequest if (traces[0][0].type !== 'web') throw new Error('Not a web span') // iastJson == undefiend is valid const iastJson = traces[0][0].meta['_dd.iast.json'] || '' - expect(iastJson).to.not.include(`"${vulnerability}"`) + assert.ok(!(iastJson).includes(`"${vulnerability}"`)) }) .then(done) .catch(done) diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js index 10abfad787c..d7821bf8797 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js +++ b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/index.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') @@ -145,8 +144,11 @@ describe('Vulnerability formatter', () => { it('should set custom redaction patterns', () => { vulnerabilityFormatter.setRedactVulnerabilities(true, 'customNamePattern', 'customValuePattern') - expect(sensitiveHandler.setRedactionPatterns) - .to.have.been.calledOnceWithExactly('customNamePattern', 'customValuePattern') + sinon.assert.calledOnceWithExactly( + sensitiveHandler.setRedactionPatterns, + 'customNamePattern', + 'customValuePattern' + ) }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js b/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js index 141942f16e8..796b755efdb 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js +++ b/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const sinon = require('sinon') const { addVulnerability, sendVulnerabilities, clearCache, start, stop } = require('../../../src/appsec/iast/vulnerability-reporter') @@ -144,8 +143,7 @@ describe('vulnerability-reporter', () => { '_dd.iast.json': '{"sources":[],"vulnerabilities":[{"type":"INSECURE_HASHING","hash":3410512655,' + '"evidence":{"value":"sha1"},"location":{"spanId":42,"stackId":"1","path":"filename.js","line":73}}]}' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(onTheFlySpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, onTheFlySpan, USER_KEEP, ASM) sinon.assert.calledOnce(onTheFlySpan.finish) }) @@ -385,10 +383,8 @@ describe('vulnerability-reporter', () => { }) sinon.assert.calledTwice(prioritySampler.setPriority) - expect(prioritySampler.setPriority.firstCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) - expect(prioritySampler.setPriority.secondCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.firstCall, span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.secondCall, span, USER_KEEP, ASM) }) it('should send multiple vulnerabilities with same tainted source', () => { @@ -445,10 +441,8 @@ describe('vulnerability-reporter', () => { }) sinon.assert.calledTwice(prioritySampler.setPriority) - expect(prioritySampler.setPriority.firstCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) - expect(prioritySampler.setPriority.secondCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.firstCall, span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.secondCall, span, USER_KEEP, ASM) }) it('should send once with multiple vulnerabilities', () => { @@ -470,12 +464,9 @@ describe('vulnerability-reporter', () => { '"location":{"spanId":-5,"path":"/path/to/file3.js","line":3}}]}' }) sinon.assert.calledThrice(prioritySampler.setPriority) - expect(prioritySampler.setPriority.firstCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) - expect(prioritySampler.setPriority.secondCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) - expect(prioritySampler.setPriority.thirdCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.firstCall, span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.secondCall, span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.thirdCall, span, USER_KEEP, ASM) }) it('should send once vulnerability with one vulnerability', () => { @@ -536,10 +527,8 @@ describe('vulnerability-reporter', () => { '{"spanId":888,"path":"filename.js","line":88}}]}' }) sinon.assert.calledTwice(prioritySampler.setPriority) - expect(prioritySampler.setPriority.firstCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) - expect(prioritySampler.setPriority.secondCall) - .to.have.been.calledWithExactly(span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.firstCall, span, USER_KEEP, ASM) + sinon.assert.calledWithExactly(prioritySampler.setPriority.secondCall, span, USER_KEEP, ASM) }) }) diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index 5429333a6ae..85c27dbc063 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -1,14 +1,13 @@ 'use strict' +const assert = require('node:assert/strict') +const path = require('node:path') +const zlib = require('node:zlib') const Axios = require('axios') const semver = require('semver') const sinon = require('sinon') const { describe, it, before, beforeEach, afterEach, after } = require('mocha') -const assert = require('node:assert/strict') -const path = require('node:path') -const zlib = require('node:zlib') - const { NODE_MAJOR } = require('../../../../version') const agent = require('../plugins/agent') const appsec = require('../../src/appsec') @@ -83,6 +82,8 @@ withVersions('express', 'express', version => { afterEach(() => { appsec.disable() + // TODO: Remove the workaround once https://github.com/sinonjs/sinon/issues/2671 is resolved + paramCallbackSpy.resetHistory() sinon.reset() }) diff --git a/packages/dd-trace/test/appsec/index.fastify.plugin.spec.js b/packages/dd-trace/test/appsec/index.fastify.plugin.spec.js index 24f8b288153..677dc7263b0 100644 --- a/packages/dd-trace/test/appsec/index.fastify.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.fastify.plugin.spec.js @@ -322,6 +322,9 @@ withVersions('fastify', 'fastify', '>=2', (fastifyVersion, _, fastifyLoadedVersi afterEach(() => { appsec.disable() + // TODO: Remove the workaround once https://github.com/sinonjs/sinon/issues/2671 is resolved + preHandlerHookSpy.resetHistory() + preValidationHookSpy.resetHistory() sinon.reset() }) diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 800e44f10f3..3479ecab7ef 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -4,7 +4,7 @@ const assert = require('node:assert/strict') const fs = require('node:fs') const axios = require('axios') -const { expect } = require('chai') + const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -178,8 +178,7 @@ describe('AppSec Index', function () { sinon.assert.calledOnceWithExactly(RuleManager.loadRules, config.appsec) sinon.assert.calledOnceWithExactly(Reporter.init, config.appsec) sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, 'anon', false) - expect(incomingHttpRequestStart.subscribe) - .to.have.been.calledOnceWithExactly(AppSec.incomingHttpStartTranslator) + sinon.assert.calledOnceWithExactly(incomingHttpRequestStart.subscribe, AppSec.incomingHttpStartTranslator) sinon.assert.calledOnceWithExactly(incomingHttpRequestEnd.subscribe, AppSec.incomingHttpEndTranslator) sinon.assert.calledOnce(graphql.enable) }) @@ -257,13 +256,13 @@ describe('AppSec Index', function () { } AppSec.enable(config) - expect(appsecTelemetry.enable).to.be.calledOnceWithExactly(config) + sinon.assert.calledOnceWithExactly(appsecTelemetry.enable, config) }) it('should call rasp enable', () => { AppSec.enable(config) - expect(rasp.enable).to.be.calledOnceWithExactly(config) + sinon.assert.calledOnceWithExactly(rasp.enable, config) }) it('should not call rasp enable when rasp is disabled', () => { @@ -289,8 +288,7 @@ describe('AppSec Index', function () { AppSec.disable() sinon.assert.calledOnce(RuleManager.clearAllRules) - expect(incomingHttpRequestStart.unsubscribe) - .to.have.been.calledOnceWithExactly(AppSec.incomingHttpStartTranslator) + sinon.assert.calledOnceWithExactly(incomingHttpRequestStart.unsubscribe, AppSec.incomingHttpStartTranslator) sinon.assert.calledOnceWithExactly(incomingHttpRequestEnd.unsubscribe, AppSec.incomingHttpEndTranslator) sinon.assert.calledOnce(graphql.disable) sinon.assert.calledOnce(rasp.disable) @@ -422,7 +420,7 @@ describe('AppSec Index', function () { AppSec.incomingHttpEndTranslator({ req, res }) - expect(waf.run).to.have.not.been.called + sinon.assert.notCalled(waf.run) sinon.assert.calledOnceWithExactly(Reporter.finishRequest, req, res, {}, undefined) }) @@ -505,7 +503,7 @@ describe('AppSec Index', function () { AppSec.incomingHttpEndTranslator({ req, res }) - expect(waf.run).to.have.not.been.called + sinon.assert.notCalled(waf.run) sinon.assert.calledOnceWithExactly(Reporter.finishRequest, req, res, {}, undefined) }) @@ -696,8 +694,8 @@ describe('AppSec Index', function () { responseBody.publish({ req: {}, body: 'string' }) responseBody.publish({ req: {}, body: null }) - expect(apiSecuritySampler.sampleRequest).to.not.been.called - expect(waf.run).to.not.been.called + sinon.assert.notCalled(apiSecuritySampler.sampleRequest) + sinon.assert.notCalled(waf.run) }) it('should not call to the waf if it is not a sampled request', () => { @@ -708,7 +706,7 @@ describe('AppSec Index', function () { responseBody.publish({ req, res, body: {} }) sinon.assert.calledOnceWithMatch(apiSecuritySampler.sampleRequest, req, res) - expect(waf.run).to.not.been.called + sinon.assert.notCalled(waf.run) }) it('should call to the waf if it is a sampled request', () => { @@ -915,7 +913,7 @@ describe('AppSec Index', function () { passportVerify.publish(payload) - expect(storage('legacy').getStore).to.have.been.calledOnce + sinon.assert.calledOnce(storage('legacy').getStore) sinon.assert.calledOnceWithExactly(web.root, req) sinon.assert.calledOnceWithExactly(UserTracking.trackLogin, payload.framework, @@ -942,7 +940,7 @@ describe('AppSec Index', function () { passportVerify.publish(payload) - expect(storage('legacy').getStore).to.have.been.calledOnce + sinon.assert.calledOnce(storage('legacy').getStore) sinon.assert.calledOnceWithExactly(web.root, req) sinon.assert.calledOnceWithExactly(UserTracking.trackLogin, payload.framework, @@ -969,7 +967,7 @@ describe('AppSec Index', function () { passportVerify.publish(payload) - expect(storage('legacy').getStore).to.have.been.calledOnce + sinon.assert.calledOnce(storage('legacy').getStore) sinon.assert.calledOnceWithExactly(log.warn, '[ASM] No rootSpan found in onPassportVerify') sinon.assert.notCalled(UserTracking.trackLogin) assert.strictEqual(abortController.signal.aborted, false) @@ -993,7 +991,7 @@ describe('AppSec Index', function () { passportUser.publish(payload) - expect(storage('legacy').getStore).to.have.been.calledOnce + sinon.assert.calledOnce(storage('legacy').getStore) sinon.assert.calledOnceWithExactly(web.root, req) sinon.assert.calledOnceWithExactly(UserTracking.trackUser, payload.user, @@ -1014,7 +1012,7 @@ describe('AppSec Index', function () { passportUser.publish(payload) - expect(storage('legacy').getStore).to.have.been.calledOnce + sinon.assert.calledOnce(storage('legacy').getStore) sinon.assert.calledOnceWithExactly(web.root, req) sinon.assert.calledOnceWithExactly(UserTracking.trackUser, payload.user, @@ -1035,7 +1033,7 @@ describe('AppSec Index', function () { passportUser.publish(payload) - expect(storage('legacy').getStore).to.have.been.calledOnce + sinon.assert.calledOnce(storage('legacy').getStore) sinon.assert.calledOnceWithExactly(log.warn, '[ASM] No rootSpan found in onPassportDeserializeUser') sinon.assert.notCalled(UserTracking.trackUser) assert.strictEqual(abortController.signal.aborted, false) @@ -1164,14 +1162,14 @@ describe('AppSec Index', function () { } } }, req) - expect(abortController.abort).to.have.not.been.called - expect(res.constructor.prototype.end).to.have.not.been.called + sinon.assert.notCalled(abortController.abort) + sinon.assert.notCalled(res.constructor.prototype.end) responseWriteHead.publish({ req, res, abortController, statusCode: 404, responseHeaders }) sinon.assert.calledOnce(waf.run) - expect(abortController.abort).to.have.not.been.called - expect(res.constructor.prototype.end).to.have.not.been.called + sinon.assert.notCalled(abortController.abort) + sinon.assert.notCalled(res.constructor.prototype.end) }) it('should not do anything without a root span', () => { @@ -1186,9 +1184,9 @@ describe('AppSec Index', function () { responseWriteHead.publish({ req, res, abortController, statusCode: 404, responseHeaders }) - expect(waf.run).to.have.not.been.called - expect(abortController.abort).to.have.not.been.called - expect(res.constructor.prototype.end).to.have.not.been.called + sinon.assert.notCalled(waf.run) + sinon.assert.notCalled(abortController.abort) + sinon.assert.notCalled(res.constructor.prototype.end) }) it('should call the WAF with responde code and headers', () => { @@ -1240,7 +1238,7 @@ describe('AppSec Index', function () { it('should not call abortController if response was not blocked', () => { responseSetHeader.publish({ res, abortController }) - expect(abortController.abort).to.have.not.been.calledOnce + sinon.assert.notCalled(abortController.abort) }) }) }) @@ -1331,6 +1329,14 @@ describe('IP blocking', function () { let http, appListener, port + function createTransaction (changes) { + return { + ...changes, + ack: sinon.spy(), + error: sinon.spy() + } + } + before(() => { return agent.load('http') .then(() => { @@ -1360,7 +1366,7 @@ describe('IP blocking', function () { } })) - RuleManager.updateWafFromRC({ toUnapply: [], toApply: [], toModify }) + RuleManager.updateWafFromRC(createTransaction({ toUnapply: [], toApply: [], toModify })) }) afterEach(() => { @@ -1462,11 +1468,11 @@ describe('IP blocking', function () { }] beforeEach(() => { - RuleManager.updateWafFromRC({ + RuleManager.updateWafFromRC(createTransaction({ toUnapply: [], toApply: [], toModify: [...toModify, ...toModifyCustomActions] - }) + })) }) afterEach(() => { @@ -1523,11 +1529,11 @@ describe('IP blocking', function () { }] beforeEach(() => { - RuleManager.updateWafFromRC({ + RuleManager.updateWafFromRC(createTransaction({ toUnapply: [], toApply: [], toModify: [...toModify, ...toModifyCustomActions] - }) + })) }) afterEach(() => { diff --git a/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js b/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js index ced35eff30a..9c5d0aed9ec 100644 --- a/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js +++ b/packages/dd-trace/test/appsec/rasp/command_injection.integration.spec.js @@ -2,11 +2,10 @@ const assert = require('node:assert/strict') +const path = require('node:path') const Axios = require('axios') const { describe, it, before, beforeEach, afterEach } = require('mocha') -const path = require('node:path') - const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') describe('RASP - command_injection - integration', () => { @@ -71,12 +70,12 @@ describe('RASP - command_injection - integration', () => { const evalSerie = series.find(s => s.metric === 'rasp.rule.eval') const matchSerie = series.find(s => s.metric === 'rasp.rule.match') - assert.ok(evalSerie != null) + assert.ok(evalSerie) assert.ok(evalSerie.tags.includes('rule_type:command_injection')) assert.ok(evalSerie.tags.includes(`rule_variant:${variant}`)) assert.strictEqual(evalSerie.type, 'count') - assert.ok(matchSerie != null) + assert.ok(matchSerie) assert.ok(matchSerie.tags.includes('rule_type:command_injection')) assert.ok(matchSerie.tags.includes(`rule_variant:${variant}`)) assert.strictEqual(matchSerie.type, 'count') diff --git a/packages/dd-trace/test/appsec/rasp/fs-plugin.spec.js b/packages/dd-trace/test/appsec/rasp/fs-plugin.spec.js index 6feb4f69e41..2061f6536a3 100644 --- a/packages/dd-trace/test/appsec/rasp/fs-plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/fs-plugin.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert/strict') +const path = require('node:path') const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const dc = require('dc-polyfill') -const path = require('node:path') const { storage } = require('../../../../datadog-core') const { AppsecFsPlugin } = require('../../../src/appsec/rasp/fs-plugin') diff --git a/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js index f0ff5929d4e..c89d78318ec 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js @@ -13,8 +13,8 @@ const { NODE_MAJOR } = require('../../../../../version') const agent = require('../../plugins/agent') const appsec = require('../../../src/appsec') const { withVersions } = require('../../setup/mocha') -const { checkRaspExecutedAndNotThreat, checkRaspExecutedAndHasThreat } = require('./utils') const { getConfigFresh } = require('../../helpers/config') +const { checkRaspExecutedAndNotThreat, checkRaspExecutedAndHasThreat } = require('./utils') describe('RASP - lfi', () => { let axios diff --git a/packages/dd-trace/test/appsec/rasp/lfi.integration.express.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.integration.express.plugin.spec.js index 9188e586ccf..272e50447e0 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.integration.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.integration.express.plugin.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') describe('RASP - lfi - integration - sync', () => { let axios, cwd, appFile, agent, proc @@ -24,8 +24,8 @@ describe('RASP - lfi - integration - sync', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_APPSEC_ENABLED: true, - DD_APPSEC_RASP_ENABLED: true, + DD_APPSEC_ENABLED: 'true', + DD_APPSEC_RASP_ENABLED: 'true', DD_APPSEC_RULES: path.join(cwd, 'resources', 'lfi_rasp_rules.json') } }) diff --git a/packages/dd-trace/test/appsec/rasp/rasp-metrics.integration.spec.js b/packages/dd-trace/test/appsec/rasp/rasp-metrics.integration.spec.js index 744b035323e..933caf559bd 100644 --- a/packages/dd-trace/test/appsec/rasp/rasp-metrics.integration.spec.js +++ b/packages/dd-trace/test/appsec/rasp/rasp-metrics.integration.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') describe('RASP metrics', () => { let axios, cwd, appFile @@ -30,9 +30,9 @@ describe('RASP metrics', () => { DD_TRACE_AGENT_PORT: agent.port, DD_APPSEC_ENABLED: 'true', DD_APPSEC_RASP_ENABLED: 'true', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', DD_APPSEC_RULES: path.join(cwd, 'resources', 'rasp_rules.json'), - DD_APPSEC_WAF_TIMEOUT: 0.1 + DD_APPSEC_WAF_TIMEOUT: '0.1' } }) axios = Axios.create({ baseURL: proc.url }) @@ -62,7 +62,7 @@ describe('RASP metrics', () => { const series = payload.payload.series const errorSerie = series.find(s => s.metric === 'rasp.error') - assert.ok(errorSerie != null) + assert.ok(errorSerie) assert.ok(errorSerie.tags.includes('waf_error:-127')) assert.strictEqual(errorSerie.type, 'count') } @@ -83,8 +83,8 @@ describe('RASP metrics', () => { DD_TRACE_AGENT_PORT: agent.port, DD_APPSEC_ENABLED: 'true', DD_APPSEC_RASP_ENABLED: 'true', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - DD_APPSEC_WAF_TIMEOUT: 1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + DD_APPSEC_WAF_TIMEOUT: '1' } }) axios = Axios.create({ baseURL: proc.url }) @@ -112,7 +112,7 @@ describe('RASP metrics', () => { const series = payload.payload.series const timeoutSerie = series.find(s => s.metric === 'rasp.timeout') - assert.ok(timeoutSerie != null) + assert.ok(timeoutSerie) assert.ok(timeoutSerie.tags.includes('rule_type:command_injection')) assert.ok(timeoutSerie.tags.includes('rule_variant:shell')) assert.strictEqual(timeoutSerie.type, 'count') diff --git a/packages/dd-trace/test/appsec/rasp/rasp_blocking.fastify.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/rasp_blocking.fastify.plugin.spec.js index d691954fefc..2d2c084a242 100644 --- a/packages/dd-trace/test/appsec/rasp/rasp_blocking.fastify.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/rasp_blocking.fastify.plugin.spec.js @@ -3,13 +3,12 @@ const assert = require('node:assert/strict') const path = require('node:path') -const agent = require('../../plugins/agent') -const { getConfigFresh } = require('../../helpers/config') -const appsec = require('../../../src/appsec') - const Axios = require('axios') const { describe, it, afterEach, before, after } = require('mocha') const sinon = require('sinon') +const agent = require('../../plugins/agent') +const { getConfigFresh } = require('../../helpers/config') +const appsec = require('../../../src/appsec') const { withVersions } = require('../../setup/mocha') const { json: blockedJson } = require('../../../src/appsec/blocked_templates') diff --git a/packages/dd-trace/test/appsec/rasp/resources/lfi-app/index.js b/packages/dd-trace/test/appsec/rasp/resources/lfi-app/index.js index bb81f4e202f..57bc9aed5f8 100644 --- a/packages/dd-trace/test/appsec/rasp/resources/lfi-app/index.js +++ b/packages/dd-trace/test/appsec/rasp/resources/lfi-app/index.js @@ -5,9 +5,10 @@ tracer.init({ flushInterval: 0 }) -const express = require('express') const { readFileSync } = require('fs') +const express = require('express') + const app = express() app.get('/lfi/sync', (req, res) => { diff --git a/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js b/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js index 7a1ed5aae5f..d9dd0a5ef1d 100644 --- a/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js +++ b/packages/dd-trace/test/appsec/rasp/resources/shi-app/index.js @@ -5,9 +5,10 @@ tracer.init({ flushInterval: 1 }) -const express = require('express') const childProcess = require('child_process') +const express = require('express') + const app = express() app.get('/shi/execFileSync', async (req, res) => { diff --git a/packages/dd-trace/test/appsec/rasp/sql_injection.integration.pg.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/sql_injection.integration.pg.plugin.spec.js index 978b6ef6fae..ceffb79616a 100644 --- a/packages/dd-trace/test/appsec/rasp/sql_injection.integration.pg.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/sql_injection.integration.pg.plugin.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../../integration-tests/helpers') // These test are here and not in the integration tests // because they require postgres instance describe('RASP - sql_injection - integration', () => { @@ -26,8 +26,8 @@ describe('RASP - sql_injection - integration', () => { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, - DD_APPSEC_ENABLED: true, - DD_APPSEC_RASP_ENABLED: true, + DD_APPSEC_ENABLED: 'true', + DD_APPSEC_RASP_ENABLED: 'true', DD_APPSEC_RULES: path.join(cwd, 'resources', 'rasp_rules.json') } }) diff --git a/packages/dd-trace/test/appsec/rasp/sql_injection.mysql2.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/sql_injection.mysql2.plugin.spec.js index 283fb3f972a..6b5742dd10b 100644 --- a/packages/dd-trace/test/appsec/rasp/sql_injection.mysql2.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/sql_injection.mysql2.plugin.spec.js @@ -2,12 +2,12 @@ const assert = require('node:assert/strict') +const path = require('path') +const Axios = require('axios') const agent = require('../../plugins/agent') const appsec = require('../../../src/appsec') const getConfig = require('../../../src/config') const { withVersions } = require('../../setup/mocha') -const path = require('path') -const Axios = require('axios') const { checkRaspExecutedAndNotThreat, checkRaspExecutedAndHasThreat } = require('./utils') describe('RASP - sql_injection', () => { diff --git a/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js index 78441c37ceb..e8a7af1f5b0 100644 --- a/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/sql_injection.pg.plugin.spec.js @@ -1,10 +1,10 @@ 'use strict' -const Axios = require('axios') const assert = require('node:assert') +const path = require('node:path') +const Axios = require('axios') const { describe, it, beforeEach, afterEach, before, after } = require('mocha') const sinon = require('sinon') -const path = require('node:path') const agent = require('../../plugins/agent') const appsec = require('../../../src/appsec') diff --git a/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js b/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js index a0859f3b92e..28d6797e5e7 100644 --- a/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js +++ b/packages/dd-trace/test/appsec/rasp/sql_injection.spec.js @@ -2,9 +2,9 @@ const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') +const proxyquire = require('proxyquire') const { pgQueryStart, mysql2OuterQueryStart } = require('../../../src/appsec/channels') const addresses = require('../../../src/appsec/addresses') -const proxyquire = require('proxyquire') describe('RASP - sql_injection', () => { let waf, legacyStorage, sqli diff --git a/packages/dd-trace/test/appsec/rasp/utils.spec.js b/packages/dd-trace/test/appsec/rasp/utils.spec.js index 897c478aa69..ed745f40e25 100644 --- a/packages/dd-trace/test/appsec/rasp/utils.spec.js +++ b/packages/dd-trace/test/appsec/rasp/utils.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -215,9 +214,8 @@ describe('RASP - utils.js', () => { assert.strictEqual(error.message, 'DatadogRaspAbortError') assert.strictEqual(error.blockingAction, blockingAction) assert.strictEqual(error.raspRule, raspRule) - assert.ok(Object.hasOwn(error, 'req')) - assert.ok(Object.hasOwn(error, 'res')) - expect(Object.keys(error)).to.not.include.members(['req', 'res']) + assert.strictEqual(Object.getOwnPropertyDescriptor(error, 'req')?.enumerable, false) + assert.strictEqual(Object.getOwnPropertyDescriptor(error, 'res')?.enumerable, false) }) }) }) diff --git a/packages/dd-trace/test/appsec/remote_config.spec.js b/packages/dd-trace/test/appsec/remote_config.spec.js new file mode 100644 index 00000000000..70e45fdd9a2 --- /dev/null +++ b/packages/dd-trace/test/appsec/remote_config.spec.js @@ -0,0 +1,378 @@ +'use strict' + +const assert = require('node:assert/strict') + +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire') + +const getConfig = require('../../src/config') +const RuleManager = require('../../src/appsec/rule_manager') +const RemoteConfigCapabilities = require('../../src/remote_config/capabilities') +const { assertObjectContains } = require('../../../../integration-tests/helpers') + +require('../setup/core') + +let config +let rc +let UserTracking +let log +let telemetry +let appsec +let appsecRemoteConfig + +describe('AppSec Remote Config', () => { + beforeEach(() => { + config = getConfig({ + appsec: { + enabled: undefined, + eventTracking: { + mode: 'identification' + } + } + }) + + rc = { + updateCapabilities: sinon.spy(), + setBatchHandler: sinon.spy(), + removeBatchHandler: sinon.spy(), + setProductHandler: sinon.spy(), + removeProductHandler: sinon.spy(), + subscribeProducts: sinon.spy(), + unsubscribeProducts: sinon.spy() + } + + UserTracking = { + setCollectionMode: sinon.stub() + } + + log = { + error: sinon.stub() + } + + telemetry = { + updateConfig: sinon.stub() + } + + appsec = { + enable: sinon.spy(), + disable: sinon.spy() + } + + appsecRemoteConfig = proxyquire('../../src/appsec/remote_config', { + './user_tracking': UserTracking, + '../log': log, + '../telemetry': telemetry + }) + }) + + describe('enable', () => { + it('should listen to remote config when appsec is not explicitly configured', () => { + config.appsec.enabled = undefined + + appsecRemoteConfig.enable(rc, config, appsec) + + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION, true) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + sinon.assert.calledWith(rc.setProductHandler, 'ASM_FEATURES') + assert.strictEqual(typeof rc.setProductHandler.firstCall.args[1], 'function') + }) + + it('should listen to remote config when appsec is explicitly configured as enabled=true', () => { + config.appsec.enabled = true + + appsecRemoteConfig.enable(rc, config, appsec) + + sinon.assert.neverCalledWith(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + sinon.assert.calledOnceWithMatch(rc.setProductHandler, 'ASM_FEATURES') + assert.strictEqual(typeof rc.setProductHandler.firstCall.args[1], 'function') + }) + + it('should not listen to remote config when appsec is explicitly configured as enabled=false', () => { + config.appsec.enabled = false + + appsecRemoteConfig.enable(rc, config, appsec) + + sinon.assert.neverCalledWith(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION, true) + sinon.assert.neverCalledWith(rc.updateCapabilities, RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + sinon.assert.notCalled(rc.setProductHandler) + }) + + describe('ASM_FEATURES remote config listener', () => { + let listener + + beforeEach(() => { + config.appsec.enabled = undefined + appsecRemoteConfig.enable(rc, config, appsec) + + listener = rc.setProductHandler.firstCall.args[1] + }) + + it('should do nothing when listener is called with falsy rcConfig', () => { + listener('apply', null) + + sinon.assert.notCalled(appsec.enable) + sinon.assert.notCalled(appsec.disable) + sinon.assert.notCalled(UserTracking.setCollectionMode) + }) + + it('should not call enableOrDisableAppsec when activation is not ONECLICK', () => { + // When config.appsec.enabled is true, activation is not ONECLICK + config.appsec.enabled = true + appsecRemoteConfig.enable(rc, config, appsec) + + const listener2 = rc.setProductHandler.secondCall.args[1] + appsec.enable.resetHistory() + appsec.disable.resetHistory() + + listener2('apply', { asm: { enabled: true } }) + + // Should not call enable/disable because activation is not ONECLICK + sinon.assert.notCalled(appsec.enable) + sinon.assert.notCalled(appsec.disable) + }) + + it('should enable appsec when listener is called with apply and enabled', () => { + listener('apply', { asm: { enabled: true } }) + + sinon.assert.called(appsec.enable) + }) + + it('should enable appsec when listener is called with modify and enabled', () => { + listener('modify', { asm: { enabled: true } }) + + sinon.assert.called(appsec.enable) + }) + + it('should disable appsec when listener is called with unapply and enabled', () => { + listener('unapply', { asm: { enabled: true } }) + + sinon.assert.calledOnce(appsec.disable) + }) + + it('should not do anything when listener is called with apply and malformed data', () => { + listener('apply', {}) + + sinon.assert.notCalled(appsec.enable) + sinon.assert.notCalled(appsec.disable) + }) + + describe('update config origin activation', () => { + const rcConfigAsmEnabling = { asm: { enabled: true } } + const rcConfigAsmDisabling = { asm: { enabled: true } } + + it('should update appsec.enabled when applying asm enabling by RC', () => { + listener('apply', rcConfigAsmEnabling) + + sinon.assert.calledOnce(telemetry.updateConfig) + assertObjectContains(telemetry.updateConfig.firstCall.args, [[{ + name: 'appsec.enabled', + origin: 'remote_config', + value: rcConfigAsmEnabling.asm.enabled + }]]) + }) + + it('should update appsec.enabled when modifying asm enabling by RC', () => { + listener('modify', rcConfigAsmDisabling) + + sinon.assert.calledOnce(telemetry.updateConfig) + assertObjectContains(telemetry.updateConfig.firstCall.args, [[{ + name: 'appsec.enabled', + origin: 'remote_config', + value: rcConfigAsmDisabling.asm.enabled + }]]) + }) + + it('should update when unapplying asm enabling by RC', () => { + listener('unapply', { asm: { enabled: true } }) + + sinon.assert.calledOnce(telemetry.updateConfig) + assertObjectContains(telemetry.updateConfig.firstCall.args, [[{ + name: 'appsec.enabled', + origin: 'default', + value: config.appsec.enabled + }]]) + }) + }) + + describe('auto_user_instrum', () => { + const rcConfig = { auto_user_instrum: { mode: 'anonymous' } } + const configId = 'collectionModeId' + + afterEach(() => { + listener('unapply', rcConfig, configId) + }) + + it('should not update collection mode when not a string', () => { + listener('apply', { auto_user_instrum: { mode: 123 } }, configId) + + sinon.assert.notCalled(UserTracking.setCollectionMode) + }) + + it('should throw when called two times with different config ids', () => { + listener('apply', rcConfig, configId) + + assert.throws(() => listener('apply', rcConfig, 'anotherId')) + sinon.assert.calledOnceWithExactly(log.error, + '[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config' + ) + }) + + it('should update collection mode when called with apply', () => { + listener('apply', rcConfig, configId) + + sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, rcConfig.auto_user_instrum.mode) + }) + + it('should update collection mode when called with modify', () => { + listener('modify', rcConfig, configId) + + sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, rcConfig.auto_user_instrum.mode) + }) + + it('should revert collection mode when called with unapply', () => { + listener('apply', rcConfig, configId) + UserTracking.setCollectionMode.resetHistory() + + listener('unapply', rcConfig, configId) + + sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, config.appsec.eventTracking.mode) + }) + + it('should not revert collection mode when called with unapply and unknown id', () => { + listener('apply', rcConfig, configId) + UserTracking.setCollectionMode.resetHistory() + + listener('unapply', rcConfig, 'unknownId') + + sinon.assert.notCalled(UserTracking.setCollectionMode) + }) + }) + }) + }) + + describe('enableWafUpdate', () => { + const expectCapabilitiesCalledWith = (capabilityList, expectedValue) => { + capabilityList.forEach(capability => { + sinon.assert.calledWithExactly(rc.updateCapabilities, capability, expectedValue) + }) + } + + const expectCapabilitiesNotCalled = (capabilityList) => { + capabilityList.forEach(capability => { + sinon.assert.neverCalledWith(rc.updateCapabilities, capability) + }) + } + + const CORE_ASM_CAPABILITIES = [ + RemoteConfigCapabilities.ASM_IP_BLOCKING, + RemoteConfigCapabilities.ASM_USER_BLOCKING, + RemoteConfigCapabilities.ASM_DD_RULES, + RemoteConfigCapabilities.ASM_EXCLUSIONS, + RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, + RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, + RemoteConfigCapabilities.ASM_CUSTOM_RULES, + RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, + RemoteConfigCapabilities.ASM_TRUSTED_IPS, + RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, + RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, + RemoteConfigCapabilities.ASM_EXCLUSION_DATA, + RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, + RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, + RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, + RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, + RemoteConfigCapabilities.ASM_DD_MULTICONFIG, + RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES + ] + + const RASP_CAPABILITIES = [ + RemoteConfigCapabilities.ASM_RASP_SSRF, + RemoteConfigCapabilities.ASM_RASP_SQLI, + RemoteConfigCapabilities.ASM_RASP_LFI, + RemoteConfigCapabilities.ASM_RASP_SHI, + RemoteConfigCapabilities.ASM_RASP_CMDI + ] + + const ALL_ASM_CAPABILITIES = [...CORE_ASM_CAPABILITIES, ...RASP_CAPABILITIES] + + describe('enable', () => { + it('should not fail if remote config is not configured before', () => { + config.appsec = {} + appsecRemoteConfig.enableWafUpdate(config.appsec) + + sinon.assert.notCalled(rc.updateCapabilities) + sinon.assert.notCalled(rc.setProductHandler) + }) + + it('should not enable when custom appsec rules are provided', () => { + config.appsec = { enabled: true, rules: {} } + appsecRemoteConfig.enable(rc, config, appsec) + appsecRemoteConfig.enableWafUpdate(config.appsec) + + sinon.assert.neverCalledWith(rc.updateCapabilities, 'ASM_ACTIVATION') + sinon.assert.called(rc.setProductHandler) + }) + + it('should enable when using default rules', () => { + config.appsec = { enabled: true, rules: null, rasp: { enabled: true } } + appsecRemoteConfig.enable(rc, config, appsec) + appsecRemoteConfig.enableWafUpdate(config.appsec) + + expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, true) + + sinon.assert.calledWith(rc.subscribeProducts, 'ASM', 'ASM_DD', 'ASM_DATA') + sinon.assert.calledWithExactly(rc.setBatchHandler, ['ASM', 'ASM_DD', 'ASM_DATA'], RuleManager.updateWafFromRC) + }) + + it('should activate if appsec is manually enabled', () => { + config.appsec = { enabled: true, rasp: { enabled: true } } + appsecRemoteConfig.enable(rc, config, appsec) + appsecRemoteConfig.enableWafUpdate(config.appsec) + + expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, true) + + sinon.assert.calledWith(rc.subscribeProducts, 'ASM', 'ASM_DD', 'ASM_DATA') + sinon.assert.calledWithExactly(rc.setBatchHandler, ['ASM', 'ASM_DD', 'ASM_DATA'], RuleManager.updateWafFromRC) + }) + + it('should activate if appsec enabled is not defined', () => { + config.appsec = { rasp: { enabled: true } } + appsecRemoteConfig.enable(rc, config, appsec) + appsecRemoteConfig.enableWafUpdate(config.appsec) + + expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, true) + }) + + it('should not activate rasp capabilities if rasp is disabled', () => { + config.appsec = { rasp: { enabled: false } } + appsecRemoteConfig.enable(rc, config, appsec) + appsecRemoteConfig.enableWafUpdate(config.appsec) + + expectCapabilitiesCalledWith(CORE_ASM_CAPABILITIES, true) + expectCapabilitiesNotCalled(RASP_CAPABILITIES) + }) + }) + + describe('disable', () => { + it('should update capabilities and unsubscribe listener', () => { + appsecRemoteConfig.enable(rc, config, appsec) + rc.updateCapabilities.resetHistory() + appsecRemoteConfig.disableWafUpdate() + + expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, false) + + sinon.assert.calledWith(rc.unsubscribeProducts, 'ASM', 'ASM_DD', 'ASM_DATA') + sinon.assert.calledWithExactly(rc.removeBatchHandler, RuleManager.updateWafFromRC) + }) + + it('should not fail when called without rc being configured', () => { + // Call disableWafUpdate without calling enable first + appsecRemoteConfig.disableWafUpdate() + + sinon.assert.notCalled(rc.updateCapabilities) + sinon.assert.notCalled(rc.unsubscribeProducts) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 8ebe06ad9e6..7b758264105 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -3,7 +3,6 @@ const assert = require('node:assert/strict') const zlib = require('node:zlib') -const { expect } = require('chai') const dc = require('dc-polyfill') const { after, afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') @@ -342,28 +341,28 @@ describe('reporter', () => { Reporter.reportWafConfigUpdate(product, rcConfigId, diagnostics) sinon.assert.calledThrice(telemetryLogHandlerAssert) - expect(telemetryLogHandlerAssert.getCall(0)).to.have.been.calledWithExactly({ + assert.strictEqual(telemetryLogHandlerAssert.getCall(0).calledWithExactly({ message: '"missing key operator": ["blk-001-001"]', level: 'ERROR', tags: 'log_type:rc::asm_dd::diagnostic,appsec_config_key:rules,rc_config_id:1' - }, 'datadog:telemetry:log') - expect(telemetryLogHandlerAssert.getCall(1)).to.have.been.calledWithExactly({ + }, 'datadog:telemetry:log'), true) + assert.strictEqual(telemetryLogHandlerAssert.getCall(1).calledWithExactly({ message: '"invalid tag": ["blk-001-001"]', level: 'WARN', tags: 'log_type:rc::asm_dd::diagnostic,appsec_config_key:rules,rc_config_id:1' - }, 'datadog:telemetry:log') - expect(telemetryLogHandlerAssert.getCall(2)).to.have.been.calledWithExactly({ + }, 'datadog:telemetry:log'), true) + assert.strictEqual(telemetryLogHandlerAssert.getCall(2).calledWithExactly({ message: '"no mappings defined": ["http-endpoint-fingerprint"]', level: 'ERROR', tags: 'log_type:rc::asm_dd::diagnostic,appsec_config_key:processors,rc_config_id:1' - }, 'datadog:telemetry:log') + }, 'datadog:telemetry:log'), true) }) it('should increment waf.config_errors metric', () => { Reporter.reportWafConfigUpdate(product, rcConfigId, diagnostics, '1.24.1') sinon.assert.calledTwice(telemetry.incrementWafConfigErrorsMetric) - expect(telemetry.incrementWafConfigErrorsMetric).to.always.have.been.calledWithExactly('1.24.1', '1.42.11') + sinon.assert.calledWithExactly(telemetry.incrementWafConfigErrorsMetric, '1.24.1', '1.42.11') }) }) @@ -718,7 +717,7 @@ describe('reporter', () => { it('should call addTags with an empty array', () => { Reporter.reportAttributes([]) - expect(span.addTags).to.be.calledOnceWithExactly({}) + sinon.assert.calledOnceWithExactly(span.addTags, {}) }) it('should call addTags', () => { @@ -740,7 +739,7 @@ describe('reporter', () => { Reporter.reportAttributes(attributes) const schemaEncoded = zlib.gzipSync(JSON.stringify(schemaValue)).toString('base64') - expect(span.addTags).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(span.addTags, { '_dd.appsec.fp.http.endpoint': 'endpoint_fingerprint', '_dd.appsec.fp.http.header': 'header_fingerprint', '_dd.appsec.fp.http.network': 'network_fingerprint', @@ -1014,7 +1013,7 @@ describe('reporter', () => { const res = {} Reporter.finishRequest(req, res) - expect(telemetry.incrementWafRequestsMetric).to.be.calledOnceWithExactly(req) + sinon.assert.calledOnceWithExactly(telemetry.incrementWafRequestsMetric, req) }) it('should set waf.duration tags if there are metrics stored', () => { diff --git a/packages/dd-trace/test/appsec/rule_manager.spec.js b/packages/dd-trace/test/appsec/rule_manager.spec.js index 871e5fc8e3d..cc60cbf99dd 100644 --- a/packages/dd-trace/test/appsec/rule_manager.spec.js +++ b/packages/dd-trace/test/appsec/rule_manager.spec.js @@ -11,7 +11,7 @@ const blocking = require('../../src/appsec/blocking') const rules = require('../../src/appsec/recommended.json') const { loadRules, clearAllRules } = require('../../src/appsec/rule_manager') const waf = require('../../src/appsec/waf') -const { ACKNOWLEDGED, UNACKNOWLEDGED, ERROR } = require('../../src/remote_config/apply_states') +const { UNACKNOWLEDGED } = require('../../src/remote_config/apply_states') const { getConfigFresh } = require('../helpers/config') describe('AppSec Rule Manager', () => { @@ -75,6 +75,14 @@ describe('AppSec Rule Manager', () => { }) describe('updateWafFromRC', () => { + function createTransaction (changes) { + return { + ...changes, + ack: sinon.spy(), + error: sinon.spy() + } + } + function getRcConfigs () { return { toUnapply: [{ @@ -170,7 +178,7 @@ describe('AppSec Rule Manager', () => { }) it('should not apply configs from non ASM products', () => { - const rcConfigsForNonAsmProducts = { + const changes = { toUnapply: [{ id: 'test.toUnapply', product: 'NON_ASM_PRODUCT', @@ -194,14 +202,11 @@ describe('AppSec Rule Manager', () => { }] } - RuleManager.updateWafFromRC(rcConfigsForNonAsmProducts) + const transaction = createTransaction(changes) + RuleManager.updateWafFromRC(transaction) - assert.strictEqual(rcConfigsForNonAsmProducts.toUnapply[0].apply_state, UNACKNOWLEDGED) - assert.ok(!('apply_error' in rcConfigsForNonAsmProducts.toUnapply[0])) - assert.strictEqual(rcConfigsForNonAsmProducts.toModify[0].apply_state, UNACKNOWLEDGED) - assert.ok(!('apply_error' in rcConfigsForNonAsmProducts.toModify[0])) - assert.strictEqual(rcConfigsForNonAsmProducts.toApply[0].apply_state, UNACKNOWLEDGED) - assert.ok(!('apply_error' in rcConfigsForNonAsmProducts.toApply[0])) + sinon.assert.notCalled(transaction.ack) + sinon.assert.notCalled(transaction.error) sinon.assert.notCalled(waf.updateConfig) sinon.assert.notCalled(waf.removeConfig) @@ -213,62 +218,70 @@ describe('AppSec Rule Manager', () => { waf.updateConfig.callThrough() waf.removeConfig.callThrough() - const rcConfigs = getRcConfigs() + const changes = getRcConfigs() + const transaction = createTransaction(changes) - RuleManager.updateWafFromRC(rcConfigs) + RuleManager.updateWafFromRC(transaction) - sinon.assert.calledOnceWithExactly(waf.removeConfig, rcConfigs.toUnapply[0].path) + sinon.assert.calledOnceWithExactly(waf.removeConfig, changes.toUnapply[0].path) sinon.assert.calledTwice(waf.updateConfig) sinon.assert.calledWith( waf.updateConfig, - rcConfigs.toApply[0].product, - rcConfigs.toApply[0].id, - rcConfigs.toApply[0].path, - rcConfigs.toApply[0].file + changes.toApply[0].product, + changes.toApply[0].id, + changes.toApply[0].path, + changes.toApply[0].file ) sinon.assert.calledWith( waf.updateConfig, - rcConfigs.toModify[0].product, - rcConfigs.toModify[0].id, - rcConfigs.toModify[0].path, - rcConfigs.toModify[0].file + changes.toModify[0].product, + changes.toModify[0].id, + changes.toModify[0].path, + changes.toModify[0].file ) assert.strictEqual(waf.wafManager.ddwaf.configPaths.length, 3) - assert.ok(waf.wafManager.ddwaf.configPaths.includes(waf.wafManager.constructor.defaultWafConfigPath)) - assert.ok(waf.wafManager.ddwaf.configPaths.includes(rcConfigs.toApply[0].path)) - assert.ok(waf.wafManager.ddwaf.configPaths.includes(rcConfigs.toModify[0].path)) + assert.deepStrictEqual(waf.wafManager.ddwaf.configPaths.sort(), [ + waf.wafManager.constructor.defaultWafConfigPath, + changes.toApply[0].path, + changes.toModify[0].path + ].sort()) + + // Should ack for each ASM product config. + sinon.assert.calledWithExactly(transaction.ack, changes.toUnapply[0].path) + sinon.assert.calledWithExactly(transaction.ack, changes.toApply[0].path) + sinon.assert.calledWithExactly(transaction.ack, changes.toModify[0].path) + sinon.assert.notCalled(transaction.error) }) - it('should update apply_state and apply_error on successful apply', () => { + it('should ack on successful apply', () => { waf.updateConfig.callThrough() waf.removeConfig.callThrough() - const rcConfigs = getRcConfigs() + const changes = getRcConfigs() + const transaction = createTransaction(changes) - RuleManager.updateWafFromRC(rcConfigs) + RuleManager.updateWafFromRC(transaction) - assert.strictEqual(rcConfigs.toUnapply[0].apply_state, ACKNOWLEDGED) - assert.ok(!('apply_error' in rcConfigs.toUnapply[0])) - assert.strictEqual(rcConfigs.toModify[0].apply_state, ACKNOWLEDGED) - assert.ok(!('apply_error' in rcConfigs.toModify[0])) - assert.strictEqual(rcConfigs.toApply[0].apply_state, ACKNOWLEDGED) - assert.ok(!('apply_error' in rcConfigs.toApply[0])) + sinon.assert.calledWithExactly(transaction.ack, changes.toUnapply[0].path) + sinon.assert.calledWithExactly(transaction.ack, changes.toApply[0].path) + sinon.assert.calledWithExactly(transaction.ack, changes.toModify[0].path) + sinon.assert.notCalled(transaction.error) }) - it('should update apply_state and apply_error on failed config remove', () => { + it('should call transaction.error on failed config remove', () => { const removeConfigError = new Error('Error remove config') waf.removeConfig.throws(removeConfigError) - const { toUnapply } = getRcConfigs() + const changes = { toUnapply: getRcConfigs().toUnapply, toApply: [], toModify: [] } + const transaction = createTransaction(changes) - RuleManager.updateWafFromRC({ toUnapply, toApply: [], toModify: [] }) + RuleManager.updateWafFromRC(transaction) - assert.strictEqual(toUnapply[0].apply_state, ERROR) - assert.strictEqual(toUnapply[0].apply_error, removeConfigError.toString()) + sinon.assert.calledWithMatch(transaction.error, changes.toUnapply[0].path, removeConfigError) }) - it('should update apply_state and apply_error on failed config update', () => { + it('should call transaction.error on failed config update', () => { const diagnostics = { rules: { loaded: [], @@ -320,19 +333,19 @@ describe('AppSec Rule Manager', () => { waf.updateConfig.throws(new waf.WafUpdateError(diagnostics)) const { toModify, toApply } = getRcConfigs() + const changes = { toUnapply: [], toApply, toModify } + const transaction = createTransaction(changes) - RuleManager.updateWafFromRC({ toUnapply: [], toApply, toModify }) + RuleManager.updateWafFromRC(transaction) - assert.strictEqual(toApply[0].apply_state, ERROR) - assert.strictEqual(toApply[0].apply_error, JSON.stringify(expectedApplyError)) - assert.strictEqual(toModify[0].apply_state, ERROR) - assert.strictEqual(toModify[0].apply_error, JSON.stringify(expectedApplyError)) + sinon.assert.calledWithExactly(transaction.error, toApply[0].path, JSON.stringify(expectedApplyError)) + sinon.assert.calledWithExactly(transaction.error, toModify[0].path, JSON.stringify(expectedApplyError)) }) it('should report successful waf update', () => { - const rcConfigs = getRcConfigs() + const transaction = createTransaction(getRcConfigs()) - RuleManager.updateWafFromRC(rcConfigs) + RuleManager.updateWafFromRC(transaction) sinon.assert.calledOnceWithExactly( reportWafUpdate, @@ -345,9 +358,9 @@ describe('AppSec Rule Manager', () => { it('should report failed waf update', () => { waf.updateConfig.throws(new waf.WafUpdateError({ error: 'Update failed' })) - const rcConfigs = getRcConfigs() + const transaction = createTransaction(getRcConfigs()) - RuleManager.updateWafFromRC(rcConfigs) + RuleManager.updateWafFromRC(transaction) sinon.assert.calledOnceWithExactly( reportWafUpdate, @@ -362,7 +375,7 @@ describe('AppSec Rule Manager', () => { waf.updateConfig.callThrough() waf.removeConfig.callThrough() - const rcConfigs = { + const changes = { toApply: [ { id: 'asm_dd.test.failed', @@ -375,9 +388,10 @@ describe('AppSec Rule Manager', () => { toUnapply: [] } - RuleManager.updateWafFromRC(rcConfigs) + const transaction = createTransaction(changes) + RuleManager.updateWafFromRC(transaction) - assert.strictEqual(rcConfigs.toApply[0].apply_state, ERROR) + sinon.assert.called(transaction.error) assert.deepStrictEqual(waf.wafManager.ddwaf.configPaths, [waf.wafManager.constructor.defaultWafConfigPath]) }) @@ -420,7 +434,7 @@ describe('AppSec Rule Manager', () => { } ] - RuleManager.updateWafFromRC({ toUnapply: [], toApply, toModify: [] }) + RuleManager.updateWafFromRC(createTransaction({ toUnapply: [], toApply, toModify: [] })) const expectedActions = [ { @@ -465,7 +479,7 @@ describe('AppSec Rule Manager', () => { } ] - RuleManager.updateWafFromRC({ toUnapply: [], toApply, toModify: [] }) + RuleManager.updateWafFromRC(createTransaction({ toUnapply: [], toApply, toModify: [] })) sinon.assert.calledOnceWithExactly(setDefaultBlockingActionParameters, asm.actions) sinon.resetHistory() @@ -477,7 +491,7 @@ describe('AppSec Rule Manager', () => { } ] - RuleManager.updateWafFromRC({ toUnapply, toApply: [], toModify: [] }) + RuleManager.updateWafFromRC(createTransaction({ toUnapply, toApply: [], toModify: [] })) sinon.assert.calledOnceWithExactly(setDefaultBlockingActionParameters, []) }) diff --git a/packages/dd-trace/test/appsec/sdk/set_user.spec.js b/packages/dd-trace/test/appsec/sdk/set_user.spec.js index c2d30631577..394a574282c 100644 --- a/packages/dd-trace/test/appsec/sdk/set_user.spec.js +++ b/packages/dd-trace/test/appsec/sdk/set_user.spec.js @@ -4,7 +4,7 @@ const assert = require('node:assert/strict') const path = require('node:path') const axios = require('axios') -const { expect } = require('chai') + const { after, before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -63,7 +63,7 @@ describe('set_user', () => { getRootSpan.returns(undefined) setUser(tracer, { id: 'user' }) - expect(getRootSpan).to.be.calledOnceWithExactly(tracer) + sinon.assert.calledOnceWithExactly(getRootSpan, tracer) sinon.assert.calledOnceWithExactly(log.warn, '[ASM] Root span not available in setUser') sinon.assert.notCalled(rootSpan.setTag) sinon.assert.notCalled(waf.run) @@ -80,11 +80,11 @@ describe('set_user', () => { setUser(tracer, user) sinon.assert.notCalled(log.warn) assert.strictEqual(rootSpan.setTag.callCount, 5) - expect(rootSpan.setTag.getCall(0)).to.have.been.calledWithExactly('usr.id', '123') - expect(rootSpan.setTag.getCall(1)).to.have.been.calledWithExactly('usr.email', 'a@b.c') - expect(rootSpan.setTag.getCall(2)).to.have.been.calledWithExactly('usr.custom', 'hello') - expect(rootSpan.setTag.getCall(3)).to.have.been.calledWithExactly('usr.session_id', '133769') - expect(rootSpan.setTag.getCall(4)).to.have.been.calledWithExactly('_dd.appsec.user.collection_mode', 'sdk') + assert.strictEqual(rootSpan.setTag.getCall(0).calledWithExactly('usr.id', '123'), true) + assert.strictEqual(rootSpan.setTag.getCall(1).calledWithExactly('usr.email', 'a@b.c'), true) + assert.strictEqual(rootSpan.setTag.getCall(2).calledWithExactly('usr.custom', 'hello'), true) + assert.strictEqual(rootSpan.setTag.getCall(3).calledWithExactly('usr.session_id', '133769'), true) + assert.strictEqual(rootSpan.setTag.getCall(4).calledWithExactly('_dd.appsec.user.collection_mode', 'sdk'), true) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { 'usr.id': '123', diff --git a/packages/dd-trace/test/appsec/sdk/track_event.spec.js b/packages/dd-trace/test/appsec/sdk/track_event.spec.js index 0b7387f6117..e266267d7a2 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -1,6 +1,5 @@ 'use strict' -const { expect } = require('chai') const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -72,10 +71,8 @@ describe('track_event - Internal API', () => { trackUserLoginSuccessEvent(tracer, {}, { key: 'value' }) sinon.assert.calledTwice(log.warn) - expect(log.warn.firstCall) - .to.have.been.calledWithExactly('[ASM] Invalid user provided to trackUserLoginSuccessEvent') - expect(log.warn.secondCall) - .to.have.been.calledWithExactly('[ASM] Invalid user provided to trackUserLoginSuccessEvent') + sinon.assert.calledWithExactly(log.warn.firstCall, '[ASM] Invalid user provided to trackUserLoginSuccessEvent') + sinon.assert.calledWithExactly(log.warn.secondCall, '[ASM] Invalid user provided to trackUserLoginSuccessEvent') sinon.assert.notCalled(setUserTags) sinon.assert.notCalled(rootSpan.addTags) sinon.assert.notCalled(telemetry.incrementSdkEventMetric) @@ -86,8 +83,7 @@ describe('track_event - Internal API', () => { trackUserLoginSuccessEvent(tracer, { id: 'user_id' }, { key: 'value' }) - expect(log.warn) - .to.have.been.calledOnceWithExactly('[ASM] Root span not available in trackUserLoginSuccessEvent') + sinon.assert.calledOnceWithExactly(log.warn, '[ASM] Root span not available in trackUserLoginSuccessEvent') sinon.assert.notCalled(setUserTags) sinon.assert.calledWithExactly(telemetry.incrementSdkEventMetric, 'login_success', 'v1') }) @@ -111,8 +107,7 @@ describe('track_event - Internal API', () => { 'appsec.events.users.login.success.metakey2': 'metaValue2', 'appsec.events.users.login.success.metakey3': 'metaValue3' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { [LOGIN_SUCCESS]: null, @@ -135,8 +130,7 @@ describe('track_event - Internal API', () => { '_dd.appsec.events.users.login.success.sdk': 'true', 'appsec.events.users.login.success.usr.login': 'user_id' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { [LOGIN_SUCCESS]: null, @@ -159,8 +153,7 @@ describe('track_event - Internal API', () => { '_dd.appsec.events.users.login.success.sdk': 'true', 'appsec.events.users.login.success.usr.login': 'user_login' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { [LOGIN_SUCCESS]: null, @@ -178,10 +171,8 @@ describe('track_event - Internal API', () => { trackUserLoginFailureEvent(tracer, [], false, { key: 'value' }) sinon.assert.calledTwice(log.warn) - expect(log.warn.firstCall) - .to.have.been.calledWithExactly('[ASM] Invalid userId provided to trackUserLoginFailureEvent') - expect(log.warn.secondCall) - .to.have.been.calledWithExactly('[ASM] Invalid userId provided to trackUserLoginFailureEvent') + sinon.assert.calledWithExactly(log.warn.firstCall, '[ASM] Invalid userId provided to trackUserLoginFailureEvent') + sinon.assert.calledWithExactly(log.warn.secondCall, '[ASM] Invalid userId provided to trackUserLoginFailureEvent') sinon.assert.notCalled(setUserTags) sinon.assert.notCalled(rootSpan.addTags) sinon.assert.notCalled(telemetry.incrementSdkEventMetric) @@ -192,8 +183,7 @@ describe('track_event - Internal API', () => { trackUserLoginFailureEvent(tracer, 'user_id', false, { key: 'value' }) - expect(log.warn) - .to.have.been.calledOnceWithExactly('[ASM] Root span not available in %s', 'trackUserLoginFailureEvent') + sinon.assert.calledOnceWithExactly(log.warn, '[ASM] Root span not available in %s', 'trackUserLoginFailureEvent') sinon.assert.notCalled(setUserTags) sinon.assert.calledWithExactly(telemetry.incrementSdkEventMetric, 'login_failure', 'v1') }) @@ -217,8 +207,7 @@ describe('track_event - Internal API', () => { 'appsec.events.users.login.failure.metakey2': 'metaValue2', 'appsec.events.users.login.failure.metakey3': 'metaValue3' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { [LOGIN_FAILURE]: null, @@ -247,8 +236,7 @@ describe('track_event - Internal API', () => { 'appsec.events.users.login.failure.metakey2': 'metaValue2', 'appsec.events.users.login.failure.metakey3': 'metaValue3' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { [LOGIN_FAILURE]: null, @@ -270,8 +258,7 @@ describe('track_event - Internal API', () => { 'appsec.events.users.login.failure.usr.login': 'user_id', 'appsec.events.users.login.failure.usr.exists': 'true' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledOnceWithExactly(waf.run, { persistent: { [LOGIN_FAILURE]: null, @@ -288,10 +275,8 @@ describe('track_event - Internal API', () => { trackCustomEvent(tracer, { name: 'name' }) sinon.assert.calledTwice(log.warn) - expect(log.warn.firstCall) - .to.have.been.calledWithExactly('[ASM] Invalid eventName provided to trackCustomEvent') - expect(log.warn.secondCall) - .to.have.been.calledWithExactly('[ASM] Invalid eventName provided to trackCustomEvent') + sinon.assert.calledWithExactly(log.warn.firstCall, '[ASM] Invalid eventName provided to trackCustomEvent') + sinon.assert.calledWithExactly(log.warn.secondCall, '[ASM] Invalid eventName provided to trackCustomEvent') sinon.assert.notCalled(setUserTags) sinon.assert.notCalled(rootSpan.addTags) sinon.assert.notCalled(telemetry.incrementSdkEventMetric) @@ -302,8 +287,7 @@ describe('track_event - Internal API', () => { trackCustomEvent(tracer, 'custom_event') - expect(log.warn) - .to.have.been.calledOnceWithExactly('[ASM] Root span not available in %s', 'trackCustomEvent') + sinon.assert.calledOnceWithExactly(log.warn, '[ASM] Root span not available in %s', 'trackCustomEvent') sinon.assert.notCalled(setUserTags) sinon.assert.calledWithExactly(telemetry.incrementSdkEventMetric, 'custom', 'v1') }) @@ -322,8 +306,7 @@ describe('track_event - Internal API', () => { 'appsec.events.custom_event.metaKey1': 'metaValue1', 'appsec.events.custom_event.metakey2': 'metaValue2' }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.notCalled(waf.run) sinon.assert.calledWithExactly(telemetry.incrementSdkEventMetric, 'custom', 'v1') }) @@ -338,8 +321,7 @@ describe('track_event - Internal API', () => { '_dd.appsec.events.custom_event.sdk': 'true' }) sinon.assert.notCalled(waf.run) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) sinon.assert.calledWithExactly(telemetry.incrementSdkEventMetric, 'custom', 'v1') }) @@ -371,8 +353,10 @@ describe('track_event - Internal API', () => { trackUserLoginSuccessV2(tracer, 'login') - expect(log.warn) - .to.have.been.calledOnceWithExactly('[ASM] Root span not available in eventTrackingV2.trackUserLoginSuccess') + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Root span not available in eventTrackingV2.trackUserLoginSuccess' + ) sinon.assert.notCalled(setUserTags) }) @@ -381,10 +365,14 @@ describe('track_event - Internal API', () => { trackUserLoginSuccessV2(tracer, {}) sinon.assert.calledTwice(log.warn) - expect(log.warn.firstCall) - .to.have.been.calledWithExactly('[ASM] Invalid login provided to eventTrackingV2.trackUserLoginSuccess') - expect(log.warn.secondCall) - .to.have.been.calledWithExactly('[ASM] Invalid login provided to eventTrackingV2.trackUserLoginSuccess') + sinon.assert.calledWithExactly( + log.warn.firstCall, + '[ASM] Invalid login provided to eventTrackingV2.trackUserLoginSuccess' + ) + sinon.assert.calledWithExactly( + log.warn.secondCall, + '[ASM] Invalid login provided to eventTrackingV2.trackUserLoginSuccess' + ) sinon.assert.notCalled(setUserTags) sinon.assert.notCalled(rootSpan.addTags) sinon.assert.notCalled(waf.run) @@ -590,8 +578,7 @@ describe('track_event - Internal API', () => { it('should keep the trace', () => { trackUserLoginSuccessV2(tracer, 'login') - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) }) it('should update the metrics', () => { @@ -607,8 +594,10 @@ describe('track_event - Internal API', () => { trackUserLoginFailureV2(tracer, 'login', false) - expect(log.warn) - .to.have.been.calledOnceWithExactly('[ASM] Root span not available in eventTrackingV2.trackUserLoginFailure') + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Root span not available in eventTrackingV2.trackUserLoginFailure' + ) sinon.assert.notCalled(setUserTags) }) @@ -617,10 +606,14 @@ describe('track_event - Internal API', () => { trackUserLoginFailureV2(tracer, {}, false) sinon.assert.calledTwice(log.warn) - expect(log.warn.firstCall) - .to.have.been.calledWithExactly('[ASM] Invalid login provided to eventTrackingV2.trackUserLoginFailure') - expect(log.warn.secondCall) - .to.have.been.calledWithExactly('[ASM] Invalid login provided to eventTrackingV2.trackUserLoginFailure') + sinon.assert.calledWithExactly( + log.warn.firstCall, + '[ASM] Invalid login provided to eventTrackingV2.trackUserLoginFailure' + ) + sinon.assert.calledWithExactly( + log.warn.secondCall, + '[ASM] Invalid login provided to eventTrackingV2.trackUserLoginFailure' + ) sinon.assert.notCalled(setUserTags) sinon.assert.notCalled(rootSpan.addTags) sinon.assert.notCalled(waf.run) @@ -815,8 +808,7 @@ describe('track_event - Internal API', () => { it('should keep the trace', () => { trackUserLoginFailureV2(tracer, 'login', true) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, ASM) + sinon.assert.calledOnceWithExactly(prioritySampler.setPriority, rootSpan, USER_KEEP, ASM) }) it('should update the metrics', () => { diff --git a/packages/dd-trace/test/appsec/sdk/user_blocking.spec.js b/packages/dd-trace/test/appsec/sdk/user_blocking.spec.js index 5b1ae6e6567..9fb9ffe718b 100644 --- a/packages/dd-trace/test/appsec/sdk/user_blocking.spec.js +++ b/packages/dd-trace/test/appsec/sdk/user_blocking.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { before, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -114,7 +113,7 @@ describe('user_blocking - Internal API', () => { const ret = userBlocking.blockRequest(tracer) assert.strictEqual(ret, true) sinon.assert.calledOnce(legacyStorage.getStore) - expect(block).to.be.calledOnceWithExactly(req, res, rootSpan) + sinon.assert.calledOnceWithExactly(block, req, res, rootSpan) }) it('should log warning when req or res is not available', () => { @@ -123,8 +122,7 @@ describe('user_blocking - Internal API', () => { const ret = userBlocking.blockRequest(tracer) assert.strictEqual(ret, false) sinon.assert.calledOnce(legacyStorage.getStore) - expect(log.warn) - .to.have.been.calledOnceWithExactly('[ASM] Requests or response object not available in blockRequest') + sinon.assert.calledOnceWithExactly(log.warn, '[ASM] Requests or response object not available in blockRequest') sinon.assert.notCalled(block) }) diff --git a/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js b/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js index c6b086256e4..9eb03cc69b7 100644 --- a/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js +++ b/packages/dd-trace/test/appsec/waf-metrics.integration.spec.js @@ -2,9 +2,9 @@ const assert = require('node:assert/strict') -const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../integration-tests/helpers') const path = require('path') const Axios = require('axios') +const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../../../../integration-tests/helpers') describe('WAF Metrics', () => { let axios, cwd, appFile @@ -29,8 +29,8 @@ describe('WAF Metrics', () => { env: { DD_TRACE_AGENT_PORT: agent.port, DD_APPSEC_ENABLED: 'true', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - DD_APPSEC_WAF_TIMEOUT: 0.1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + DD_APPSEC_WAF_TIMEOUT: '0.1' } }) axios = Axios.create({ baseURL: proc.url }) @@ -63,13 +63,13 @@ describe('WAF Metrics', () => { const series = payload.payload.series const wafRequests = series.find(s => s.metric === 'waf.requests') - assert.ok(wafRequests != null) + assert.ok(wafRequests) assert.strictEqual(wafRequests.type, 'count') assert.ok(wafRequests.tags.includes('waf_error:true')) assert.ok(wafRequests.tags.includes('rate_limited:false')) const wafError = series.find(s => s.metric === 'waf.error') - assert.ok(wafError != null) + assert.ok(wafError) assert.strictEqual(wafError.type, 'count') assert.ok(wafError.tags.includes('waf_error:-127')) } @@ -91,8 +91,8 @@ describe('WAF Metrics', () => { env: { DD_TRACE_AGENT_PORT: agent.port, DD_APPSEC_ENABLED: 'true', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1, - DD_APPSEC_WAF_TIMEOUT: 1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1', + DD_APPSEC_WAF_TIMEOUT: '1' } }) axios = Axios.create({ baseURL: proc.url }) @@ -122,7 +122,7 @@ describe('WAF Metrics', () => { const series = payload.payload.series const wafRequests = series.find(s => s.metric === 'waf.requests') - assert.ok(wafRequests != null) + assert.ok(wafRequests) assert.strictEqual(wafRequests.type, 'count') assert.ok(wafRequests.tags.includes('waf_timeout:true')) } @@ -144,7 +144,7 @@ describe('WAF Metrics', () => { env: { DD_TRACE_AGENT_PORT: agent.port, DD_APPSEC_ENABLED: 'true', - DD_TELEMETRY_HEARTBEAT_INTERVAL: 1 + DD_TELEMETRY_HEARTBEAT_INTERVAL: '1' } }) axios = Axios.create({ baseURL: proc.url }) @@ -176,12 +176,12 @@ describe('WAF Metrics', () => { const series = payload.payload.series const inputTruncated = series.find(s => s.metric === 'waf.input_truncated') - assert.ok(inputTruncated != null) + assert.ok(inputTruncated) assert.strictEqual(inputTruncated.type, 'count') assert.ok(inputTruncated.tags.includes('truncation_reason:7')) const wafRequests = series.find(s => s.metric === 'waf.requests') - assert.ok(wafRequests != null) + assert.ok(wafRequests) assert.ok(wafRequests.tags.includes('input_truncated:true')) } }, 'generate-metrics', 30_000, 2) diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index ed6629e6674..f585f1c817d 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -1,8 +1,7 @@ 'use strict' -const assert = require('node:assert') +const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -107,7 +106,7 @@ describe('WAF Manager', () => { try { waf.init(rules, config.appsec) - expect.fail('waf init should have thrown an error') + assert.fail('waf init should have thrown an error') } catch (err) { assert.strictEqual(err, error) sinon.assert.calledWith(Reporter.reportWafInit, '1.2.3', 'unknown') @@ -131,7 +130,7 @@ describe('WAF Manager', () => { const req = {} waf.run(payload, req, 'ssrf') - expect(run).to.be.calledOnceWithExactly(payload, 'ssrf') + sinon.assert.calledOnceWithExactly(run, payload, 'ssrf') }) it('should call wafManager.run without raspRuleType', () => { @@ -143,7 +142,7 @@ describe('WAF Manager', () => { const req = {} waf.run(payload, req) - expect(run).to.be.calledOnceWithExactly(payload, undefined) + sinon.assert.calledOnceWithExactly(run, payload, undefined) }) describe('sampling priority', () => { @@ -436,7 +435,7 @@ describe('WAF Manager', () => { } }) - expect(ddwafContext.run).to.be.calledOnceWithExactly({ + sinon.assert.calledOnceWithExactly(ddwafContext.run, { persistent: { 'server.request.headers.no_cookies': { header: 'value' }, 'server.request.uri.raw': 'https://testurl', @@ -461,7 +460,7 @@ describe('WAF Manager', () => { wafContextWrapper.run(params) - expect(Reporter.reportAttack).to.be.calledOnceWith(match({ events: ['ATTACK DATA'] })) + sinon.assert.calledOnceWithExactly(Reporter.reportAttack, match({ events: ['ATTACK DATA'] })) }) it('should report if rule is triggered', () => { @@ -585,7 +584,7 @@ describe('WAF Manager', () => { ddwafContext.run.returns(result) wafContextWrapper.run(params) - expect(Reporter.reportAttributes).to.be.calledOnceWithExactly(result.attributes) + sinon.assert.calledOnceWithExactly(Reporter.reportAttributes, result.attributes) }) it('should report fingerprints when ddwafContext returns fingerprints in results attributes', () => { diff --git a/packages/dd-trace/test/appsec/waf/waf_context_wrapper.spec.js b/packages/dd-trace/test/appsec/waf/waf_context_wrapper.spec.js index e09022c74ef..962703aa3f6 100644 --- a/packages/dd-trace/test/appsec/waf/waf_context_wrapper.spec.js +++ b/packages/dd-trace/test/appsec/waf/waf_context_wrapper.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -66,7 +65,7 @@ describe('WAFContextWrapper', () => { wafContextWrapper.run(payload) sinon.assert.calledTwice(ddwafContext.run) - expect(ddwafContext.run).to.always.have.been.calledWithExactly(payload, 1000) + sinon.assert.calledWithExactly(ddwafContext.run, payload, 1000) const firstCall = Reporter.reportMetrics.getCall(0).args[0] assert.strictEqual(firstCall.errorCode, -127) @@ -147,7 +146,7 @@ describe('WAFContextWrapper', () => { wafContextWrapper.run(payload) wafRunFinished.unsubscribe(finishedCallback) - expect(finishedCallback).to.be.calledOnceWith({ payload }) + sinon.assert.calledOnceWithMatch(finishedCallback, { payload }) }) it('should report error code when the waf run fails', () => { diff --git a/packages/dd-trace/test/azure_metadata.spec.js b/packages/dd-trace/test/azure_metadata.spec.js index c26b6846ac7..4d85ceafe93 100644 --- a/packages/dd-trace/test/azure_metadata.spec.js +++ b/packages/dd-trace/test/azure_metadata.spec.js @@ -1,14 +1,42 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha const os = require('node:os') -require('./setup/core') +const { describe, it, afterEach } = require('mocha') -const { getAzureAppMetadata, getAzureTagsFromMetadata } = require('../src/azure_metadata') +require('./setup/core') +const { getAzureAppMetadata, getAzureTagsFromMetadata, getAzureFunctionMetadata } = require('../src/azure_metadata') describe('Azure metadata', () => { + const AZURE_ENV_KEYS = [ + 'COMPUTERNAME', + 'DD_AAS_DOTNET_EXTENSION_VERSION', + 'DD_AZURE_RESOURCE_GROUP', + 'FUNCTIONS_EXTENSION_VERSION', + 'FUNCTIONS_WORKER_RUNTIME', + 'FUNCTIONS_WORKER_RUNTIME_VERSION', + 'WEBSITE_INSTANCE_ID', + 'WEBSITE_OWNER_NAME', + 'WEBSITE_OS', + 'WEBSITE_RESOURCE_GROUP', + 'WEBSITE_SITE_NAME', + 'WEBSITE_SKU' + ] + + const initialAzureEnv = Object.fromEntries(AZURE_ENV_KEYS.map(key => [key, process.env[key]])) + + afterEach(() => { + for (const key of AZURE_ENV_KEYS) { + const value = initialAzureEnv[key] + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + }) + describe('for apps is', () => { it('not provided without WEBSITE_SITE_NAME', () => { delete process.env.WEBSITE_SITE_NAME @@ -25,11 +53,6 @@ describe('Azure metadata', () => { }) it('provided completely with minimum vars', () => { - delete process.env.WEBSITE_RESOURCE_GROUP - delete process.env.WEBSITE_OS - delete process.env.FUNCTIONS_EXTENSION_VERSION - delete process.env.FUNCTIONS_WORKER_RUNTIME - delete process.env.FUNCTIONS_WORKER_RUNTIME_VERSION process.env.COMPUTERNAME = 'boaty_mcboatface' process.env.WEBSITE_SITE_NAME = 'website_name' process.env.WEBSITE_OWNER_NAME = 'subscription_id+resource_group-regionwebspace' @@ -82,11 +105,6 @@ describe('Azure metadata', () => { }) it('tags are correctly generated from vars', () => { - delete process.env.WEBSITE_RESOURCE_GROUP - delete process.env.WEBSITE_OS - delete process.env.FUNCTIONS_EXTENSION_VERSION - delete process.env.FUNCTIONS_WORKER_RUNTIME - delete process.env.FUNCTIONS_WORKER_RUNTIME_VERSION process.env.COMPUTERNAME = 'boaty_mcboatface' process.env.WEBSITE_SITE_NAME = 'website_name' process.env.WEBSITE_OWNER_NAME = 'subscription_id+resource_group-regionwebspace' @@ -107,4 +125,43 @@ describe('Azure metadata', () => { } assert.deepStrictEqual(getAzureTagsFromMetadata(getAzureAppMetadata()), expected) }) + + it('uses DD_AZURE_RESOURCE_GROUP for Flex Consumption Azure Functions', () => { + process.env.COMPUTERNAME = 'flex_function' + process.env.WEBSITE_SITE_NAME = 'flex_function_app' + process.env.WEBSITE_OWNER_NAME = 'subscription_id+flex-regionwebspace' + process.env.WEBSITE_INSTANCE_ID = 'instance_id' + process.env.WEBSITE_SKU = 'FlexConsumption' + process.env.FUNCTIONS_EXTENSION_VERSION = '4' + process.env.FUNCTIONS_WORKER_RUNTIME = 'node' + process.env.DD_AZURE_RESOURCE_GROUP = 'flex_resource_group' + const expected = { + functionRuntimeVersion: '4', + instanceID: 'instance_id', + instanceName: 'flex_function', + operatingSystem: os.platform(), + resourceGroup: 'flex_resource_group', + resourceID: + '/subscriptions/subscription_id/resourcegroups/flex_resource_group' + + '/providers/microsoft.web/sites/flex_function_app', + runtime: 'node', + siteKind: 'functionapp', + siteName: 'flex_function_app', + siteType: 'function', + subscriptionID: 'subscription_id' + } + assert.deepStrictEqual(getAzureFunctionMetadata(), expected) + }) + + it('uses WEBSITE_RESOURCE_GROUP for non-Flex Consumption plans', () => { + process.env.WEBSITE_SITE_NAME = 'regular_function_app' + process.env.WEBSITE_RESOURCE_GROUP = 'regular_resource_group' + process.env.WEBSITE_OWNER_NAME = 'subscription_id+extracted_group-regionwebspace' + process.env.WEBSITE_SKU = 'Consumption' + process.env.FUNCTIONS_EXTENSION_VERSION = '4' + process.env.FUNCTIONS_WORKER_RUNTIME = 'node' + process.env.DD_AZURE_RESOURCE_GROUP = 'should_not_use_this' + const metadata = getAzureFunctionMetadata() + assert.strictEqual(metadata.resourceGroup, 'regular_resource_group') + }) }) diff --git a/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/dynamic-instrumentation.spec.js b/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/dynamic-instrumentation.spec.js index 5b142146dd7..62cd9c05337 100644 --- a/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/dynamic-instrumentation.spec.js +++ b/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/dynamic-instrumentation.spec.js @@ -1,10 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, afterEach } = require('tap').mocha const { fork } = require('node:child_process') const path = require('node:path') +const { describe, it, afterEach } = require('mocha') + require('../../../../dd-trace/test/setup/core') describe('test visibility with dynamic instrumentation', () => { @@ -24,9 +25,9 @@ describe('test visibility with dynamic instrumentation', () => { childProcess = fork(path.join(__dirname, 'target-app', 'test-visibility-dynamic-instrumentation-script.js')) childProcess.on('message', ({ snapshot: { language, stack, probe, captures }, probeId }) => { - assert.ok(probeId != null) - assert.ok(probe != null) - assert.ok(stack != null) + assert.ok(probeId) + assert.ok(probe) + assert.ok(stack) assert.strictEqual(language, 'javascript') assert.deepStrictEqual(captures, { diff --git a/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js b/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js index c856c865c78..6a2815ddf36 100644 --- a/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js +++ b/packages/dd-trace/test/ci-visibility/dynamic-instrumentation/target-app/test-visibility-dynamic-instrumentation-script.js @@ -2,8 +2,8 @@ const path = require('path') const getDiClient = require('../../../../src/ci-visibility/dynamic-instrumentation') -const sum = require('./di-dependency') const getConfig = require('../../../../src/config') +const sum = require('./di-dependency') // keep process alive const intervalId = setInterval(() => {}, 5000) diff --git a/packages/dd-trace/test/ci-visibility/encode/json-encoder.spec.js b/packages/dd-trace/test/ci-visibility/encode/json-encoder.spec.js index 9a75e056d51..49bef278f59 100644 --- a/packages/dd-trace/test/ci-visibility/encode/json-encoder.spec.js +++ b/packages/dd-trace/test/ci-visibility/encode/json-encoder.spec.js @@ -1,13 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../../../dd-trace/test/setup/core') - const { JSONEncoder } = require('../../../src/ci-visibility/encode/json-encoder') describe('CI Visibility JSON encoder', () => { diff --git a/packages/dd-trace/test/ci-visibility/exporters/agent-proxy/agent-proxy.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agent-proxy/agent-proxy.spec.js index 417ef63c1b0..3c3ff6492bd 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agent-proxy/agent-proxy.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agent-proxy/agent-proxy.spec.js @@ -2,15 +2,13 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { assertObjectContains } = require('../../../../../../integration-tests/helpers') - -const { describe, it, beforeEach, context } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') +const context = describe const sinon = require('sinon') const nock = require('nock') +const { assertObjectContains } = require('../../../../../../integration-tests/helpers') require('../../../../../dd-trace/test/setup/core') - const AgentProxyCiVisibilityExporter = require('../../../../src/ci-visibility/exporters/agent-proxy') const AgentlessWriter = require('../../../../src/ci-visibility/exporters/agentless/writer') const DynamicInstrumentationLogsWriter = require('../../../../src/ci-visibility/exporters/agentless/di-logs-writer') @@ -64,8 +62,8 @@ describe('AgentProxyCiVisibilityExporter', () => { await agentProxyCiVisibilityExporter._canUseCiVisProtocolPromise - expect(agentProxyCiVisibilityExporter.getUncodedTraces()).not.to.include(trace) - expect(agentProxyCiVisibilityExporter._coverageBuffer).not.to.include(coverage) + assert.ok(!(agentProxyCiVisibilityExporter.getUncodedTraces()).includes(trace)) + assert.ok(!(agentProxyCiVisibilityExporter._coverageBuffer).includes(coverage)) // old traces and coverages are exported at once sinon.assert.calledWith(agentProxyCiVisibilityExporter.export, trace) sinon.assert.calledWith(agentProxyCiVisibilityExporter.exportCoverage, coverage) diff --git a/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js index b5a6fa5eb8d..fc4d725c9f0 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js @@ -1,11 +1,10 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../../../../../dd-trace/test/setup/core') - const id = require('../../../../src/id') let CoverageWriter diff --git a/packages/dd-trace/test/ci-visibility/exporters/agentless/di-logs-writer.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agentless/di-logs-writer.spec.js index 4e855d272e1..44f60de43d7 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agentless/di-logs-writer.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agentless/di-logs-writer.spec.js @@ -1,12 +1,13 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach, context } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') +const context = describe const sinon = require('sinon') const nock = require('nock') require('../../../../../dd-trace/test/setup/core') - const DynamicInstrumentationLogsWriter = require('../../../../src/ci-visibility/exporters/agentless/di-logs-writer') const log = require('../../../../src/log') diff --git a/packages/dd-trace/test/ci-visibility/exporters/agentless/exporter.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agentless/exporter.spec.js index da7c903f8c1..4ac06ce7379 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agentless/exporter.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agentless/exporter.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') +const cp = require('node:child_process') -const { describe, it, beforeEach, afterEach, before, after, context } = require('tap').mocha +const { describe, it, beforeEach, afterEach, before, after } = require('mocha') +const context = describe const sinon = require('sinon') const nock = require('nock') -const cp = require('node:child_process') require('../../../../../dd-trace/test/setup/core') - const AgentlessCiVisibilityExporter = require('../../../../src/ci-visibility/exporters/agentless') const DynamicInstrumentationLogsWriter = require('../../../../src/ci-visibility/exporters/agentless/di-logs-writer') diff --git a/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js index f93b656807c..aa80b403eaa 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js @@ -1,6 +1,6 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js b/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js index c63f6000c6b..1b657517047 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/ci-visibility-exporter.spec.js @@ -1,19 +1,17 @@ 'use strict' const assert = require('node:assert/strict') - -const { expect } = require('chai') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') - -const { describe, it, beforeEach, afterEach, context } = require('tap').mocha -const sinon = require('sinon') -const nock = require('nock') const cp = require('node:child_process') const fs = require('node:fs') const zlib = require('node:zlib') -require('../../../../dd-trace/test/setup/core') +const { describe, it, beforeEach, afterEach } = require('mocha') +const context = describe +const sinon = require('sinon') +const nock = require('nock') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') +require('../../../../dd-trace/test/setup/core') const CiVisibilityExporter = require('../../../src/ci-visibility/exporters/ci-visibility-exporter') describe('CI Visibility Exporter', () => { @@ -948,7 +946,7 @@ describe('CI Visibility Exporter', () => { }, diLog ) - expect(ciVisibilityExporter._logsWriter.append).to.be.calledWith(sinon.match({ + sinon.assert.calledWith(ciVisibilityExporter._logsWriter.append, sinon.match({ ddtags: 'git.repository_url:https://github.com/datadog/dd-trace-js.git,git.commit.sha:1234', level: 'error', ddsource: 'dd_debugger', 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 681c6b1276b..986c8eb66ae 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 @@ -1,17 +1,16 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../../../integration-tests/helpers') +const fs = require('node:fs') +const os = require('node:os') -const { describe, it, beforeEach, afterEach, before, after } = require('tap').mocha +const { describe, it, beforeEach, afterEach, before, after } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire').noPreserveCache() const nock = require('nock') -const fs = require('node:fs') -const os = require('node:os') +const { assertObjectContains } = require('../../../../../../integration-tests/helpers') require('../../../../../dd-trace/test/setup/core') - const { validateGitRepositoryUrl, validateGitCommitSha } = require('../../../../src/plugins/util/user-provided-git') describe('git_metadata', () => { 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 d60698e38a5..2d4f1ab5347 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 @@ -1,11 +1,12 @@ 'use strict' -const { describe, it, beforeEach, afterEach, context } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') +const context = describe const sinon = require('sinon') require('../../../../../dd-trace/test/setup/core') - const TestWorkerCiVisibilityExporter = require('../../../../src/ci-visibility/exporters/test-worker') + const { JEST_WORKER_TRACE_PAYLOAD_CODE, JEST_WORKER_COVERAGE_PAYLOAD_CODE, diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 83d6b6da389..f66909b97a4 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -1,17 +1,16 @@ 'use strict' -const { expect } = require('chai') -const sinon = require('sinon') -const { it, describe, beforeEach, afterEach, context } = require('tap').mocha -const proxyquire = require('proxyquire') - const { readFileSync, mkdtempSync, rmSync, writeFileSync } = require('node:fs') const assert = require('node:assert/strict') const { once } = require('node:events') const path = require('node:path') -require('./setup/core') +const sinon = require('sinon') +const { it, describe, beforeEach, afterEach } = require('mocha') +const context = describe +const proxyquire = require('proxyquire') +require('./setup/core') const { GRPC_CLIENT_ERROR_STATUSES, GRPC_SERVER_ERROR_STATUSES } = require('../src/constants') const { getEnvironmentVariable, getEnvironmentVariables } = require('../src/config-helper') const { assertObjectContains } = require('../../../integration-tests/helpers') @@ -38,6 +37,8 @@ describe('Config', () => { const BLOCKED_TEMPLATE_GRAPHQL_PATH = require.resolve('./fixtures/config/appsec-blocked-graphql-template.json') const BLOCKED_TEMPLATE_GRAPHQL = readFileSync(BLOCKED_TEMPLATE_GRAPHQL_PATH, { encoding: 'utf8' }) + const comparator = (a, b) => a.name.localeCompare(b.name) || a.origin.localeCompare(b.origin) + function reloadLoggerAndConfig () { log = proxyquire('../src/log', {}) log.use = sinon.spy() @@ -130,7 +131,7 @@ describe('Config', () => { process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED = 'true' assert.strictEqual(process.env.DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED, undefined) const config = getConfig() - expect(config).to.have.property('runtimeMetricsRuntimeId', true) + assert.strictEqual(config.runtimeMetricsRuntimeId, true) assert.strictEqual(getEnvironmentVariable('DD_RUNTIME_METRICS_RUNTIME_ID_ENABLED'), 'true') delete process.env.DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED @@ -159,9 +160,11 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('debug', true) - expect(config).to.have.property('logger', undefined) - expect(config).to.have.property('logLevel', 'error') + assertObjectContains(config, { + debug: true, + logger: undefined, + logLevel: 'error' + }) }) it('should initialize from environment variables with DD env vars taking precedence OTEL env vars', () => { @@ -188,19 +191,28 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('debug', false) - expect(config).to.have.property('service', 'service') - expect(config).to.have.property('logLevel', 'error') - expect(config).to.have.property('sampleRate', 0.5) - expect(config).to.have.nested.property('runtimeMetrics.enabled', true) - expect(config.tags).to.include({ foo: 'bar', baz: 'qux' }) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['b3', 'tracecontext']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['b3', 'tracecontext']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.otelPropagators', false) + assertObjectContains(config, { + debug: false, + service: 'service', + logLevel: 'error', + sampleRate: 0.5, + runtimeMetrics: { + enabled: true + }, + tags: { + foo: 'bar', + baz: 'qux' + }, + tracePropagationStyle: { + inject: ['b3', 'tracecontext'], + extract: ['b3', 'tracecontext'], + otelPropagators: false + } + }) const indexFile = require('../src/index') const proxy = require('../src/proxy') - expect(indexFile).to.equal(proxy) + assert.strictEqual(indexFile, proxy) }) it('should initialize with OTEL environment variables when DD env vars are not set', () => { @@ -218,20 +230,29 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('debug', true) - expect(config).to.have.property('service', 'otel_service') - expect(config).to.have.property('logLevel', 'debug') - expect(config).to.have.property('sampleRate', 0.1) - expect(config).to.have.nested.property('runtimeMetrics.enabled', false) - expect(config.tags).to.include({ foo: 'bar1', baz: 'qux1' }) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['b3', 'datadog']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['b3', 'datadog']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.otelPropagators', true) + assertObjectContains(config, { + debug: true, + service: 'otel_service', + logLevel: 'debug', + sampleRate: 0.1, + runtimeMetrics: { + enabled: false + }, + tags: { + foo: 'bar1', + baz: 'qux1' + }, + tracePropagationStyle: { + inject: ['b3', 'datadog'], + extract: ['b3', 'datadog'], + otelPropagators: true + } + }) delete require.cache[require.resolve('../src/index')] const indexFile = require('../src/index') const noop = require('../src/noop/proxy') - expect(indexFile).to.equal(noop) + assert.strictEqual(indexFile, noop) }) it('should correctly map OTEL_RESOURCE_ATTRIBUTES', () => { @@ -239,142 +260,195 @@ describe('Config', () => { 'deployment.environment=test1,service.name=test2,service.version=5,foo=bar1,baz=qux1' const config = getConfig() - expect(config).to.have.property('env', 'test1') - expect(config).to.have.property('service', 'test2') - expect(config).to.have.property('version', '5') - expect(config.tags).to.include({ foo: 'bar1', baz: 'qux1' }) + assertObjectContains(config, { + env: 'test1', + service: 'test2', + version: '5', + tags: { + foo: 'bar1', + baz: 'qux1' + } + }) }) it('should correctly map OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG', () => { process.env.OTEL_TRACES_SAMPLER = 'always_on' process.env.OTEL_TRACES_SAMPLER_ARG = '0.1' let config = getConfig() - expect(config).to.have.property('sampleRate', 1.0) + assert.strictEqual(config.sampleRate, 1.0) process.env.OTEL_TRACES_SAMPLER = 'always_off' config = getConfig() - expect(config).to.have.property('sampleRate', 0.0) + assert.strictEqual(config.sampleRate, 0.0) process.env.OTEL_TRACES_SAMPLER = 'traceidratio' config = getConfig() - expect(config).to.have.property('sampleRate', 0.1) + assert.strictEqual(config.sampleRate, 0.1) process.env.OTEL_TRACES_SAMPLER = 'parentbased_always_on' config = getConfig() - expect(config).to.have.property('sampleRate', 1.0) + assert.strictEqual(config.sampleRate, 1.0) process.env.OTEL_TRACES_SAMPLER = 'parentbased_always_off' config = getConfig() - expect(config).to.have.property('sampleRate', 0.0) + assert.strictEqual(config.sampleRate, 0.0) process.env.OTEL_TRACES_SAMPLER = 'parentbased_traceidratio' config = getConfig() - expect(config).to.have.property('sampleRate', 0.1) + assert.strictEqual(config.sampleRate, 0.1) }) it('should initialize with the correct defaults', () => { const config = getConfig() - expect(config).to.have.nested.property('apmTracingEnabled', true) - expect(config).to.have.property('appKey', undefined) - expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.sampleDelay', 30) - expect(config).to.have.nested.property('appsec.apiSecurity.endpointCollectionEnabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.endpointCollectionMessageLimit', 300) - expect(config).to.have.nested.property('appsec.blockedTemplateHtml', undefined) - expect(config).to.have.nested.property('appsec.blockedTemplateJson', undefined) - expect(config).to.have.nested.property('appsec.blockedTemplateGraphql', undefined) - expect(config).to.have.nested.property('appsec.enabled', undefined) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'identification') - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.enabled', false) - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.maxHeaders', 50) - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.redaction', true) - expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex').with.length(190) - expect(config).to.have.nested.property('appsec.obfuscatorValueRegex').with.length(578) - expect(config).to.have.nested.property('appsec.rules', undefined) - expect(config).to.have.nested.property('appsec.rasp.bodyCollection', false) - expect(config).to.have.nested.property('appsec.rasp.enabled', true) - expect(config).to.have.nested.property('appsec.rateLimit', 100) - expect(config).to.have.nested.property('appsec.sca.enabled', null) - expect(config).to.have.nested.property('appsec.stackTrace.enabled', true) - expect(config).to.have.nested.property('appsec.stackTrace.maxDepth', 32) - expect(config).to.have.nested.property('appsec.stackTrace.maxStackTraces', 2) - expect(config).to.have.nested.property('appsec.wafTimeout', 5e3) - expect(config).to.have.property('clientIpEnabled', false) - expect(config).to.have.property('clientIpHeader', null) - expect(config).to.have.nested.property('codeOriginForSpans.enabled', true) - expect(config).to.have.nested.property('codeOriginForSpans.experimental.exit_spans.enabled', false) - expect(config).to.have.nested.property('crashtracking.enabled', true) - expect(config).to.have.property('debug', false) - expect(config).to.have.nested.property('dogstatsd.hostname', '127.0.0.1') - expect(config).to.have.nested.property('dogstatsd.port', '8125') - expect(config).to.have.nested.property('dynamicInstrumentation.enabled', false) - expect(config).to.have.nested.property('dynamicInstrumentation.probeFile', undefined) - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactedIdentifiers', []) - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactionExcludedIdentifiers', []) - expect(config).to.have.nested.property('dynamicInstrumentation.uploadIntervalSeconds', 1) - expect(config).to.have.property('env', undefined) - expect(config).to.have.nested.property('experimental.aiguard.enabled', false) - expect(config).to.have.nested.property('experimental.aiguard.endpoint', undefined) - expect(config).to.have.nested.property('experimental.aiguard.maxContentSize', 512 * 1024) - expect(config).to.have.nested.property('experimental.aiguard.maxMessagesLength', 16) - expect(config).to.have.nested.property('experimental.aiguard.timeout', 10_000) - expect(config).to.have.nested.property('experimental.exporter', undefined) - expect(config).to.have.nested.property('experimental.enableGetRumData', false) - expect(config).to.have.property('flushInterval', 2000) - expect(config).to.have.property('flushMinSpans', 1000) - expect(config.grpc.client.error.statuses).to.deep.equal(GRPC_CLIENT_ERROR_STATUSES) - expect(config.grpc.server.error.statuses).to.deep.equal(GRPC_SERVER_ERROR_STATUSES) - expect(config).to.have.nested.property('heapSnapshot.count', 0) - expect(config).to.have.nested.property('heapSnapshot.destination', '') - expect(config).to.have.nested.property('heapSnapshot.interval', 3600) - expect(config).to.have.nested.property('iast.enabled', false) - expect(config).to.have.nested.property('iast.redactionEnabled', true) - expect(config).to.have.nested.property('iast.redactionNamePattern', null) - expect(config).to.have.nested.property('iast.redactionValuePattern', null) - expect(config).to.have.nested.property('iast.telemetryVerbosity', 'INFORMATION') - expect(config).to.have.nested.property('iast.stackTrace.enabled', true) - expect(config).to.have.nested.property('injectForce', null) - expect(config).to.have.nested.deep.property('injectionEnabled', []) - expect(config).to.have.nested.property('installSignature.id', null) - expect(config).to.have.nested.property('installSignature.time', null) - expect(config).to.have.nested.property('installSignature.type', null) - expect(config).to.have.nested.property('instrumentationSource', 'manual') - expect(config).to.have.property('instrumentation_config_id', undefined) - expect(config).to.have.nested.property('llmobs.agentlessEnabled', undefined) - expect(config).to.have.nested.property('llmobs.enabled', false) - expect(config).to.have.nested.property('llmobs.mlApp', undefined) - expect(config).to.have.property('logLevel', 'debug') - expect(config).to.have.property('middlewareTracingEnabled', true) - expect(config).to.have.property('plugins', true) - expect(config).to.have.property('protocolVersion', '0.4') - expect(config).to.have.property('queryStringObfuscation').with.length(626) - expect(config).to.have.nested.property('remoteConfig.enabled', true) - expect(config).to.have.nested.property('remoteConfig.pollInterval', 5) - expect(config).to.have.property('reportHostname', false) - expect(config).to.have.nested.property('runtimeMetrics.enabled', false) - expect(config).to.have.nested.property('runtimeMetrics.eventLoop', true) - expect(config).to.have.nested.property('runtimeMetrics.gc', true) - expect(config).to.have.property('runtimeMetricsRuntimeId', false) - expect(config).to.have.property('sampleRate', undefined) - expect(config).to.have.property('scope', undefined) - expect(config).to.have.property('service', 'node') - expect(config).to.have.deep.property('serviceMapping', {}) - expect(config).to.have.property('spanAttributeSchema', 'v0') - expect(config).to.have.property('spanComputePeerService', false) - expect(config).to.have.property('spanRemoveIntegrationFromService', false) - expect(config.tags).to.have.property('service', 'node') - expect(config).to.have.property('traceEnabled', true) - expect(config).to.have.property('traceId128BitGenerationEnabled', true) - expect(config).to.have.property('traceId128BitLoggingEnabled', true) - expect(config).to.have.nested.property('tracePropagationBehaviorExtract', 'continue') - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['datadog', 'tracecontext', 'baggage']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['datadog', 'tracecontext', 'baggage']) - expect(config).to.have.property('tracing', true) - - expect(updateConfig).to.be.calledOnce - - expect(updateConfig.getCall(0).args[0]).to.deep.include.members([ + assertObjectContains(config, { + apmTracingEnabled: true, + appKey: undefined, + appsec: { + apiSecurity: { + enabled: true, + sampleDelay: 30, + endpointCollectionEnabled: true, + endpointCollectionMessageLimit: 300 + }, + blockedTemplateHtml: undefined, + blockedTemplateJson: undefined, + blockedTemplateGraphql: undefined, + enabled: undefined, + eventTracking: { + mode: 'identification' + }, + extendedHeadersCollection: { + enabled: false, + maxHeaders: 50, + redaction: true + }, + rules: undefined, + rasp: { + bodyCollection: false, + enabled: true + }, + rateLimit: 100, + sca: { + enabled: null + }, + stackTrace: { + enabled: true, + maxDepth: 32, + maxStackTraces: 2 + }, + wafTimeout: 5e3 + }, + clientIpEnabled: false, + clientIpHeader: null, + codeOriginForSpans: { + enabled: true, + experimental: { + exit_spans: { + enabled: false + } + } + }, + crashtracking: { + enabled: true + }, + debug: false, + dogstatsd: { + hostname: '127.0.0.1', + port: '8125' + }, + dynamicInstrumentation: { + enabled: false, + probeFile: undefined, + uploadIntervalSeconds: 1 + }, + env: undefined, + experimental: { + aiguard: { + enabled: false, + endpoint: undefined, + maxMessagesLength: 16, + timeout: 10_000, + maxContentSize: 512 * 1024, + }, + exporter: undefined, + enableGetRumData: false + }, + flushInterval: 2000, + flushMinSpans: 1000, + heapSnapshot: { + count: 0, + destination: '', + interval: 3600 + }, + iast: { + enabled: false, + redactionEnabled: true, + redactionNamePattern: null, + redactionValuePattern: null, + telemetryVerbosity: 'INFORMATION', + stackTrace: { + enabled: true + } + }, + injectForce: null, + installSignature: { + id: null, + time: null, + type: null + }, + instrumentationSource: 'manual', + instrumentation_config_id: undefined, + llmobs: { + agentlessEnabled: undefined, + enabled: false, + mlApp: undefined + }, + logLevel: 'debug', + middlewareTracingEnabled: true, + plugins: true, + protocolVersion: '0.4', + tracing: true, + tags: { + service: 'node' + }, + remoteConfig: { + enabled: true, + pollInterval: 5 + }, + reportHostname: false, + runtimeMetrics: { + enabled: false, + eventLoop: true, + gc: true + }, + runtimeMetricsRuntimeId: false, + sampleRate: undefined, + scope: undefined, + service: 'node', + spanAttributeSchema: 'v0', + spanComputePeerService: false, + spanRemoveIntegrationFromService: false, + traceEnabled: true, + traceId128BitGenerationEnabled: true, + traceId128BitLoggingEnabled: true, + tracePropagationBehaviorExtract: 'continue' + }) + assert.deepStrictEqual(config.dynamicInstrumentation?.redactedIdentifiers, []) + assert.deepStrictEqual(config.dynamicInstrumentation?.redactionExcludedIdentifiers, []) + assert.deepStrictEqual(config.grpc.client.error.statuses, GRPC_CLIENT_ERROR_STATUSES) + assert.deepStrictEqual(config.grpc.server.error.statuses, GRPC_SERVER_ERROR_STATUSES) + assert.deepStrictEqual(config.injectionEnabled, []) + assert.deepStrictEqual(config.serviceMapping, {}) + assert.deepStrictEqual(config.tracePropagationStyle?.extract, ['datadog', 'tracecontext', 'baggage']) + assert.deepStrictEqual(config.tracePropagationStyle?.inject, ['datadog', 'tracecontext', 'baggage']) + assert.strictEqual(config.queryStringObfuscation?.length, 626) + assert.strictEqual(config.appsec?.obfuscatorKeyRegex?.length, 190) + assert.strictEqual(config.appsec?.obfuscatorValueRegex?.length, 578) + + sinon.assert.calledOnce(updateConfig) + + assertObjectContains(updateConfig.getCall(0).args[0].sort(comparator), [ { name: 'apmTracingEnabled', value: true, origin: 'default' }, { name: 'appsec.apiSecurity.enabled', value: true, origin: 'default' }, { name: 'appsec.apiSecurity.sampleDelay', value: 30, origin: 'default' }, @@ -459,13 +533,10 @@ describe('Config', () => { { name: 'isGitUploadEnabled', value: false, origin: 'default' }, { name: 'isIntelligentTestRunnerEnabled', value: false, origin: 'default' }, { name: 'isManualApiEnabled', value: false, origin: 'default' }, - { name: 'isTestDynamicInstrumentationEnabled', value: false, origin: 'default' }, { name: 'langchain.spanCharLimit', value: 128, origin: 'default' }, { name: 'langchain.spanPromptCompletionSampleRate', value: 1.0, origin: 'default' }, { name: 'llmobs.agentlessEnabled', value: undefined, origin: 'default' }, { name: 'llmobs.mlApp', value: undefined, origin: 'default' }, - { name: 'ciVisibilityTestSessionName', value: '', origin: 'default' }, - { name: 'ciVisAgentlessLogSubmissionEnabled', value: false, origin: 'default' }, { name: 'isTestDynamicInstrumentationEnabled', value: false, origin: 'default' }, { name: 'logInjection', value: true, origin: 'default' }, { name: 'lookup', value: undefined, origin: 'default' }, @@ -488,7 +559,6 @@ describe('Config', () => { { name: 'remoteConfig.enabled', value: true, origin: 'default' }, { name: 'remoteConfig.pollInterval', value: 5, origin: 'default' }, { name: 'reportHostname', value: false, origin: 'default' }, - { name: 'reportHostname', value: false, origin: 'default' }, { name: 'runtimeMetrics.enabled', value: false, origin: 'default' }, { name: 'runtimeMetricsRuntimeId', value: false, origin: 'default' }, { name: 'sampleRate', value: undefined, origin: 'default' }, @@ -517,7 +587,7 @@ describe('Config', () => { { name: 'version', value: '', origin: 'default' }, { name: 'vertexai.spanCharLimit', value: 128, origin: 'default' }, { name: 'vertexai.spanPromptCompletionSampleRate', value: 1.0, origin: 'default' } - ]) + ].sort(comparator)) }) it('should support logging', () => { @@ -526,8 +596,8 @@ describe('Config', () => { debug: true }) - expect(log.use).to.have.been.calledWith(config.logger) - expect(log.toggle).to.have.been.calledWith(config.debug) + sinon.assert.calledWith(log.use, config.logger) + sinon.assert.calledWith(log.toggle, config.debug) }) it('should not warn on undefined DD_TRACE_SPAN_ATTRIBUTE_SCHEMA', () => { @@ -535,8 +605,8 @@ describe('Config', () => { logger: {}, debug: true }) - expect(log.warn).not.to.be.called - expect(config).to.have.property('spanAttributeSchema', 'v0') + sinon.assert.notCalled(log.warn) + assert.strictEqual(config.spanAttributeSchema, 'v0') }) it('should initialize from the default service', () => { @@ -545,8 +615,8 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('service', 'test') - expect(config.tags).to.have.property('service', 'test') + assert.strictEqual(config.service, 'test') + assert.strictEqual(config.tags?.service, 'test') }) it('should initialize from the default version', () => { @@ -555,16 +625,16 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('version', '1.2.3') - expect(config.tags).to.have.property('version', '1.2.3') + assert.strictEqual(config.version, '1.2.3') + assert.strictEqual(config.tags?.version, '1.2.3') }) it('should initialize from environment variables', () => { process.env.DD_AI_GUARD_ENABLED = 'true' process.env.DD_AI_GUARD_ENDPOINT = 'https://dd.datad0g.com/api/unstable/ai-guard' - process.env.DD_AI_GUARD_MAX_CONTENT_SIZE = 1024 * 1024 - process.env.DD_AI_GUARD_MAX_MESSAGES_LENGTH = 32 - process.env.DD_AI_GUARD_TIMEOUT = 2000 + process.env.DD_AI_GUARD_MAX_CONTENT_SIZE = String(1024 * 1024) + process.env.DD_AI_GUARD_MAX_MESSAGES_LENGTH = '32' + process.env.DD_AI_GUARD_TIMEOUT = '2000' process.env.DD_API_SECURITY_ENABLED = 'true' process.env.DD_API_SECURITY_SAMPLE_DELAY = '25' process.env.DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED = 'false' @@ -684,89 +754,151 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.nested.property('apmTracingEnabled', false) - expect(config).to.have.property('appKey', 'myAppKey') - expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.sampleDelay', 25) - expect(config).to.have.nested.property('appsec.apiSecurity.endpointCollectionEnabled', false) - expect(config).to.have.nested.property('appsec.apiSecurity.endpointCollectionMessageLimit', 500) - expect(config).to.have.nested.property('appsec.blockedTemplateGraphql', BLOCKED_TEMPLATE_GRAPHQL) - expect(config).to.have.nested.property('appsec.blockedTemplateHtml', BLOCKED_TEMPLATE_HTML) - expect(config).to.have.nested.property('appsec.blockedTemplateJson', BLOCKED_TEMPLATE_JSON) - expect(config).to.have.nested.property('appsec.enabled', true) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'extended') - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.enabled', true) - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.maxHeaders', 42) - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.redaction', false) - expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex', '.*') - expect(config).to.have.nested.property('appsec.obfuscatorValueRegex', '.*') - expect(config).to.have.nested.property('appsec.rasp.bodyCollection', true) - expect(config).to.have.nested.property('appsec.rasp.enabled', false) - expect(config).to.have.nested.property('appsec.rateLimit', 42) - expect(config).to.have.nested.property('appsec.rules', RULES_JSON_PATH) - expect(config).to.have.nested.property('appsec.sca.enabled', true) - expect(config).to.have.nested.property('appsec.stackTrace.enabled', false) - expect(config).to.have.nested.property('appsec.stackTrace.maxDepth', 42) - expect(config).to.have.nested.property('appsec.stackTrace.maxStackTraces', 5) - expect(config).to.have.nested.property('appsec.wafTimeout', 42) - expect(config).to.have.property('clientIpEnabled', true) - expect(config).to.have.property('clientIpHeader', 'x-true-client-ip') - expect(config).to.have.nested.property('codeOriginForSpans.enabled', false) - expect(config).to.have.nested.property('codeOriginForSpans.experimental.exit_spans.enabled', true) - expect(config).to.have.nested.property('crashtracking.enabled', false) - expect(config).to.have.property('debug', true) - expect(config).to.have.nested.property('dogstatsd.hostname', 'dsd-agent') - expect(config).to.have.nested.property('dogstatsd.port', '5218') - expect(config).to.have.nested.property('dynamicInstrumentation.enabled', true) - expect(config).to.have.nested.property('dynamicInstrumentation.probeFile', 'probes.json') - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactedIdentifiers', ['foo', 'bar']) - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactionExcludedIdentifiers', ['a', 'b', 'c']) - expect(config).to.have.nested.property('dynamicInstrumentation.uploadIntervalSeconds', 0.1) - expect(config).to.have.property('env', 'test') - expect(config).to.have.nested.property('experimental.aiguard.enabled', true) - expect(config).to.have.nested.property('experimental.aiguard.endpoint', 'https://dd.datad0g.com/api/unstable/ai-guard') - expect(config).to.have.nested.property('experimental.aiguard.maxContentSize', 1024 * 1024) - expect(config).to.have.nested.property('experimental.aiguard.maxMessagesLength', 32) - expect(config).to.have.nested.property('experimental.aiguard.timeout', 2000) - expect(config).to.have.nested.property('experimental.enableGetRumData', true) - expect(config).to.have.nested.property('experimental.exporter', 'log') - expect(config.grpc.client.error.statuses).to.deep.equal([3, 13, 400, 401, 402, 403]) - expect(config.grpc.server.error.statuses).to.deep.equal([3, 13, 400, 401, 402, 403]) - expect(config).to.have.property('hostname', 'agent') - expect(config).to.have.nested.property('heapSnapshot.count', 1) - expect(config).to.have.nested.property('heapSnapshot.destination', '/tmp') - expect(config).to.have.nested.property('heapSnapshot.interval', 1800) - expect(config).to.have.nested.property('iast.dbRowsToTaint', 2) - expect(config).to.have.nested.property('iast.deduplicationEnabled', false) - expect(config).to.have.nested.property('iast.enabled', true) - expect(config).to.have.nested.property('iast.maxConcurrentRequests', 3) - expect(config).to.have.nested.property('iast.maxContextOperations', 4) - expect(config).to.have.nested.property('iast.redactionEnabled', false) - expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') - expect(config).to.have.nested.property('iast.redactionValuePattern', 'REDACTION_VALUE_PATTERN') - expect(config).to.have.nested.property('iast.requestSampling', 40) - expect(config).to.have.nested.property('iast.securityControlsConfiguration', - 'SANITIZER:CODE_INJECTION:sanitizer.js:method') - expect(config).to.have.nested.property('iast.stackTrace.enabled', false) - expect(config).to.have.nested.property('iast.telemetryVerbosity', 'DEBUG') - expect(config).to.have.deep.property('installSignature', - { id: '68e75c48-57ca-4a12-adfc-575c4b05fcbe', type: 'k8s_single_step', time: '1703188212' }) - expect(config).to.have.property('instrumentation_config_id', 'abcdef123') - expect(config).to.have.nested.property('llmobs.agentlessEnabled', true) - expect(config).to.have.nested.property('llmobs.mlApp', 'myMlApp') - expect(config).to.have.property('middlewareTracingEnabled', false) - expect(config).to.have.deep.property('peerServiceMapping', { c: 'cc', d: 'dd' }) - expect(config).to.have.property('protocolVersion', '0.5') - expect(config).to.have.property('queryStringObfuscation', '.*') - expect(config).to.have.nested.property('remoteConfig.enabled', false) - expect(config).to.have.nested.property('remoteConfig.pollInterval', 42) - expect(config).to.have.property('reportHostname', true) - expect(config).to.have.nested.property('runtimeMetrics.enabled', true) - expect(config).to.have.nested.property('runtimeMetrics.eventLoop', false) - expect(config).to.have.nested.property('runtimeMetrics.gc', false) - expect(config).to.have.property('runtimeMetricsRuntimeId', true) - expect(config).to.have.property('sampleRate', 0.5) - expect(config).to.have.deep.nested.property('sampler', { + assertObjectContains(config, { + apmTracingEnabled: false, + appKey: 'myAppKey', + appsec: { + apiSecurity: { + enabled: true, + sampleDelay: 25, + endpointCollectionEnabled: false, + endpointCollectionMessageLimit: 500 + }, + blockedTemplateGraphql: BLOCKED_TEMPLATE_GRAPHQL, + blockedTemplateHtml: BLOCKED_TEMPLATE_HTML, + blockedTemplateJson: BLOCKED_TEMPLATE_JSON, + enabled: true, + eventTracking: { + mode: 'extended' + }, + extendedHeadersCollection: { + enabled: true, + maxHeaders: 42, + redaction: false + }, + obfuscatorKeyRegex: '.*', + obfuscatorValueRegex: '.*', + rasp: { + bodyCollection: true, + enabled: false + }, + rateLimit: 42, + rules: RULES_JSON_PATH, + sca: { + enabled: true + }, + stackTrace: { + enabled: false, + maxDepth: 42, + maxStackTraces: 5 + }, + wafTimeout: 42 + }, + clientIpEnabled: true, + clientIpHeader: 'x-true-client-ip', + codeOriginForSpans: { + enabled: false, + experimental: { + exit_spans: { + enabled: true + } + } + }, + crashtracking: { + enabled: false + }, + debug: true, + dogstatsd: { + hostname: 'dsd-agent', + port: '5218' + }, + dynamicInstrumentation: { + enabled: true, + probeFile: 'probes.json', + redactedIdentifiers: ['foo', 'bar'], + redactionExcludedIdentifiers: ['a', 'b', 'c'], + uploadIntervalSeconds: 0.1 + }, + env: 'test', + experimental: { + aiguard: { + enabled: true, + endpoint: 'https://dd.datad0g.com/api/unstable/ai-guard', + maxContentSize: 1024 * 1024, + maxMessagesLength: 32, + timeout: 2000 + }, + enableGetRumData: true, + exporter: 'log' + }, + hostname: 'agent', + heapSnapshot: { + count: 1, + destination: '/tmp', + interval: 1800 + }, + iast: { + dbRowsToTaint: 2, + deduplicationEnabled: false, + enabled: true, + maxConcurrentRequests: 3, + maxContextOperations: 4, + redactionEnabled: false, + redactionNamePattern: 'REDACTION_NAME_PATTERN', + redactionValuePattern: 'REDACTION_VALUE_PATTERN', + requestSampling: 40, + securityControlsConfiguration: 'SANITIZER:CODE_INJECTION:sanitizer.js:method', + stackTrace: { + enabled: false + }, + telemetryVerbosity: 'DEBUG' + }, + instrumentation_config_id: 'abcdef123', + llmobs: { + agentlessEnabled: true, + mlApp: 'myMlApp' + }, + middlewareTracingEnabled: false, + protocolVersion: '0.5', + queryStringObfuscation: '.*', + remoteConfig: { + enabled: false, + pollInterval: 42 + }, + reportHostname: true, + runtimeMetrics: { + enabled: true, + eventLoop: false, + gc: false + }, + runtimeMetricsRuntimeId: true, + sampleRate: 0.5, + service: 'service', + spanAttributeSchema: 'v1', + spanComputePeerService: true, + spanRemoveIntegrationFromService: true, + tags: { + foo: 'bar', + baz: 'qux', + service: 'service', + version: '1.0.0', + env: 'test' + }, + traceEnabled: true, + traceId128BitGenerationEnabled: true, + traceId128BitLoggingEnabled: true, + tracePropagationBehaviorExtract: 'restart', + tracing: false, + version: '1.0.0' + }) + assert.deepStrictEqual(config.grpc.client.error.statuses, [3, 13, 400, 401, 402, 403]) + assert.deepStrictEqual(config.grpc.server.error.statuses, [3, 13, 400, 401, 402, 403]) + assert.deepStrictEqual( + config.installSignature, + { id: '68e75c48-57ca-4a12-adfc-575c4b05fcbe', type: 'k8s_single_step', time: '1703188212' } + ) + assert.deepStrictEqual(config.peerServiceMapping, { c: 'cc', d: 'dd' }) + assert.deepStrictEqual(config.sampler, { sampleRate: 0.5, rateLimit: '-1', rules: [ @@ -782,25 +914,13 @@ describe('Config', () => { { sampleRate: 0.1 } ] }) - expect(config).to.have.property('service', 'service') - expect(config).to.have.deep.property('serviceMapping', { a: 'aa', b: 'bb' }) - expect(config).to.have.property('spanAttributeSchema', 'v1') - expect(config).to.have.property('spanComputePeerService', true) - expect(config).to.have.property('spanRemoveIntegrationFromService', true) - expect(config.tags).to.include({ foo: 'bar', baz: 'qux' }) - expect(config.tags).to.include({ service: 'service', version: '1.0.0', env: 'test' }) - expect(config).to.have.property('traceEnabled', true) - expect(config).to.have.property('traceId128BitGenerationEnabled', true) - expect(config).to.have.property('traceId128BitLoggingEnabled', true) - expect(config).to.have.nested.property('tracePropagationBehaviorExtract', 'restart') - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['b3', 'tracecontext']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['b3', 'tracecontext']) - expect(config).to.have.property('tracing', false) - expect(config).to.have.property('version', '1.0.0') - - expect(updateConfig).to.be.calledOnce - - expect(updateConfig.getCall(0).args[0]).to.deep.include.members([ + assert.deepStrictEqual(config.serviceMapping, { a: 'aa', b: 'bb' }) + assert.deepStrictEqual(config.tracePropagationStyle?.extract, ['b3', 'tracecontext']) + assert.deepStrictEqual(config.tracePropagationStyle?.inject, ['b3', 'tracecontext']) + + sinon.assert.calledOnce(updateConfig) + + assertObjectContains(updateConfig.getCall(0).args[0].sort(comparator), [ { name: 'apmTracingEnabled', value: false, origin: 'env_var' }, { name: 'appsec.apiSecurity.enabled', value: true, origin: 'env_var' }, { name: 'appsec.apiSecurity.sampleDelay', value: 25, origin: 'env_var' }, @@ -893,7 +1013,7 @@ describe('Config', () => { { name: 'version', value: '1.0.0', origin: 'env_var' }, { name: 'vertexai.spanCharLimit', value: 50, origin: 'env_var' }, { name: 'vertexai.spanPromptCompletionSampleRate', value: 0.5, origin: 'env_var' } - ]) + ].sort(comparator)) }) it('should ignore empty strings', () => { @@ -901,17 +1021,21 @@ describe('Config', () => { let config = getConfig() - expect(config).to.have.property('service', 'node') - expect(config).to.have.property('env', undefined) - expect(config).to.have.property('version', '') + assertObjectContains(config, { + service: 'node', + env: undefined, + version: '' + }) process.env.DD_TAGS = 'service: env: version:' config = getConfig() - expect(config).to.have.property('service', 'node') - expect(config).to.have.property('env', undefined) - expect(config).to.have.property('version', '') + assertObjectContains(config, { + service: 'node', + env: undefined, + version: '' + }) }) it('should support space separated tags when experimental mode enabled', () => { @@ -919,36 +1043,42 @@ describe('Config', () => { let config = getConfig() - expect(config.tags).to.include({ key1: 'value1', key2: 'value2' }) + assertObjectContains(config.tags, { key1: 'value1', key2: 'value2' }) process.env.DD_TAGS = 'env:test aKey:aVal bKey:bVal cKey:' config = getConfig() - expect(config.tags).to.have.property('env', 'test') - expect(config.tags).to.have.property('aKey', 'aVal') - expect(config.tags).to.have.property('bKey', 'bVal') - expect(config.tags).to.have.property('cKey', '') + assertObjectContains(config.tags, { + env: 'test', + aKey: 'aVal', + bKey: 'bVal', + cKey: '' + }) process.env.DD_TAGS = 'env:test,aKey:aVal bKey:bVal cKey:' config = getConfig() - expect(config.tags).to.have.property('env', 'test') - expect(config.tags).to.have.property('aKey', 'aVal bKey:bVal cKey:') + assertObjectContains(config.tags, { + env: 'test', + aKey: 'aVal bKey:bVal cKey:' + }) process.env.DD_TAGS = 'a:b:c:d' config = getConfig() - expect(config.tags).to.have.property('a', 'b:c:d') + assert.strictEqual(config.tags?.a, 'b:c:d') process.env.DD_TAGS = 'a,1' config = getConfig() - expect(config.tags).to.have.property('a', '') - expect(config.tags).to.have.property('1', '') + assertObjectContains(config.tags, { + a: '', + 1: '' + }) }) it('should read case-insensitive booleans from environment variables', () => { @@ -958,9 +1088,13 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('tracing', false) - expect(config).to.have.property('tracePropagationExtractFirst', true) - expect(config).to.have.nested.property('runtimeMetrics.enabled', false) + assertObjectContains(config, { + tracing: false, + tracePropagationExtractFirst: true, + runtimeMetrics: { + enabled: false + } + }) }) it('should initialize from environment variables with url taking precedence', () => { @@ -974,14 +1108,17 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('tracing', false) - expect(config).to.have.nested.property('dogstatsd.hostname', 'agent') - expect(config).to.have.nested.property('url.protocol', 'https:') - expect(config).to.have.nested.property('url.hostname', 'agent2') - expect(config).to.have.nested.property('url.port', '7777') - expect(config).to.have.property('site', 'datadoghq.eu') - expect(config).to.have.property('service', 'service') - expect(config).to.have.property('env', 'test') + assert.strictEqual(config.url.toString(), 'https://agent2:7777/') + + assertObjectContains(config, { + tracing: false, + dogstatsd: { + hostname: 'agent' + }, + site: 'datadoghq.eu', + service: 'service', + env: 'test' + }) }) it('should initialize from environment variables with inject/extract taking precedence', () => { @@ -991,8 +1128,8 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['tracecontext']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['tracecontext']) + assert.deepStrictEqual(config.tracePropagationStyle?.inject, ['tracecontext']) + assert.deepStrictEqual(config.tracePropagationStyle?.extract, ['tracecontext']) }) it('should enable crash tracking for SSI by default', () => { @@ -1000,7 +1137,7 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.nested.deep.property('crashtracking.enabled', true) + assert.deepStrictEqual(config.crashtracking?.enabled, true) }) it('should disable crash tracking for SSI when configured', () => { @@ -1009,7 +1146,7 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.nested.deep.property('crashtracking.enabled', false) + assert.deepStrictEqual(config.crashtracking?.enabled, false) }) it('should prioritize DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE over DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING', () => { @@ -1018,7 +1155,7 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'anonymous') + assert.strictEqual(config.appsec?.eventTracking?.mode, 'anonymous') }) it('should initialize from the options', () => { @@ -1143,63 +1280,106 @@ describe('Config', () => { version: '0.1.0' }) - expect(config).to.have.nested.property('appsec.enabled', false) - expect(config).to.have.property('clientIpEnabled', true) - expect(config).to.have.property('clientIpHeader', 'x-true-client-ip') - expect(config).to.have.nested.property('codeOriginForSpans.enabled', false) - expect(config).to.have.nested.property('codeOriginForSpans.experimental.exit_spans.enabled', true) - expect(config).to.have.nested.property('dogstatsd.hostname', 'agent-dsd') - expect(config).to.have.nested.property('dogstatsd.port', '5218') - expect(config).to.have.nested.property('dynamicInstrumentation.enabled', true) - expect(config).to.have.nested.property('dynamicInstrumentation.probeFile', 'probes.json') - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactedIdentifiers', ['foo', 'bar']) - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactionExcludedIdentifiers', ['a', 'b', 'c']) - expect(config).to.have.nested.property('dynamicInstrumentation.uploadIntervalSeconds', 0.1) - expect(config).to.have.property('env', 'test') - expect(config).to.have.nested.property('experimental.aiguard.enabled', true) - expect(config).to.have.nested.property('experimental.aiguard.endpoint', 'https://dd.datad0g.com/api/unstable/ai-guard') - expect(config).to.have.nested.property('experimental.aiguard.maxContentSize', 1024 * 1024) - expect(config).to.have.nested.property('experimental.aiguard.maxMessagesLength', 32) - expect(config).to.have.nested.property('experimental.aiguard.timeout', 2000) - expect(config).to.have.nested.property('experimental.enableGetRumData', true) - expect(config).to.have.nested.property('experimental.exporter', 'log') - expect(config).to.have.property('flushInterval', 5000) - expect(config).to.have.property('flushMinSpans', 500) - expect(config).to.have.property('hostname', 'agent') - expect(config).to.have.nested.property('iast.dbRowsToTaint', 2) - expect(config).to.have.nested.property('iast.deduplicationEnabled', false) - expect(config).to.have.nested.property('iast.enabled', true) - expect(config).to.have.nested.property('iast.maxConcurrentRequests', 4) - expect(config).to.have.nested.property('iast.maxContextOperations', 5) - expect(config).to.have.nested.property('iast.redactionEnabled', false) - expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') - expect(config).to.have.nested.property('iast.redactionValuePattern', 'REDACTION_VALUE_PATTERN') - expect(config).to.have.nested.property('iast.requestSampling', 50) + assertObjectContains(config, { + appsec: { + enabled: false + }, + clientIpEnabled: true, + clientIpHeader: 'x-true-client-ip', + codeOriginForSpans: { + enabled: false, + experimental: { + exit_spans: { + enabled: true + } + } + }, + dogstatsd: { + hostname: 'agent-dsd', + port: '5218' + }, + dynamicInstrumentation: { + enabled: true, + probeFile: 'probes.json' + } + }) + assert.deepStrictEqual(config.dynamicInstrumentation?.redactedIdentifiers, ['foo', 'bar']) + assert.deepStrictEqual(config.dynamicInstrumentation?.redactionExcludedIdentifiers, ['a', 'b', 'c']) + assertObjectContains(config, { + dynamicInstrumentation: { + uploadIntervalSeconds: 0.1 + }, + env: 'test', + experimental: { + aiguard: { + enabled: true, + endpoint: 'https://dd.datad0g.com/api/unstable/ai-guard' + } + } + }) + assert.strictEqual(config.experimental?.aiguard?.maxContentSize, 1024 * 1024) + assertObjectContains(config, { + experimental: { + aiguard: { + maxMessagesLength: 32, + timeout: 2000 + }, + enableGetRumData: true, + exporter: 'log' + }, + flushInterval: 5000, + flushMinSpans: 500, + hostname: 'agent', + iast: { + dbRowsToTaint: 2, + deduplicationEnabled: false, + enabled: true, + maxConcurrentRequests: 4, + maxContextOperations: 5, + redactionEnabled: false, + redactionNamePattern: 'REDACTION_NAME_PATTERN', + redactionValuePattern: 'REDACTION_VALUE_PATTERN', + requestSampling: 50 + } + }) if (DD_MAJOR < 6) { - expect(config).to.have.nested.property('iast.securityControlsConfiguration', - 'SANITIZER:CODE_INJECTION:sanitizer.js:method') + assert.strictEqual(config.iast?.securityControlsConfiguration, 'SANITIZER:CODE_INJECTION:sanitizer.js:method') } else { - expect(config).to.not.have.property('iast.securityControlsConfiguration') + assert.ok(!('iast.securityControlsConfiguration' in config)) } - expect(config).to.have.nested.property('iast.stackTrace.enabled', false) - expect(config).to.have.nested.property('iast.telemetryVerbosity', 'DEBUG') - expect(config).to.have.nested.property('llmobs.agentlessEnabled', true) - expect(config).to.have.nested.property('llmobs.mlApp', 'myMlApp') - expect(config).to.have.property('logLevel', logLevel) - expect(config).to.have.property('logger', logger) - expect(config).to.have.property('middlewareTracingEnabled', false) - expect(config).to.have.deep.property('peerServiceMapping', { d: 'dd' }) - expect(config).to.have.property('plugins', false) - expect(config).to.have.property('port', '6218') - expect(config).to.have.property('protocolVersion', '0.5') - expect(config).to.have.nested.property('remoteConfig.pollInterval', 42) - expect(config).to.have.property('reportHostname', true) - expect(config).to.have.nested.property('runtimeMetrics.enabled', true) - expect(config).to.have.nested.property('runtimeMetrics.eventLoop', false) - expect(config).to.have.nested.property('runtimeMetrics.gc', false) - expect(config).to.have.property('runtimeMetricsRuntimeId', true) - expect(config).to.have.property('sampleRate', 0.5) - expect(config).to.have.deep.nested.property('sampler', { + assertObjectContains(config, { + iast: { + stackTrace: { + enabled: false + }, + telemetryVerbosity: 'DEBUG' + }, + llmobs: { + agentlessEnabled: true, + mlApp: 'myMlApp' + } + }) + assert.strictEqual(config.logLevel, logLevel) + assert.strictEqual(config.logger, logger) + assert.strictEqual(config.middlewareTracingEnabled, false) + assert.deepStrictEqual(config.peerServiceMapping, { d: 'dd' }) + assertObjectContains(config, { + plugins: false, + port: '6218', + protocolVersion: '0.5', + remoteConfig: { + pollInterval: 42 + }, + reportHostname: true, + runtimeMetrics: { + enabled: true, + eventLoop: false, + gc: false + }, + runtimeMetricsRuntimeId: true, + sampleRate: 0.5 + }) + assert.deepStrictEqual(config.sampler, { rateLimit: 1000, rules: [ { service: 'usersvc', name: 'healthcheck', sampleRate: 0.0 }, @@ -1215,27 +1395,35 @@ describe('Config', () => { { sampleRate: 0.1 } ] }) - expect(config).to.have.property('service', 'service') - expect(config).to.have.deep.property('serviceMapping', { a: 'aa', b: 'bb' }) - expect(config).to.have.property('site', 'datadoghq.eu') - expect(config).to.have.property('spanComputePeerService', true) - expect(config).to.have.property('spanRemoveIntegrationFromService', true) - expect(config).to.have.property('tags') - expect(config.tags).to.have.property('env', 'test') - expect(config.tags).to.have.property('foo', 'bar') - expect(config.tags).to.have.property('runtime-id') - expect(config.tags['runtime-id']).to.match(/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/) - expect(config.tags).to.have.property('service', 'service') - expect(config.tags).to.have.property('version', '0.1.0') - expect(config).to.have.property('traceId128BitGenerationEnabled', true) - expect(config).to.have.property('traceId128BitLoggingEnabled', true) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['datadog']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['datadog']) - expect(config).to.have.property('version', '0.1.0') - - expect(updateConfig).to.be.calledOnce - - expect(updateConfig.getCall(0).args[0]).to.deep.include.members([ + assert.strictEqual(config.service, 'service') + assert.deepStrictEqual(config.serviceMapping, { a: 'aa', b: 'bb' }) + assertObjectContains(config, { + site: 'datadoghq.eu', + spanComputePeerService: true, + spanRemoveIntegrationFromService: true + }) + assert.ok(Object.hasOwn(config, 'tags')) + assertObjectContains(config.tags, { + env: 'test', + foo: 'bar' + }) + 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}$/) + assertObjectContains(config.tags, { + service: 'service', + version: '0.1.0' + }) + assertObjectContains(config, { + traceId128BitGenerationEnabled: true, + traceId128BitLoggingEnabled: true + }) + assert.deepStrictEqual(config.tracePropagationStyle?.extract, ['datadog']) + assert.deepStrictEqual(config.tracePropagationStyle?.inject, ['datadog']) + assert.strictEqual(config.version, '0.1.0') + + sinon.assert.calledOnce(updateConfig) + + assertObjectContains(updateConfig.getCall(0).args[0].sort(comparator), [ { name: 'appsec.enabled', value: false, origin: 'code' }, { name: 'clientIpEnabled', value: true, origin: 'code' }, { name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'code' }, @@ -1298,7 +1486,7 @@ describe('Config', () => { { name: 'traceId128BitGenerationEnabled', value: true, origin: 'code' }, { name: 'traceId128BitLoggingEnabled', value: true, origin: 'code' }, { name: 'version', value: '0.1.0', origin: 'code' } - ].filter(v => v)) + ].filter(v => v).sort(comparator)) }) it('should initialize from the options with url taking precedence', () => { @@ -1319,18 +1507,21 @@ describe('Config', () => { plugins: false }) - expect(config).to.have.nested.property('url.protocol', 'https:') - expect(config).to.have.nested.property('url.hostname', 'agent2') - expect(config).to.have.nested.property('url.port', '7777') - expect(config).to.have.property('site', 'datadoghq.eu') - expect(config).to.have.property('service', 'service') - expect(config).to.have.property('env', 'test') - expect(config).to.have.property('sampleRate', 0.5) - expect(config).to.have.property('logger', logger) - expect(config.tags).to.have.property('foo', 'bar') - expect(config).to.have.property('flushInterval', 5000) - expect(config).to.have.property('flushMinSpans', 500) - expect(config).to.have.property('plugins', false) + assert.strictEqual(config.url.toString(), 'https://agent2:7777/') + + assertObjectContains(config, { + site: 'datadoghq.eu', + service: 'service', + env: 'test', + sampleRate: 0.5 + }) + assert.strictEqual(config.logger, logger) + assert.strictEqual(config.tags?.foo, 'bar') + assertObjectContains(config, { + flushInterval: 5000, + flushMinSpans: 500, + plugins: false + }) }) it('should warn if mixing shared and extract propagation style env vars', () => { @@ -1339,7 +1530,7 @@ describe('Config', () => { getConfig() - expect(log.warn).to.have.been.calledWith('Use either the DD_TRACE_PROPAGATION_STYLE ' + + sinon.assert.calledWith(log.warn, 'Use either the DD_TRACE_PROPAGATION_STYLE ' + 'environment variable or separate DD_TRACE_PROPAGATION_STYLE_INJECT and ' + 'DD_TRACE_PROPAGATION_STYLE_EXTRACT environment variables') }) @@ -1350,7 +1541,7 @@ describe('Config', () => { getConfig() - expect(log.warn).to.have.been.calledWith('Use either the DD_TRACE_PROPAGATION_STYLE ' + + sinon.assert.calledWith(log.warn, 'Use either the DD_TRACE_PROPAGATION_STYLE ' + 'environment variable or separate DD_TRACE_PROPAGATION_STYLE_INJECT and ' + 'DD_TRACE_PROPAGATION_STYLE_EXTRACT environment variables') }) @@ -1360,8 +1551,8 @@ describe('Config', () => { const config = getConfig() - expect(log.warn).to.have.been.calledWith('Unexpected input for config.spanAttributeSchema, picked default', 'v0') - expect(config).to.have.property('spanAttributeSchema', 'v0') + sinon.assert.calledWith(log.warn, 'Unexpected input for config.spanAttributeSchema, picked default', 'v0') + assert.strictEqual(config.spanAttributeSchema, 'v0') }) it('should parse integer range sets', () => { @@ -1370,24 +1561,24 @@ describe('Config', () => { let config = getConfig() - expect(config.grpc.client.error.statuses).to.deep.equal([3, 13, 400, 401, 402, 403]) - expect(config.grpc.server.error.statuses).to.deep.equal([3, 13, 400, 401, 402, 403]) + assert.deepStrictEqual(config.grpc.client.error.statuses, [3, 13, 400, 401, 402, 403]) + assert.deepStrictEqual(config.grpc.server.error.statuses, [3, 13, 400, 401, 402, 403]) process.env.DD_GRPC_CLIENT_ERROR_STATUSES = '1' process.env.DD_GRPC_SERVER_ERROR_STATUSES = '1' config = getConfig() - expect(config.grpc.client.error.statuses).to.deep.equal([1]) - expect(config.grpc.server.error.statuses).to.deep.equal([1]) + assert.deepStrictEqual(config.grpc.client.error.statuses, [1]) + assert.deepStrictEqual(config.grpc.server.error.statuses, [1]) process.env.DD_GRPC_CLIENT_ERROR_STATUSES = '2,10,13-15' process.env.DD_GRPC_SERVER_ERROR_STATUSES = '2,10,13-15' config = getConfig() - expect(config.grpc.client.error.statuses).to.deep.equal([2, 10, 13, 14, 15]) - expect(config.grpc.server.error.statuses).to.deep.equal([2, 10, 13, 14, 15]) + assert.deepStrictEqual(config.grpc.client.error.statuses, [2, 10, 13, 14, 15]) + assert.deepStrictEqual(config.grpc.server.error.statuses, [2, 10, 13, 14, 15]) }) context('peer service tagging', () => { @@ -1395,38 +1586,38 @@ describe('Config', () => { process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = 'v0' process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'true' let config = getConfig() - expect(config).to.have.property('spanComputePeerService', true) + assert.strictEqual(config.spanComputePeerService, true) process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'foo' config = getConfig() - expect(config).to.have.property('spanComputePeerService', false) + assert.strictEqual(config.spanComputePeerService, false) process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'false' config = getConfig() - expect(config).to.have.property('spanComputePeerService', false) + assert.strictEqual(config.spanComputePeerService, false) delete process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED config = getConfig() - expect(config).to.have.property('spanComputePeerService', false) + assert.strictEqual(config.spanComputePeerService, false) }) it('should activate peer service in v1 unless explicitly false', () => { process.env.DD_TRACE_SPAN_ATTRIBUTE_SCHEMA = 'v1' process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'false' let config = getConfig() - expect(config).to.have.property('spanComputePeerService', false) + assert.strictEqual(config.spanComputePeerService, false) process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'foo' config = getConfig() - expect(config).to.have.property('spanComputePeerService', true) + assert.strictEqual(config.spanComputePeerService, true) process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED = 'true' config = getConfig() - expect(config).to.have.property('spanComputePeerService', true) + assert.strictEqual(config.spanComputePeerService, true) delete process.env.DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED config = getConfig() - expect(config).to.have.property('spanComputePeerService', true) + assert.strictEqual(config.spanComputePeerService, true) }) }) @@ -1438,16 +1629,16 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.property('hostname', 'agent') - expect(config.tags).to.include({ foo: 'foo', baz: 'qux' }) + assert.strictEqual(config.hostname, 'agent') + assertObjectContains(config.tags, { foo: 'foo', baz: 'qux' }) }) it('should give priority to the options', () => { process.env.DD_AI_GUARD_ENABLED = 'false' process.env.DD_AI_GUARD_ENDPOINT = 'https://dd.datadog.com/api/unstable/ai-guard' - process.env.DD_AI_GUARD_MAX_CONTENT_SIZE = 512 * 1024 - process.env.DD_AI_GUARD_MAX_MESSAGES_LENGTH = 16 - process.env.DD_AI_GUARD_TIMEOUT = 1_000 + process.env.DD_AI_GUARD_MAX_CONTENT_SIZE = String(512 * 1024) + process.env.DD_AI_GUARD_MAX_MESSAGES_LENGTH = '16' + process.env.DD_AI_GUARD_TIMEOUT = '1000' process.env.DD_API_KEY = '123' process.env.DD_API_SECURITY_ENABLED = 'false' process.env.DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED = 'false' @@ -1639,91 +1830,127 @@ describe('Config', () => { version: '1.0.0' }) - expect(config).to.have.nested.property('apmTracingEnabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.endpointCollectionEnabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.endpointCollectionMessageLimit', 150) - expect(config).to.have.nested.property('appsec.blockedTemplateGraphql', BLOCKED_TEMPLATE_GRAPHQL) - expect(config).to.have.nested.property('appsec.blockedTemplateHtml', BLOCKED_TEMPLATE_HTML) - expect(config).to.have.nested.property('appsec.blockedTemplateJson', BLOCKED_TEMPLATE_JSON) - expect(config).to.have.nested.property('appsec.enabled', true) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'anonymous') - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.enabled', true) - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.maxHeaders', 42) - expect(config).to.have.nested.property('appsec.extendedHeadersCollection.redaction', true) - expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex', '.*') - expect(config).to.have.nested.property('appsec.obfuscatorValueRegex', '.*') - expect(config).to.have.nested.property('appsec.rasp.bodyCollection', true) - expect(config).to.have.nested.property('appsec.rasp.enabled', false) - expect(config).to.have.nested.property('appsec.rateLimit', 42) - expect(config).to.have.nested.property('appsec.rules', RULES_JSON_PATH) - expect(config).to.have.nested.property('appsec.stackTrace.enabled', false) - expect(config).to.have.nested.property('appsec.stackTrace.maxDepth', 42) - expect(config).to.have.nested.property('appsec.stackTrace.maxStackTraces', 5) - expect(config).to.have.nested.property('appsec.wafTimeout', 42) - expect(config).to.have.property('clientIpEnabled', true) - expect(config).to.have.property('clientIpHeader', 'x-true-client-ip') - expect(config).to.have.nested.property('codeOriginForSpans.enabled', true) - expect(config).to.have.nested.property('codeOriginForSpans.experimental.exit_spans.enabled', false) - expect(config).to.have.nested.property('dogstatsd.hostname', 'server') - expect(config).to.have.nested.property('dogstatsd.port', '8888') - expect(config).to.have.nested.property('dynamicInstrumentation.enabled', false) - expect(config).to.have.nested.property('dynamicInstrumentation.probeFile', 'probes2.json') - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactedIdentifiers', ['foo2', 'bar2']) - expect(config).to.have.nested.deep.property('dynamicInstrumentation.redactionExcludedIdentifiers', ['a2', 'b2']) - expect(config).to.have.nested.property('dynamicInstrumentation.uploadIntervalSeconds', 0.2) - expect(config).to.have.property('env', 'development') - expect(config).to.have.nested.property('experimental.aiguard.enabled', true) - expect(config).to.have.nested.property('experimental.aiguard.endpoint', 'https://dd.datad0g.com/api/unstable/ai-guard') - expect(config).to.have.nested.property('experimental.aiguard.maxContentSize', 1024 * 1024) - expect(config).to.have.nested.property('experimental.aiguard.maxMessagesLength', 32) - expect(config).to.have.nested.property('experimental.aiguard.timeout', 2000) - expect(config).to.have.nested.property('experimental.enableGetRumData', false) - expect(config).to.have.nested.property('experimental.exporter', 'agent') - expect(config).to.have.property('flushMinSpans', 500) - expect(config).to.have.property('flushInterval', 500) - expect(config).to.have.nested.property('iast.dbRowsToTaint', 3) - expect(config).to.have.nested.property('iast.deduplicationEnabled', true) - expect(config).to.have.nested.property('iast.enabled', true) - expect(config).to.have.nested.property('iast.maxConcurrentRequests', 2) - expect(config).to.have.nested.property('iast.maxContextOperations', 2) - expect(config).to.have.nested.property('iast.redactionEnabled', true) - expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') - expect(config).to.have.nested.property('iast.redactionValuePattern', 'REDACTION_VALUE_PATTERN') - expect(config).to.have.nested.property('iast.requestSampling', 30) - if (DD_MAJOR < 6) { - expect(config).to.have.nested.property('iast.securityControlsConfiguration', - 'SANITIZER:CODE_INJECTION:sanitizer.js:method2') - } else { - expect(config).to.have.nested.property('iast.securityControlsConfiguration', - 'SANITIZER:CODE_INJECTION:sanitizer.js:method1') - } - expect(config).to.have.nested.property('iast.stackTrace.enabled', false) - expect(config).to.have.nested.property('llmobs.agentlessEnabled', false) - expect(config).to.have.nested.property('llmobs.mlApp', 'myOtherMlApp') - expect(config).to.have.property('middlewareTracingEnabled', true) - expect(config).to.have.deep.property('peerServiceMapping', { d: 'dd' }) - expect(config).to.have.property('protocolVersion', '0.5') - expect(config).to.have.nested.property('remoteConfig.pollInterval', 42) - expect(config).to.have.property('reportHostname', false) - expect(config).to.have.nested.property('runtimeMetrics.enabled', false) - expect(config).to.have.property('runtimeMetricsRuntimeId', false) - expect(config).to.have.property('service', 'test') - expect(config).to.have.deep.property('serviceMapping', { b: 'bb' }) - expect(config).to.have.property('site', 'datadoghq.com') - expect(config).to.have.property('spanAttributeSchema', 'v1') - expect(config).to.have.property('spanComputePeerService', true) - expect(config).to.have.property('spanRemoveIntegrationFromService', true) - expect(config.tags).to.include({ foo: 'foo' }) - expect(config.tags).to.include({ service: 'test', version: '1.0.0', env: 'development' }) - expect(config).to.have.property('traceId128BitGenerationEnabled', false) - expect(config).to.have.property('traceId128BitLoggingEnabled', false) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', []) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', []) - expect(config).to.have.nested.property('url.hostname', 'agent2') - expect(config).to.have.nested.property('url.port', '6218') - expect(config).to.have.nested.property('url.protocol', 'https:') - expect(config).to.have.property('version', '1.0.0') + assertObjectContains(config, { + apmTracingEnabled: true, + appsec: { + apiSecurity: { + enabled: true, + endpointCollectionEnabled: true, + endpointCollectionMessageLimit: 150 + }, + blockedTemplateGraphql: BLOCKED_TEMPLATE_GRAPHQL, + blockedTemplateHtml: BLOCKED_TEMPLATE_HTML, + blockedTemplateJson: BLOCKED_TEMPLATE_JSON, + rules: RULES_JSON_PATH, + enabled: true, + eventTracking: { + mode: 'anonymous' + }, + extendedHeadersCollection: { + enabled: true, + maxHeaders: 42, + redaction: true + }, + obfuscatorKeyRegex: '.*', + obfuscatorValueRegex: '.*', + rasp: { + bodyCollection: true, + enabled: false + }, + rateLimit: 42, + stackTrace: { + enabled: false, + maxDepth: 42, + maxStackTraces: 5 + }, + wafTimeout: 42 + }, + clientIpEnabled: true, + clientIpHeader: 'x-true-client-ip', + codeOriginForSpans: { + enabled: true, + experimental: { + exit_spans: { + enabled: false + } + } + }, + dogstatsd: { + hostname: 'server', + port: '8888' + }, + dynamicInstrumentation: { + enabled: false, + probeFile: 'probes2.json', + redactedIdentifiers: ['foo2', 'bar2'], + redactionExcludedIdentifiers: ['a2', 'b2'], + uploadIntervalSeconds: 0.2 + }, + env: 'development', + experimental: { + aiguard: { + enabled: true, + endpoint: 'https://dd.datad0g.com/api/unstable/ai-guard', + maxContentSize: 1024 * 1024, + maxMessagesLength: 32, + timeout: 2000 + }, + enableGetRumData: false, + exporter: 'agent' + }, + flushMinSpans: 500, + flushInterval: 500, + iast: { + dbRowsToTaint: 3, + deduplicationEnabled: true, + enabled: true, + maxConcurrentRequests: 2, + maxContextOperations: 2, + redactionEnabled: true, + redactionNamePattern: 'REDACTION_NAME_PATTERN', + redactionValuePattern: 'REDACTION_VALUE_PATTERN', + requestSampling: 30, + securityControlsConfiguration: 'SANITIZER:CODE_INJECTION:sanitizer.js:method' + (DD_MAJOR < 6 ? '2' : '1'), + stackTrace: { + enabled: false + } + }, + llmobs: { + agentlessEnabled: false, + mlApp: 'myOtherMlApp' + }, + middlewareTracingEnabled: true, + peerServiceMapping: { d: 'dd' }, + protocolVersion: '0.5', + remoteConfig: { + pollInterval: 42 + }, + reportHostname: false, + runtimeMetrics: { + enabled: false + }, + runtimeMetricsRuntimeId: false, + service: 'test', + site: 'datadoghq.com', + spanAttributeSchema: 'v1', + spanComputePeerService: true, + spanRemoveIntegrationFromService: true, + traceId128BitGenerationEnabled: false, + traceId128BitLoggingEnabled: false, + version: '1.0.0', + serviceMapping: { b: 'bb' }, + tags: { + foo: 'foo', + service: 'test', + version: '1.0.0', + env: 'development' + }, + tracePropagationStyle: { + extract: [], + inject: [] + } + }) + assert.strictEqual(config.url.toString(), 'https://agent2:6218/') }) it('should give priority to non-experimental options', () => { @@ -1818,7 +2045,7 @@ describe('Config', () => { } }) - expect(config).to.have.deep.property('appsec', { + assert.deepStrictEqual(config.appsec, { apiSecurity: { enabled: true, sampleDelay: 30, @@ -1856,7 +2083,7 @@ describe('Config', () => { wafTimeout: 42 }) - expect(config).to.have.deep.property('iast', { + assert.deepStrictEqual(config.iast, { dbRowsToTaint: 3, deduplicationEnabled: false, enabled: true, @@ -1890,11 +2117,12 @@ describe('Config', () => { env: 'development' }) - expect(config).to.have.nested.property('url.protocol', 'https:') - expect(config).to.have.nested.property('url.hostname', 'agent3') - expect(config).to.have.nested.property('url.port', '7778') - expect(config).to.have.property('service', 'test') - expect(config).to.have.property('env', 'development') + assert.strictEqual(config.url.toString(), 'https://agent3:7778/') + + assertObjectContains(config, { + service: 'test', + env: 'development' + }) }) it('should give priority to individual options over tags', () => { @@ -1905,7 +2133,7 @@ describe('Config', () => { const config = getConfig() - expect(config.tags).to.include({ + assertObjectContains(config.tags, { service: 'test', env: 'dev', version: '1.0.0' @@ -1913,9 +2141,9 @@ describe('Config', () => { }) it('should sanitize the sample rate to be between 0 and 1', () => { - expect(getConfig({ sampleRate: -1 })).to.have.property('sampleRate', 0) - expect(getConfig({ sampleRate: 2 })).to.have.property('sampleRate', 1) - expect(getConfig({ sampleRate: NaN })).to.have.property('sampleRate', undefined) + assert.strictEqual(getConfig({ sampleRate: -1 })?.sampleRate, 0) + assert.strictEqual(getConfig({ sampleRate: 2 })?.sampleRate, 1) + assert.strictEqual(getConfig({ sampleRate: NaN })?.sampleRate, undefined) }) it('should ignore empty service names', () => { @@ -1923,7 +2151,7 @@ describe('Config', () => { const config = getConfig() - expect(config.tags).to.include({ + assertObjectContains(config.tags, { service: 'node' }) }) @@ -1937,9 +2165,11 @@ describe('Config', () => { } }) - expect(config).to.have.property('service', 'service') - expect(config).to.have.property('version', '0.1.0') - expect(config).to.have.property('env', 'test') + assertObjectContains(config, { + service: 'service', + version: '0.1.0', + env: 'test' + }) }) it('should trim whitespace characters around keys', () => { @@ -1947,14 +2177,14 @@ describe('Config', () => { const config = getConfig() - expect(config.tags).to.include({ foo: 'bar', baz: 'qux' }) + assertObjectContains(config.tags, { foo: 'bar', baz: 'qux' }) }) it('should not transform the lookup parameter', () => { const lookup = () => 'test' const config = getConfig({ lookup }) - expect(config.lookup).to.equal(lookup) + assert.strictEqual(config.lookup, lookup) }) it('should not set DD_INSTRUMENTATION_TELEMETRY_ENABLED if AWS_LAMBDA_FUNCTION_NAME is present', () => { @@ -1962,7 +2192,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.enabled).to.be.false + assert.strictEqual(config.telemetry.enabled, false) }) it('should not set DD_INSTRUMENTATION_TELEMETRY_ENABLED if FUNCTION_NAME and GCP_PROJECT are present', () => { @@ -1972,7 +2202,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.enabled).to.be.false + assert.strictEqual(config.telemetry.enabled, false) }) it('should not set DD_INSTRUMENTATION_TELEMETRY_ENABLED if K_SERVICE and FUNCTION_TARGET are present', () => { @@ -1982,7 +2212,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.enabled).to.be.false + assert.strictEqual(config.telemetry.enabled, false) }) it('should not set DD_INSTRUMENTATION_TELEMETRY_ENABLED if Azure Consumption Plan Function', () => { @@ -1993,18 +2223,18 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.enabled).to.be.false + assert.strictEqual(config.telemetry.enabled, false) }) it('should set telemetry default values', () => { const config = getConfig() - expect(config.telemetry).to.not.be.undefined - expect(config.telemetry.enabled).to.be.true - expect(config.telemetry.heartbeatInterval).to.eq(60000) - expect(config.telemetry.logCollection).to.be.true - expect(config.telemetry.debug).to.be.false - expect(config.telemetry.metrics).to.be.true + assert.notStrictEqual(config.telemetry, undefined) + assert.strictEqual(config.telemetry.enabled, true) + assert.strictEqual(config.telemetry.heartbeatInterval, 60000) + assert.strictEqual(config.telemetry.logCollection, true) + assert.strictEqual(config.telemetry.debug, false) + assert.strictEqual(config.telemetry.metrics, true) }) it('should set DD_TELEMETRY_HEARTBEAT_INTERVAL', () => { @@ -2013,7 +2243,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.heartbeatInterval).to.eq(42000) + assert.strictEqual(config.telemetry.heartbeatInterval, 42000) process.env.DD_TELEMETRY_HEARTBEAT_INTERVAL = origTelemetryHeartbeatIntervalValue }) @@ -2024,7 +2254,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.enabled).to.be.false + assert.strictEqual(config.telemetry.enabled, false) process.env.DD_INSTRUMENTATION_TELEMETRY_ENABLED = origTraceTelemetryValue }) @@ -2035,7 +2265,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.metrics).to.be.false + assert.strictEqual(config.telemetry.metrics, false) process.env.DD_TELEMETRY_METRICS_ENABLED = origTelemetryMetricsEnabledValue }) @@ -2046,7 +2276,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.logCollection).to.be.false + assert.strictEqual(config.telemetry.logCollection, false) process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED = origLogsValue }) @@ -2057,7 +2287,7 @@ describe('Config', () => { const config = getConfig() - expect(config.telemetry.debug).to.be.true + assert.strictEqual(config.telemetry.debug, true) process.env.DD_TELEMETRY_DEBUG = origTelemetryDebugValue }) @@ -2067,7 +2297,7 @@ describe('Config', () => { const config = getConfig() - expect(config.remoteConfig.enabled).to.be.false + assert.strictEqual(config.remoteConfig.enabled, false) }) it('should not set DD_REMOTE_CONFIGURATION_ENABLED if FUNCTION_NAME and GCP_PROJECT are present', () => { @@ -2076,7 +2306,7 @@ describe('Config', () => { const config = getConfig() - expect(config.remoteConfig.enabled).to.be.false + assert.strictEqual(config.remoteConfig.enabled, false) }) it('should not set DD_REMOTE_CONFIGURATION_ENABLED if K_SERVICE and FUNCTION_TARGET are present', () => { @@ -2085,7 +2315,7 @@ describe('Config', () => { const config = getConfig() - expect(config.remoteConfig.enabled).to.be.false + assert.strictEqual(config.remoteConfig.enabled, false) }) it('should not set DD_REMOTE_CONFIGURATION_ENABLED if Azure Functions env vars are present', () => { @@ -2095,7 +2325,7 @@ describe('Config', () => { const config = getConfig() - expect(config.remoteConfig.enabled).to.be.false + assert.strictEqual(config.remoteConfig.enabled, false) }) it('should send empty array when remote config is called on empty options', () => { @@ -2103,8 +2333,8 @@ describe('Config', () => { config.configure({}, true) - expect(updateConfig).to.be.calledTwice - expect(updateConfig.getCall(1).args[0]).to.deep.equal([]) + sinon.assert.calledTwice(updateConfig) + assert.deepStrictEqual(updateConfig.getCall(1).args[0], []) }) it('should send remote config changes to telemetry', () => { @@ -2114,7 +2344,7 @@ describe('Config', () => { tracing_sampling_rate: 0 }, true) - expect(updateConfig.getCall(1).args[0]).to.deep.equal([ + assert.deepStrictEqual(updateConfig.getCall(1).args[0], [ { name: 'sampleRate', value: 0, origin: 'remote_config' } ]) }) @@ -2134,7 +2364,7 @@ describe('Config', () => { } ] }, true) - expect(config).to.have.deep.nested.property('sampler', { + assert.deepStrictEqual(config.sampler, { spanSamplingRules: [], rateLimit: 100, rules: [ @@ -2155,8 +2385,8 @@ describe('Config', () => { tracing_tags: { foo: 'bar' } }, true) - expect(config.tags).to.have.property('foo', 'bar') - expect(config.tags).to.have.property('runtime-id', runtimeId) + assert.strictEqual(config.tags?.foo, 'bar') + assert.strictEqual(config.tags?.['runtime-id'], runtimeId) }) it('should ignore invalid iast.requestSampling', () => { @@ -2167,7 +2397,7 @@ describe('Config', () => { } } }) - expect(config.iast.requestSampling).to.be.equals(30) + assert.strictEqual(config.iast.requestSampling, 30) }) it('should load span sampling rules from json file', () => { @@ -2176,7 +2406,7 @@ describe('Config', () => { const config = getConfig() - expect(config.sampler).to.have.deep.nested.property('spanSamplingRules', [ + assert.deepStrictEqual(config.sampler?.spanSamplingRules, [ { service: 'mysql', name: 'mysql.query', sampleRate: 0.0, maxPerSecond: 1 }, { service: 'mysql', sampleRate: 0.5 }, { service: 'mysql', sampleRate: 1.0 }, @@ -2198,19 +2428,16 @@ describe('Config', () => { } }) - expect(log.error).to.be.callCount(3) - expect(log.error.firstCall) - .to.have.been.calledWithExactly('Error reading file %s', 'DOES_NOT_EXIST.json', error) - expect(log.error.secondCall) - .to.have.been.calledWithExactly('Error reading file %s', 'DOES_NOT_EXIST.html', error) - expect(log.error.thirdCall) - .to.have.been.calledWithExactly('Error reading file %s', 'DOES_NOT_EXIST.json', error) + sinon.assert.callCount(log.error, 3) + sinon.assert.calledWithExactly(log.error.firstCall, 'Error reading file %s', 'DOES_NOT_EXIST.json', error) + sinon.assert.calledWithExactly(log.error.secondCall, 'Error reading file %s', 'DOES_NOT_EXIST.html', error) + sinon.assert.calledWithExactly(log.error.thirdCall, 'Error reading file %s', 'DOES_NOT_EXIST.json', error) - expect(config.appsec.enabled).to.be.true - expect(config.appsec.rules).to.eq('path/to/rules.json') - expect(config.appsec.blockedTemplateHtml).to.be.undefined - expect(config.appsec.blockedTemplateJson).to.be.undefined - expect(config.appsec.blockedTemplateGraphql).to.be.undefined + assert.strictEqual(config.appsec.enabled, true) + assert.strictEqual(config.appsec.rules, 'path/to/rules.json') + assert.strictEqual(config.appsec.blockedTemplateHtml, undefined) + assert.strictEqual(config.appsec.blockedTemplateJson, undefined) + assert.strictEqual(config.appsec.blockedTemplateGraphql, undefined) }) it('should enable api security with DD_EXPERIMENTAL_API_SECURITY_ENABLED', () => { @@ -2218,7 +2445,7 @@ describe('Config', () => { const config = getConfig() - expect(config.appsec.apiSecurity.enabled).to.be.true + assert.strictEqual(config.appsec.apiSecurity.enabled, true) }) it('should disable api security with DD_EXPERIMENTAL_API_SECURITY_ENABLED', () => { @@ -2226,7 +2453,7 @@ describe('Config', () => { const config = getConfig() - expect(config.appsec.apiSecurity.enabled).to.be.false + assert.strictEqual(config.appsec.apiSecurity.enabled, false) }) it('should ignore DD_EXPERIMENTAL_API_SECURITY_ENABLED with DD_API_SECURITY_ENABLED=true', () => { @@ -2235,7 +2462,7 @@ describe('Config', () => { const config = getConfig() - expect(config.appsec.apiSecurity.enabled).to.be.true + assert.strictEqual(config.appsec.apiSecurity.enabled, true) }) it('should prioritize DD_DOGSTATSD_HOST over DD_DOGSTATSD_HOSTNAME', () => { @@ -2244,7 +2471,7 @@ describe('Config', () => { const config = getConfig() - expect(config).to.have.nested.property('dogstatsd.hostname', 'localhost') + assert.strictEqual(config.dogstatsd?.hostname, 'localhost') }) context('auto configuration w/ unix domain sockets', () => { @@ -2253,14 +2480,14 @@ describe('Config', () => { osType = 'Windows_NT' const config = getConfig() - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) }) context('socket does not exist', () => { it('should not be used', () => { const config = getConfig() - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) }) context('socket exists', () => { @@ -2271,8 +2498,8 @@ describe('Config', () => { it('should be used when no options and no env vars', () => { const config = getConfig() - expect(existsSyncParam).to.equal('/var/run/datadog/apm.socket') - expect(config.url.toString()).to.equal('unix:///var/run/datadog/apm.socket') + assert.strictEqual(existsSyncParam, '/var/run/datadog/apm.socket') + assert.strictEqual(config.url.toString(), 'unix:///var/run/datadog/apm.socket') }) it('should not be used when DD_TRACE_AGENT_URL provided', () => { @@ -2280,7 +2507,7 @@ describe('Config', () => { const config = getConfig() - expect(config.url.toString()).to.equal('https://example.com/') + assert.strictEqual(config.url.toString(), 'https://example.com/') }) it('should not be used when DD_TRACE_URL provided', () => { @@ -2288,13 +2515,13 @@ describe('Config', () => { const config = getConfig() - expect(config.url.toString()).to.equal('https://example.com/') + assert.strictEqual(config.url.toString(), 'https://example.com/') }) it('should not be used when options.url provided', () => { const config = getConfig({ url: 'https://example.com/' }) - expect(config.url.toString()).to.equal('https://example.com/') + assert.strictEqual(config.url.toString(), 'https://example.com/') }) it('should not be used when DD_TRACE_AGENT_PORT provided', () => { @@ -2302,13 +2529,13 @@ describe('Config', () => { const config = getConfig() - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) it('should not be used when options.port provided', () => { const config = getConfig({ port: 12345 }) - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) it('should not be used when DD_TRACE_AGENT_HOSTNAME provided', () => { @@ -2316,7 +2543,7 @@ describe('Config', () => { const config = getConfig() - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) it('should not be used when DD_AGENT_HOST provided', () => { @@ -2324,13 +2551,13 @@ describe('Config', () => { const config = getConfig() - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) it('should not be used when options.hostname provided', () => { const config = getConfig({ hostname: 'example.com' }) - expect(config.url).to.be.undefined + assert.strictEqual(config.url, undefined) }) }) }) @@ -2356,104 +2583,104 @@ describe('Config', () => { }) it('should activate git upload by default', () => { const config = getConfig(options) - expect(config).to.have.property('isGitUploadEnabled', true) + assert.strictEqual(config.isGitUploadEnabled, true) }) it('should disable git upload if the DD_CIVISIBILITY_GIT_UPLOAD_ENABLED is set to false', () => { process.env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED = 'false' const config = getConfig(options) - expect(config).to.have.property('isGitUploadEnabled', false) + assert.strictEqual(config.isGitUploadEnabled, false) }) it('should activate ITR by default', () => { const config = getConfig(options) - expect(config).to.have.property('isIntelligentTestRunnerEnabled', true) + assert.strictEqual(config.isIntelligentTestRunnerEnabled, true) }) it('should disable ITR if DD_CIVISIBILITY_ITR_ENABLED is set to false', () => { process.env.DD_CIVISIBILITY_ITR_ENABLED = 'false' const config = getConfig(options) - expect(config).to.have.property('isIntelligentTestRunnerEnabled', false) + assert.strictEqual(config.isIntelligentTestRunnerEnabled, false) }) it('should enable manual testing API by default', () => { const config = getConfig(options) - expect(config).to.have.property('isManualApiEnabled', true) + assert.strictEqual(config.isManualApiEnabled, true) }) it('should disable manual testing API if DD_CIVISIBILITY_MANUAL_API_ENABLED is set to false', () => { process.env.DD_CIVISIBILITY_MANUAL_API_ENABLED = 'false' const config = getConfig(options) - expect(config).to.have.property('isManualApiEnabled', false) + assert.strictEqual(config.isManualApiEnabled, false) }) it('should disable memcached command tagging by default', () => { const config = getConfig(options) - expect(config).to.have.property('memcachedCommandEnabled', false) + assert.strictEqual(config.memcachedCommandEnabled, false) }) it('should enable memcached command tagging if DD_TRACE_MEMCACHED_COMMAND_ENABLED is enabled', () => { process.env.DD_TRACE_MEMCACHED_COMMAND_ENABLED = 'true' const config = getConfig(options) - expect(config).to.have.property('memcachedCommandEnabled', true) + assert.strictEqual(config.memcachedCommandEnabled, true) }) it('should enable telemetry', () => { const config = getConfig(options) - expect(config).to.nested.property('telemetry.enabled', true) + assert.strictEqual(config.telemetry?.enabled, true) }) it('should enable early flake detection by default', () => { const config = getConfig(options) - expect(config).to.have.property('isEarlyFlakeDetectionEnabled', true) + assert.strictEqual(config.isEarlyFlakeDetectionEnabled, true) }) it('should disable early flake detection if DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED is false', () => { process.env.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED = 'false' const config = getConfig(options) - expect(config).to.have.property('isEarlyFlakeDetectionEnabled', false) + assert.strictEqual(config.isEarlyFlakeDetectionEnabled, false) }) it('should enable flaky test retries by default', () => { const config = getConfig(options) - expect(config).to.have.property('isFlakyTestRetriesEnabled', true) + assert.strictEqual(config.isFlakyTestRetriesEnabled, true) }) it('should disable flaky test retries if isFlakyTestRetriesEnabled is false', () => { process.env.DD_CIVISIBILITY_FLAKY_RETRY_ENABLED = 'false' const config = getConfig(options) - expect(config).to.have.property('isFlakyTestRetriesEnabled', false) + assert.strictEqual(config.isFlakyTestRetriesEnabled, false) }) it('should read DD_CIVISIBILITY_FLAKY_RETRY_COUNT if present', () => { process.env.DD_CIVISIBILITY_FLAKY_RETRY_COUNT = '4' const config = getConfig(options) - expect(config).to.have.property('flakyTestRetriesCount', 4) + assert.strictEqual(config.flakyTestRetriesCount, 4) }) it('should default DD_CIVISIBILITY_FLAKY_RETRY_COUNT to 5', () => { const config = getConfig(options) - expect(config).to.have.property('flakyTestRetriesCount', 5) + assert.strictEqual(config.flakyTestRetriesCount, 5) }) it('should round non integer values of DD_CIVISIBILITY_FLAKY_RETRY_COUNT', () => { process.env.DD_CIVISIBILITY_FLAKY_RETRY_COUNT = '4.1' const config = getConfig(options) - expect(config).to.have.property('flakyTestRetriesCount', 4) + assert.strictEqual(config.flakyTestRetriesCount, 4) }) it('should set the default to DD_CIVISIBILITY_FLAKY_RETRY_COUNT if it is not a number', () => { process.env.DD_CIVISIBILITY_FLAKY_RETRY_COUNT = 'a' const config = getConfig(options) - expect(config).to.have.property('flakyTestRetriesCount', 5) + assert.strictEqual(config.flakyTestRetriesCount, 5) }) it('should set the session name if DD_TEST_SESSION_NAME is set', () => { process.env.DD_TEST_SESSION_NAME = 'my-test-session' const config = getConfig(options) - expect(config).to.have.property('ciVisibilityTestSessionName', 'my-test-session') + assert.strictEqual(config.ciVisibilityTestSessionName, 'my-test-session') }) it('should not enable agentless log submission by default', () => { const config = getConfig(options) - expect(config).to.have.property('ciVisAgentlessLogSubmissionEnabled', false) + assert.strictEqual(config.ciVisAgentlessLogSubmissionEnabled, false) }) it('should enable agentless log submission if DD_AGENTLESS_LOG_SUBMISSION_ENABLED is true', () => { process.env.DD_AGENTLESS_LOG_SUBMISSION_ENABLED = 'true' const config = getConfig(options) - expect(config).to.have.property('ciVisAgentlessLogSubmissionEnabled', true) + assert.strictEqual(config.ciVisAgentlessLogSubmissionEnabled, true) }) it('should set isTestDynamicInstrumentationEnabled by default', () => { const config = getConfig(options) - expect(config).to.have.property('isTestDynamicInstrumentationEnabled', true) + assert.strictEqual(config.isTestDynamicInstrumentationEnabled, true) }) it('should set isTestDynamicInstrumentationEnabled to false if DD_TEST_FAILED_TEST_REPLAY_ENABLED is false', () => { process.env.DD_TEST_FAILED_TEST_REPLAY_ENABLED = 'false' const config = getConfig(options) - expect(config).to.have.property('isTestDynamicInstrumentationEnabled', false) + assert.strictEqual(config.isTestDynamicInstrumentationEnabled, false) }) }) context('ci visibility mode is not enabled', () => { @@ -2461,14 +2688,16 @@ describe('Config', () => { process.env.DD_CIVISIBILITY_ITR_ENABLED = 'true' process.env.DD_CIVISIBILITY_GIT_UPLOAD_ENABLED = 'true' const config = getConfig(options) - expect(config).to.have.property('isIntelligentTestRunnerEnabled', false) - expect(config).to.have.property('isGitUploadEnabled', false) + assertObjectContains(config, { + isIntelligentTestRunnerEnabled: false, + isGitUploadEnabled: false + }) }) }) it('disables telemetry if inside a jest worker', () => { process.env.JEST_WORKER_ID = '1' const config = getConfig(options) - expect(config.telemetry.enabled).to.be.false + assert.strictEqual(config.telemetry.enabled, false) }) }) @@ -2493,143 +2722,149 @@ describe('Config', () => { process.env.DD_GIT_COMMIT_SHA = DUMMY_COMMIT_SHA process.env.DD_GIT_REPOSITORY_URL = DUMMY_REPOSITORY_URL const config = getConfig({}) - expect(config).to.have.property('commitSHA', DUMMY_COMMIT_SHA) - expect(config).to.have.property('repositoryUrl', DUMMY_REPOSITORY_URL) + assert.strictEqual(config.commitSHA, DUMMY_COMMIT_SHA) + assert.strictEqual(config.repositoryUrl, DUMMY_REPOSITORY_URL) }) it('reads DD_GIT_* env vars and filters out user data', () => { process.env.DD_GIT_REPOSITORY_URL = 'https://user:password@github.com/DataDog/dd-trace-js.git' const config = getConfig({}) - expect(config).to.have.property('repositoryUrl', 'https://github.com/DataDog/dd-trace-js.git') + assert.strictEqual(config.repositoryUrl, 'https://github.com/DataDog/dd-trace-js.git') }) it('reads DD_TAGS env var', () => { process.env.DD_TAGS = `git.commit.sha:${DUMMY_COMMIT_SHA},git.repository_url:${DUMMY_REPOSITORY_URL}` process.env.DD_GIT_REPOSITORY_URL = DUMMY_REPOSITORY_URL const config = getConfig({}) - expect(config).to.have.property('commitSHA', DUMMY_COMMIT_SHA) - expect(config).to.have.property('repositoryUrl', DUMMY_REPOSITORY_URL) + assert.strictEqual(config.commitSHA, DUMMY_COMMIT_SHA) + assert.strictEqual(config.repositoryUrl, DUMMY_REPOSITORY_URL) }) it('reads git.properties if it is available', () => { process.env.DD_GIT_PROPERTIES_FILE = DD_GIT_PROPERTIES_FILE const config = getConfig({}) - expect(config).to.have.property('commitSHA', '4e7da8069bcf5ffc8023603b95653e2dc99d1c7d') - expect(config).to.have.property('repositoryUrl', DUMMY_REPOSITORY_URL) + assert.strictEqual(config.commitSHA, '4e7da8069bcf5ffc8023603b95653e2dc99d1c7d') + assert.strictEqual(config.repositoryUrl, DUMMY_REPOSITORY_URL) }) it('does not crash if git.properties is not available', () => { process.env.DD_GIT_PROPERTIES_FILE = '/does/not/exist' // Should not throw const config = getConfig({}) - expect(config).to.be.an('object') + assert.ok(config !== null && typeof config === 'object' && !Array.isArray(config)) }) it('does not read git.properties if env vars are passed', () => { process.env.DD_GIT_PROPERTIES_FILE = DD_GIT_PROPERTIES_FILE process.env.DD_GIT_COMMIT_SHA = DUMMY_COMMIT_SHA process.env.DD_GIT_REPOSITORY_URL = 'https://github.com:DataDog/dd-trace-js.git' const config = getConfig({}) - expect(config).to.have.property('commitSHA', DUMMY_COMMIT_SHA) - expect(config).to.have.property('repositoryUrl', 'https://github.com:DataDog/dd-trace-js.git') + assert.strictEqual(config.commitSHA, DUMMY_COMMIT_SHA) + assert.strictEqual(config.repositoryUrl, 'https://github.com:DataDog/dd-trace-js.git') }) it('still reads git.properties if one of the env vars is missing', () => { process.env.DD_GIT_PROPERTIES_FILE = DD_GIT_PROPERTIES_FILE process.env.DD_GIT_COMMIT_SHA = DUMMY_COMMIT_SHA const config = getConfig({}) - expect(config).to.have.property('commitSHA', DUMMY_COMMIT_SHA) - expect(config).to.have.property('repositoryUrl', DUMMY_REPOSITORY_URL) + assert.strictEqual(config.commitSHA, DUMMY_COMMIT_SHA) + assert.strictEqual(config.repositoryUrl, DUMMY_REPOSITORY_URL) }) it('reads git.properties and filters out credentials', () => { process.env.DD_GIT_PROPERTIES_FILE = require.resolve('./fixtures/config/git.properties.credentials') const config = getConfig({}) - expect(config).to.have.property('commitSHA', '4e7da8069bcf5ffc8023603b95653e2dc99d1c7d') - expect(config).to.have.property('repositoryUrl', 'https://github.com/datadog/dd-trace-js') + assertObjectContains(config, { + commitSHA: '4e7da8069bcf5ffc8023603b95653e2dc99d1c7d', + repositoryUrl: 'https://github.com/datadog/dd-trace-js' + }) }) it('does not read git metadata if DD_TRACE_GIT_METADATA_ENABLED is false', () => { process.env.DD_TRACE_GIT_METADATA_ENABLED = 'false' const config = getConfig({}) - expect(config).not.to.have.property('commitSHA') - expect(config).not.to.have.property('repositoryUrl') + assert.ok(!(Object.hasOwn(config, 'commitSHA'))) + assert.ok(!(Object.hasOwn(config, 'repositoryUrl'))) }) it('reads .git/ folder if it is available', () => { process.env.DD_GIT_FOLDER_PATH = DD_GIT_FOLDER_PATH const config = getConfig({}) - expect(config).to.have.property('repositoryUrl', 'git@github.com:DataDog/dd-trace-js.git') - expect(config).to.have.property('commitSHA', '964886d9ec0c9fc68778e4abb0aab4d9982ce2b5') + assertObjectContains(config, { + repositoryUrl: 'git@github.com:DataDog/dd-trace-js.git', + commitSHA: '964886d9ec0c9fc68778e4abb0aab4d9982ce2b5' + }) }) it('does not crash if .git/ folder is not available', () => { process.env.DD_GIT_FOLDER_PATH = '/does/not/exist/' // Should not throw const config = getConfig({}) - expect(config).to.be.an('object') + assert.ok(config !== null && typeof config === 'object' && !Array.isArray(config)) }) it('does not read .git/ folder if env vars are passed', () => { process.env.DD_GIT_FOLDER_PATH = DD_GIT_FOLDER_PATH process.env.DD_GIT_COMMIT_SHA = DUMMY_COMMIT_SHA process.env.DD_GIT_REPOSITORY_URL = 'https://github.com:DataDog/dd-trace-js.git' const config = getConfig({}) - expect(config).to.have.property('commitSHA', DUMMY_COMMIT_SHA) - expect(config).to.have.property('repositoryUrl', 'https://github.com:DataDog/dd-trace-js.git') + assert.strictEqual(config.commitSHA, DUMMY_COMMIT_SHA) + assert.strictEqual(config.repositoryUrl, 'https://github.com:DataDog/dd-trace-js.git') }) it('still reads .git/ if one of the env vars is missing', () => { process.env.DD_GIT_FOLDER_PATH = DD_GIT_FOLDER_PATH process.env.DD_GIT_REPOSITORY_URL = 'git@github.com:DataDog/dummy-dd-trace-js.git' const config = getConfig({}) - expect(config).to.have.property('commitSHA', '964886d9ec0c9fc68778e4abb0aab4d9982ce2b5') - expect(config).to.have.property('repositoryUrl', 'git@github.com:DataDog/dummy-dd-trace-js.git') + assertObjectContains(config, { + commitSHA: '964886d9ec0c9fc68778e4abb0aab4d9982ce2b5', + repositoryUrl: 'git@github.com:DataDog/dummy-dd-trace-js.git' + }) }) }) context('llmobs config', () => { it('should disable llmobs by default', () => { const config = getConfig() - expect(config.llmobs.enabled).to.be.false + assert.strictEqual(config.llmobs.enabled, false) // check origin computation - expect(updateConfig.getCall(0).args[0]).to.deep.include({ + assertObjectContains(updateConfig.getCall(0).args[0], [{ name: 'llmobs.enabled', value: false, origin: 'default' - }) + }]) }) it('should enable llmobs if DD_LLMOBS_ENABLED is set to true', () => { process.env.DD_LLMOBS_ENABLED = 'true' const config = getConfig() - expect(config.llmobs.enabled).to.be.true + assert.strictEqual(config.llmobs.enabled, true) // check origin computation - expect(updateConfig.getCall(0).args[0]).to.deep.include({ + assertObjectContains(updateConfig.getCall(0).args[0], [{ name: 'llmobs.enabled', value: true, origin: 'env_var' - }) + }]) }) it('should disable llmobs if DD_LLMOBS_ENABLED is set to false', () => { process.env.DD_LLMOBS_ENABLED = 'false' const config = getConfig() - expect(config.llmobs.enabled).to.be.false + assert.strictEqual(config.llmobs.enabled, false) // check origin computation - expect(updateConfig.getCall(0).args[0]).to.deep.include({ + assertObjectContains(updateConfig.getCall(0).args[0], [{ name: 'llmobs.enabled', value: false, origin: 'env_var' - }) + }]) }) it('should enable llmobs with options and DD_LLMOBS_ENABLED is not set', () => { const config = getConfig({ llmobs: {} }) - expect(config.llmobs.enabled).to.be.true + assert.strictEqual(config.llmobs.enabled, true) // check origin computation - expect(updateConfig.getCall(0).args[0]).to.deep.include({ + assertObjectContains(updateConfig.getCall(0).args[0], [{ name: 'llmobs.enabled', value: true, origin: 'code' - }) + }]) }) it('should have DD_LLMOBS_ENABLED take priority over options', () => { process.env.DD_LLMOBS_ENABLED = 'false' const config = getConfig({ llmobs: {} }) - expect(config.llmobs.enabled).to.be.false + assert.strictEqual(config.llmobs.enabled, false) // check origin computation - expect(updateConfig.getCall(0).args[0]).to.deep.include({ + assertObjectContains(updateConfig.getCall(0).args[0], [{ name: 'llmobs.enabled', value: false, origin: 'env_var' - }) + }]) }) }) @@ -2648,56 +2883,70 @@ describe('Config', () => { it('defaults', () => { const taggingConfig = getConfig().cloudPayloadTagging - expect(taggingConfig).to.have.property('requestsEnabled', false) - expect(taggingConfig).to.have.property('responsesEnabled', false) - expect(taggingConfig).to.have.property('maxDepth', 10) + assertObjectContains(taggingConfig, { + requestsEnabled: false, + responsesEnabled: false, + maxDepth: 10 + }) }) it('enabling requests with no additional filter', () => { process.env.DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING = 'all' const taggingConfig = getConfig().cloudPayloadTagging - expect(taggingConfig).to.have.property('requestsEnabled', true) - expect(taggingConfig).to.have.property('responsesEnabled', false) - expect(taggingConfig).to.have.property('maxDepth', 10) + assertObjectContains(taggingConfig, { + requestsEnabled: true, + responsesEnabled: false, + maxDepth: 10 + }) const awsRules = taggingConfig.rules.aws for (const [serviceName, service] of Object.entries(awsRules)) { - expect(service.request).to.deep.equal(staticConfig[serviceName].request) + assert.deepStrictEqual(service.request, staticConfig[serviceName].request) } }) it('enabling requests with an additional filter', () => { process.env.DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING = '$.foo.bar' const taggingConfig = getConfig().cloudPayloadTagging - expect(taggingConfig).to.have.property('requestsEnabled', true) - expect(taggingConfig).to.have.property('responsesEnabled', false) - expect(taggingConfig).to.have.property('maxDepth', 10) + assertObjectContains(taggingConfig, { + requestsEnabled: true, + responsesEnabled: false, + maxDepth: 10 + }) const awsRules = taggingConfig.rules.aws for (const [, service] of Object.entries(awsRules)) { - expect(service.request).to.include('$.foo.bar') + assertObjectContains(service, { + request: ['$.foo.bar'], + }) } }) it('enabling responses with no additional filter', () => { process.env.DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING = 'all' const taggingConfig = getConfig().cloudPayloadTagging - expect(taggingConfig).to.have.property('requestsEnabled', false) - expect(taggingConfig).to.have.property('responsesEnabled', true) - expect(taggingConfig).to.have.property('maxDepth', 10) + assertObjectContains(taggingConfig, { + requestsEnabled: false, + responsesEnabled: true, + maxDepth: 10 + }) const awsRules = taggingConfig.rules.aws for (const [serviceName, service] of Object.entries(awsRules)) { - expect(service.response).to.deep.equal(staticConfig[serviceName].response) + assert.deepStrictEqual(service.response, staticConfig[serviceName].response) } }) it('enabling responses with an additional filter', () => { process.env.DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING = '$.foo.bar' const taggingConfig = getConfig().cloudPayloadTagging - expect(taggingConfig).to.have.property('requestsEnabled', false) - expect(taggingConfig).to.have.property('responsesEnabled', true) - expect(taggingConfig).to.have.property('maxDepth', 10) + assertObjectContains(taggingConfig, { + requestsEnabled: false, + responsesEnabled: true, + maxDepth: 10 + }) const awsRules = taggingConfig.rules.aws for (const [, service] of Object.entries(awsRules)) { - expect(service.response).to.include('$.foo.bar') + assertObjectContains(service, { + response: ['$.foo.bar'], + }) } }) @@ -2745,7 +2994,7 @@ describe('Config', () => { process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = '1' const config = getConfig() - expect(config).to.have.property('apmTracingEnabled', false) + assert.strictEqual(config.apmTracingEnabled, false) }) it('should win DD_APM_TRACING_ENABLED', () => { @@ -2753,14 +3002,14 @@ describe('Config', () => { process.env.DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED = 'true' const config = getConfig() - expect(config).to.have.property('apmTracingEnabled', true) + 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' const config = getConfig({ experimental: { appsec: { standalone: { enabled: true } } } }) - expect(config).to.have.property('apmTracingEnabled', false) + assert.strictEqual(config.apmTracingEnabled, false) }) it('should win apmTracingEnabled option', () => { @@ -2770,17 +3019,21 @@ describe('Config', () => { apmTracingEnabled: false, experimental: { appsec: { standalone: { enabled: true } } } }) - expect(config).to.have.property('apmTracingEnabled', false) + assert.strictEqual(config.apmTracingEnabled, false) }) it('should not affect stats', () => { process.env.DD_TRACE_STATS_COMPUTATION_ENABLED = 'true' const config = getConfig() - expect(config).to.have.property('apmTracingEnabled', true) - expect(config).to.have.nested.property('stats.enabled', true) + assertObjectContains(config, { + apmTracingEnabled: true, + stats: { + enabled: true + } + }) - expect(updateConfig.getCall(0).args[0]).to.deep.include.members([ + assertObjectContains(updateConfig.getCall(0).args[0], [ { name: 'stats.enabled', value: true, origin: 'calculated' } ]) }) @@ -2790,10 +3043,14 @@ describe('Config', () => { process.env.DD_TRACE_STATS_COMPUTATION_ENABLED = 'true' const config = getConfig() - expect(config).to.have.property('apmTracingEnabled', false) - expect(config).to.have.nested.property('stats.enabled', false) + assertObjectContains(config, { + apmTracingEnabled: false, + stats: { + enabled: false + } + }) - expect(updateConfig.getCall(0).args[0]).to.deep.include.members([ + assertObjectContains(updateConfig.getCall(0).args[0], [ { name: 'stats.enabled', value: false, origin: 'calculated' } ]) }) @@ -2802,23 +3059,35 @@ describe('Config', () => { const config = getConfig({ apmTracingEnabled: false }) - expect(config).to.have.property('apmTracingEnabled', false) - expect(config).to.have.nested.property('stats.enabled', false) + assertObjectContains(config, { + apmTracingEnabled: false, + stats: { + enabled: false + } + }) }) }) context('library config', () => { + const fs = require('node:fs') + const os = require('node:os') + const path = require('node:path') + const StableConfig = require('../src/config_stable') - const path = require('path') + // os.tmpdir returns undefined on Windows somehow const baseTempDir = os.platform() !== 'win32' ? os.tmpdir() : 'C:\\Windows\\Temp' let env let tempDir + let localConfigPath + let fleetConfigPath beforeEach(() => { env = process.env tempDir = fs.mkdtempSync(path.join(baseTempDir, 'config-test-')) - process.env.DD_TEST_LOCAL_CONFIG_PATH = path.join(tempDir, 'local.yaml') - process.env.DD_TEST_FLEET_CONFIG_PATH = path.join(tempDir, 'fleet.yaml') + localConfigPath = path.join(tempDir, 'local.yaml') + fleetConfigPath = path.join(tempDir, 'fleet.yaml') + process.env.DD_TEST_LOCAL_CONFIG_PATH = localConfigPath + process.env.DD_TEST_FLEET_CONFIG_PATH = fleetConfigPath }) afterEach(() => { @@ -2828,18 +3097,18 @@ describe('Config', () => { it('should apply host wide config', () => { fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: - DD_RUNTIME_METRICS_ENABLED: true + DD_RUNTIME_METRICS_ENABLED: 'true' `) const config = getConfig() - expect(config).to.have.nested.property('runtimeMetrics.enabled', true) + assert.strictEqual(config.runtimeMetrics?.enabled, true) }) it('should apply service specific config', () => { fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` rules: - selectors: @@ -2851,17 +3120,17 @@ rules: DD_SERVICE: my-service `) const config = getConfig() - expect(config).to.have.property('service', 'my-service') + assert.strictEqual(config.service, 'my-service') }) it('should respect the priority sources', () => { // 1. Default const config1 = getConfig() - expect(config1).to.have.property('service', 'node') + assert.strictEqual(config1?.service, 'node') // 2. Local stable > Default fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` rules: - selectors: @@ -2873,24 +3142,16 @@ rules: DD_SERVICE: service_local_stable `) const config2 = getConfig() - expect(config2).to.have.property( - 'service', - 'service_local_stable', - 'default < local stable config' - ) + assert.strictEqual(config2?.service, 'service_local_stable') // 3. Env > Local stable > Default process.env.DD_SERVICE = 'service_env' const config3 = getConfig() - expect(config3).to.have.property( - 'service', - 'service_env', - 'default < local stable config < env var' - ) + assert.strictEqual(config3?.service, 'service_env') // 4. Fleet Stable > Env > Local stable > Default fs.writeFileSync( - process.env.DD_TEST_FLEET_CONFIG_PATH, + fleetConfigPath, ` rules: - selectors: @@ -2902,91 +3163,83 @@ rules: DD_SERVICE: service_fleet_stable `) const config4 = getConfig() - expect(config4).to.have.property( - 'service', - 'service_fleet_stable', - 'default < local stable config < env var < fleet stable config' - ) + assert.strictEqual(config4?.service, 'service_fleet_stable') // 5. Code > Fleet Stable > Env > Local stable > Default const config5 = getConfig({ service: 'service_code' }) - expect(config5).to.have.property( - 'service', - 'service_code', - 'default < local stable config < env var < fleet config < code' - ) + assert.strictEqual(config5?.service, 'service_code') }) it('should ignore unknown keys', () => { fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: - DD_RUNTIME_METRICS_ENABLED: true + DD_RUNTIME_METRICS_ENABLED: 'true' DD_FOOBAR_ENABLED: baz `) const stableConfig = new StableConfig() - expect(stableConfig.warnings).to.have.lengthOf(0) + assert.strictEqual(stableConfig.warnings?.length, 0) const config = getConfig() - expect(config).to.have.nested.property('runtimeMetrics.enabled', true) + assert.strictEqual(config.runtimeMetrics?.enabled, true) }) it('should log a warning if the YAML files are malformed', () => { fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: DD_RUNTIME_METRICS_ENABLED true `) const stableConfig = new StableConfig() - expect(stableConfig.warnings).to.have.lengthOf(1) + assert.strictEqual(stableConfig.warnings?.length, 1) }) it('should only load the WASM module if the stable config files exist', () => { const stableConfig1 = new StableConfig() - expect(stableConfig1).to.have.property('wasm_loaded', false) + assert.strictEqual(stableConfig1?.wasm_loaded, false) fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: - DD_RUNTIME_METRICS_ENABLED: true + DD_RUNTIME_METRICS_ENABLED: 'true' `) const stableConfig2 = new StableConfig() - expect(stableConfig2).to.have.property('wasm_loaded', true) + assert.strictEqual(stableConfig2?.wasm_loaded, true) }) it('should not load the WASM module in a serverless environment', () => { fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: - DD_RUNTIME_METRICS_ENABLED: true + DD_RUNTIME_METRICS_ENABLED: 'true' `) process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-great-lambda-function' const stableConfig = getConfig() - expect(stableConfig).to.not.have.property('stableConfig') + assert.ok(!(Object.hasOwn(stableConfig, 'stableConfig'))) }) it('should support all extended configs across product areas', () => { fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: DD_TRACE_PROPAGATION_STYLE: "tracecontext" DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED: true - DD_APPSEC_TRACE_RATE_LIMIT: 100 - DD_APPSEC_MAX_STACK_TRACES: 2 + DD_APPSEC_TRACE_RATE_LIMIT: '100' + DD_APPSEC_MAX_STACK_TRACES: '2' DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP: "password|token" - DD_IAST_REQUEST_SAMPLING: 50 - DD_IAST_MAX_CONCURRENT_REQUESTS: 10 + DD_IAST_REQUEST_SAMPLING: '50' + DD_IAST_MAX_CONCURRENT_REQUESTS: '10' - DD_TELEMETRY_HEARTBEAT_INTERVAL: 42 - DD_TELEMETRY_METRICS_ENABLED: false + DD_TELEMETRY_HEARTBEAT_INTERVAL: '42' + DD_TELEMETRY_METRICS_ENABLED: 'false' DD_LLMOBS_ML_APP: "my-llm-app" @@ -2997,31 +3250,37 @@ apm_configuration_default: const config = getConfig() // Tracing - expect(config).to.have.nested.property('traceId128BitGenerationEnabled', true) - expect(config).to.have.nested.deep.property('tracePropagationStyle.inject', ['tracecontext']) - expect(config).to.have.nested.deep.property('tracePropagationStyle.extract', ['tracecontext']) + assert.strictEqual(config.traceId128BitGenerationEnabled, true) + assert.deepStrictEqual(config.tracePropagationStyle?.inject, ['tracecontext']) + assert.deepStrictEqual(config.tracePropagationStyle?.extract, ['tracecontext']) // Appsec - expect(config).to.have.nested.property('appsec.rateLimit', 100) - expect(config).to.have.nested.property('appsec.stackTrace.maxStackTraces', 2) - expect(config).to.have.nested.property('appsec.obfuscatorKeyRegex', 'password|token') - - // IAST - expect(config).to.have.nested.property('iast.requestSampling', 50) - expect(config).to.have.nested.property('iast.maxConcurrentRequests', 10) - - // Telemetry - expect(config).to.have.nested.property('telemetry.heartbeatInterval', 42000) - expect(config).to.have.nested.property('telemetry.metrics', false) - - // LLMObs - expect(config).to.have.nested.property('llmobs.mlApp', 'my-llm-app') - - // Profiling - expect(config).to.have.nested.property('profiling.exporters', 'agent') - - // Dynamic Instrumentation - expect(config).to.have.nested.property('dynamicInstrumentation.probeFile', '/tmp/probes') + assertObjectContains(config, { + appsec: { + rateLimit: 100, + stackTrace: { + maxStackTraces: 2 + }, + obfuscatorKeyRegex: 'password|token' + }, + iast: { + requestSampling: 50, + maxConcurrentRequests: 10 + }, + telemetry: { + heartbeatInterval: 42000, + metrics: false + }, + llmobs: { + mlApp: 'my-llm-app' + }, + profiling: { + exporters: 'agent' + }, + dynamicInstrumentation: { + probeFile: '/tmp/probes' + } + }) }) // Regression test for fields that were previously set directly from environment variables @@ -3029,7 +3288,7 @@ apm_configuration_default: it('should support legacy direct-set fields through all stableconfig and env var sources', () => { // Test 1: Local stable config should work fs.writeFileSync( - process.env.DD_TEST_LOCAL_CONFIG_PATH, + localConfigPath, ` apm_configuration_default: DD_API_KEY: "local-api-key" @@ -3038,16 +3297,22 @@ apm_configuration_default: DD_INSTRUMENTATION_INSTALL_TIME: "1234567890" DD_INSTRUMENTATION_INSTALL_TYPE: "local_install" DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING: "all" - DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH: 5 + DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH: '5' `) let config = getConfig() - expect(config).to.have.property('apiKey', 'local-api-key') - expect(config).to.have.property('appKey', 'local-app-key') - expect(config).to.have.nested.property('installSignature.id', 'local-install-id') - expect(config).to.have.nested.property('installSignature.time', '1234567890') - expect(config).to.have.nested.property('installSignature.type', 'local_install') - expect(config).to.have.nested.property('cloudPayloadTagging.requestsEnabled', true) - expect(config).to.have.nested.property('cloudPayloadTagging.maxDepth', 5) + assertObjectContains(config, { + apiKey: 'local-api-key', + appKey: 'local-app-key', + installSignature: { + id: 'local-install-id', + time: '1234567890', + type: 'local_install' + }, + cloudPayloadTagging: { + requestsEnabled: true, + maxDepth: 5 + } + }) // Test 2: Env vars should take precedence over local stable config process.env.DD_API_KEY = 'env-api-key' @@ -3055,14 +3320,20 @@ apm_configuration_default: process.env.DD_INSTRUMENTATION_INSTALL_ID = 'env-install-id' process.env.DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH = '7' config = getConfig() - expect(config).to.have.property('apiKey', 'env-api-key') - expect(config).to.have.property('appKey', 'env-app-key') - expect(config).to.have.nested.property('installSignature.id', 'env-install-id') - expect(config).to.have.nested.property('cloudPayloadTagging.maxDepth', 7) + assertObjectContains(config, { + apiKey: 'env-api-key', + appKey: 'env-app-key', + installSignature: { + id: 'env-install-id' + }, + cloudPayloadTagging: { + maxDepth: 7 + } + }) // Test 3: Fleet stable config should take precedence over env vars fs.writeFileSync( - process.env.DD_TEST_FLEET_CONFIG_PATH, + fleetConfigPath, ` rules: - selectors: @@ -3078,17 +3349,23 @@ rules: DD_INSTRUMENTATION_INSTALL_TYPE: "fleet_install" DD_TRACE_CLOUD_REQUEST_PAYLOAD_TAGGING: "" DD_TRACE_CLOUD_RESPONSE_PAYLOAD_TAGGING: "all" - DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH: 15 + DD_TRACE_CLOUD_PAYLOAD_TAGGING_MAX_DEPTH: '15' `) config = getConfig() - expect(config).to.have.property('apiKey', 'fleet-api-key') - expect(config).to.have.property('appKey', 'fleet-app-key') - expect(config).to.have.nested.property('installSignature.id', 'fleet-install-id') - expect(config).to.have.nested.property('installSignature.time', '9999999999') - expect(config).to.have.nested.property('installSignature.type', 'fleet_install') - expect(config).to.have.nested.property('cloudPayloadTagging.requestsEnabled', false) - expect(config).to.have.nested.property('cloudPayloadTagging.responsesEnabled', true) - expect(config).to.have.nested.property('cloudPayloadTagging.maxDepth', 15) + assertObjectContains(config, { + apiKey: 'fleet-api-key', + appKey: 'fleet-app-key', + installSignature: { + id: 'fleet-install-id', + time: '9999999999', + type: 'fleet_install' + }, + cloudPayloadTagging: { + requestsEnabled: false, + responsesEnabled: true, + maxDepth: 15 + } + }) }) }) @@ -3118,60 +3395,60 @@ rules: it('should be false by default', () => { const config = getConfig() - expect(config).to.have.property('resourceRenamingEnabled', false) + assert.strictEqual(config.resourceRenamingEnabled, false) }) it('should be enabled when DD_TRACE_RESOURCE_RENAMING_ENABLED is true', () => { process.env.DD_TRACE_RESOURCE_RENAMING_ENABLED = 'true' const config = getConfig() - expect(config).to.have.property('resourceRenamingEnabled', true) + assert.strictEqual(config.resourceRenamingEnabled, true) }) it('should be disabled when DD_TRACE_RESOURCE_RENAMING_ENABLED is false', () => { process.env.DD_TRACE_RESOURCE_RENAMING_ENABLED = 'false' const config = getConfig() - expect(config).to.have.property('resourceRenamingEnabled', false) + assert.strictEqual(config.resourceRenamingEnabled, false) }) it('should be enabled when appsec is enabled via env var', () => { process.env.DD_APPSEC_ENABLED = 'true' const config = getConfig() - expect(config).to.have.property('resourceRenamingEnabled', true) + assert.strictEqual(config.resourceRenamingEnabled, true) }) it('should be enabled when appsec is enabled via options', () => { const config = getConfig({ appsec: { enabled: true } }) - expect(config).to.have.property('resourceRenamingEnabled', true) + assert.strictEqual(config.resourceRenamingEnabled, true) }) it('should prioritize DD_TRACE_RESOURCE_RENAMING_ENABLED over appsec setting', () => { process.env.DD_APPSEC_ENABLED = 'true' process.env.DD_TRACE_RESOURCE_RENAMING_ENABLED = 'false' const config = getConfig() - expect(config).to.have.property('resourceRenamingEnabled', false) + assert.strictEqual(config.resourceRenamingEnabled, false) }) it('should prioritize DD_TRACE_RESOURCE_RENAMING_ENABLED over appsec option', () => { process.env.DD_TRACE_RESOURCE_RENAMING_ENABLED = 'false' const config = getConfig({ appsec: { enabled: true } }) - expect(config).to.have.property('resourceRenamingEnabled', false) + assert.strictEqual(config.resourceRenamingEnabled, false) }) it('should enable when appsec is enabled via both env and options', () => { process.env.DD_APPSEC_ENABLED = 'true' const config = getConfig({ appsec: { enabled: true } }) - expect(config).to.have.property('resourceRenamingEnabled', true) + assert.strictEqual(config.resourceRenamingEnabled, true) }) it('should remain false when appsec is disabled', () => { process.env.DD_APPSEC_ENABLED = 'false' const config = getConfig() - expect(config).to.have.property('resourceRenamingEnabled', false) + assert.strictEqual(config.resourceRenamingEnabled, false) }) it('should remain false when appsec is disabled via options', () => { const config = getConfig({ appsec: { enabled: false } }) - expect(config).to.have.property('resourceRenamingEnabled', false) + assert.strictEqual(config.resourceRenamingEnabled, false) }) }) @@ -3189,7 +3466,7 @@ rules: it('should return default value', () => { const config = getConfig() - expect(config.getOrigin('appsec.enabled')).to.be.equal('default') + assert.strictEqual(config.getOrigin('appsec.enabled'), 'default') }) it('should return env_var', () => { @@ -3197,7 +3474,7 @@ rules: const config = getConfig() - expect(config.getOrigin('appsec.enabled')).to.be.equal('env_var') + assert.strictEqual(config.getOrigin('appsec.enabled'), 'env_var') }) it('should return code', () => { @@ -3205,7 +3482,7 @@ rules: appsec: true }) - expect(config.getOrigin('appsec.enabled')).to.be.equal('code') + assert.strictEqual(config.getOrigin('appsec.enabled'), 'code') }) }) }) diff --git a/packages/dd-trace/test/config/disabled_instrumentations.spec.js b/packages/dd-trace/test/config/disabled_instrumentations.spec.js index 81ff70d5a1e..eeefecf3a61 100644 --- a/packages/dd-trace/test/config/disabled_instrumentations.spec.js +++ b/packages/dd-trace/test/config/disabled_instrumentations.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha + +const { describe, it } = require('mocha') require('../setup/core') diff --git a/packages/dd-trace/test/config/remote_config.spec.js b/packages/dd-trace/test/config/remote_config.spec.js new file mode 100644 index 00000000000..49b6b03348d --- /dev/null +++ b/packages/dd-trace/test/config/remote_config.spec.js @@ -0,0 +1,77 @@ +'use strict' + +const { describe, it, beforeEach } = require('mocha') +const sinon = require('sinon') + +const RemoteConfigCapabilities = require('../../src/remote_config/capabilities') +const { enable } = require('../../src/config/remote_config') + +require('../setup/core') + +describe('Tracing Remote Config', () => { + let rc + let config + let enableOrDisableTracing + let handlers + + beforeEach(() => { + handlers = new Map() + + rc = { + updateCapabilities: sinon.spy(), + setProductHandler: sinon.spy((product, handler) => { + handlers.set(product, handler) + }) + } + + config = { + configure: sinon.spy() + } + + enableOrDisableTracing = sinon.spy() + }) + + describe('enable', () => { + it('should register all APM tracing capabilities', () => { + enable(rc, config, enableOrDisableTracing) + + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.APM_TRACING_CUSTOM_TAGS, true) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.APM_TRACING_HTTP_HEADER_TAGS, true) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.APM_TRACING_LOGS_INJECTION, true) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.APM_TRACING_SAMPLE_RATE, true) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.APM_TRACING_ENABLED, true) + sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.APM_TRACING_SAMPLE_RULES, true) + }) + + it('should register APM_TRACING product handler', () => { + enable(rc, config, enableOrDisableTracing) + + sinon.assert.calledOnceWithExactly(rc.setProductHandler, 'APM_TRACING', sinon.match.func) + }) + + describe('APM_TRACING handler', () => { + it('should configure tracer on apply action', () => { + enable(rc, config, enableOrDisableTracing) + + const handler = handlers.get('APM_TRACING') + const libConfig = { service: 'test-service' } + + handler('apply', { lib_config: libConfig }) + + sinon.assert.calledOnceWithExactly(config.configure, libConfig, true) + sinon.assert.calledOnceWithExactly(enableOrDisableTracing, config, rc) + }) + + it('should reset config on unapply action', () => { + enable(rc, config, enableOrDisableTracing) + + const handler = handlers.get('APM_TRACING') + + handler('unapply', {}) + + sinon.assert.calledOnceWithExactly(config.configure, {}, true) + sinon.assert.calledOnceWithExactly(enableOrDisableTracing, config, rc) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/crashtracking/crashtracker.spec.js b/packages/dd-trace/test/crashtracking/crashtracker.spec.js index ed2982c2e5a..181daf150e6 100644 --- a/packages/dd-trace/test/crashtracking/crashtracker.spec.js +++ b/packages/dd-trace/test/crashtracking/crashtracker.spec.js @@ -107,5 +107,57 @@ describe('crashtracking', () => { assert.doesNotThrow(() => crashtracker.configure(config)) }) }) + + describe('process tags', () => { + it('should include process tags in metadata', () => { + crashtracker.start(config) + + sinon.assert.calledOnce(binding.init) + const metadata = binding.init.firstCall.args[2] + + assert.ok(metadata) + assert.ok(Array.isArray(metadata.tags)) + + // Check that process tags are included + const hasEntrypointType = metadata.tags.some(tag => tag.startsWith('entrypoint.type:')) + const hasEntrypointName = metadata.tags.some(tag => tag.startsWith('entrypoint.name:')) + const hasEntrypointWorkdir = metadata.tags.some(tag => tag.startsWith('entrypoint.workdir:')) + const hasEntrypointBasedir = metadata.tags.some(tag => tag.startsWith('entrypoint.basedir:')) + + assert.ok(hasEntrypointType, 'should include entrypoint.type tag') + assert.ok(hasEntrypointName, 'should include entrypoint.name tag') + assert.ok(hasEntrypointWorkdir, 'should include entrypoint.workdir tag') + assert.ok(hasEntrypointBasedir, 'should include entrypoint.basedir tag') + }) + + it('should include user tags and process tags together', () => { + crashtracker.start(config) + + const metadata = binding.init.firstCall.args[2] + + // Check that user tags are included + const hasFooTag = metadata.tags.some(tag => tag === 'foo:bar') + assert.ok(hasFooTag, 'should include user-defined tags') + + // Check that process tags are also included + const hasProcessTags = metadata.tags.some(tag => tag.startsWith('entrypoint.')) + assert.ok(hasProcessTags, 'should include process tags') + }) + + it('should update process tags when reconfiguring', () => { + crashtracker.start(config) + crashtracker.configure(config) + + sinon.assert.called(binding.updateMetadata) + const metadata = binding.updateMetadata.firstCall.args[0] + + assert.ok(metadata) + assert.ok(Array.isArray(metadata.tags)) + + // Verify process tags are in the updated metadata + const hasProcessTags = metadata.tags.some(tag => tag.startsWith('entrypoint.')) + assert.ok(hasProcessTags, 'should include process tags in updated metadata') + }) + }) }) }) diff --git a/packages/dd-trace/test/crashtracking/index.spec.js b/packages/dd-trace/test/crashtracking/index.spec.js index e850aaec292..3e6fba9f078 100644 --- a/packages/dd-trace/test/crashtracking/index.spec.js +++ b/packages/dd-trace/test/crashtracking/index.spec.js @@ -1,11 +1,12 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha -const sinon = require('sinon') -const proxyquire = require('proxyquire') const path = require('node:path') const { Worker } = require('node:worker_threads') +const { describe, it, beforeEach } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire') + require('../setup/core') describe('crashtracking', () => { diff --git a/packages/dd-trace/test/custom-metrics.spec.js b/packages/dd-trace/test/custom-metrics.spec.js index 28d221d8b55..4bcbfa80b72 100644 --- a/packages/dd-trace/test/custom-metrics.spec.js +++ b/packages/dd-trace/test/custom-metrics.spec.js @@ -1,14 +1,13 @@ 'use strict' const assert = require('node:assert/strict') -/* eslint-disable no-console */ -const { describe, it, beforeEach, afterEach } = require('tap').mocha - const http = require('node:http') const path = require('node:path') const os = require('node:os') const { exec } = require('node:child_process') +const { describe, it, beforeEach, afterEach } = require('mocha') + require('./setup/core') describe('Custom Metrics', () => { @@ -52,7 +51,9 @@ describe('Custom Metrics', () => { } }, (err, stdout, stderr) => { if (err) return done(err) + // eslint-disable-next-line no-console if (stdout) console.log(stdout) + // eslint-disable-next-line no-console if (stderr) console.error(stderr) assert.strictEqual(metricsData.split('#')[0], 'page.views.data:1|c|') diff --git a/packages/dd-trace/test/datastreams/data_streams_checkpointer.spec.js b/packages/dd-trace/test/datastreams/data_streams_checkpointer.spec.js index 3bd6dab4fce..c4f206f8acd 100644 --- a/packages/dd-trace/test/datastreams/data_streams_checkpointer.spec.js +++ b/packages/dd-trace/test/datastreams/data_streams_checkpointer.spec.js @@ -2,11 +2,10 @@ const assert = require('node:assert/strict') -const { describe, it, before, after } = require('tap').mocha +const { describe, it, before, after } = require('mocha') const sinon = require('sinon') require('../setup/core') - const agent = require('../plugins/agent') const expectedProducerHash = '6359420180750536220' diff --git a/packages/dd-trace/test/datastreams/encoding.spec.js b/packages/dd-trace/test/datastreams/encoding.spec.js index 717fc2f85be..5a622bafa5a 100644 --- a/packages/dd-trace/test/datastreams/encoding.spec.js +++ b/packages/dd-trace/test/datastreams/encoding.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../setup/core') +const { describe, it } = require('mocha') +require('../setup/core') const { encodeVarint, decodeVarint } = require('../../src/datastreams/encoding') describe('encoding', () => { diff --git a/packages/dd-trace/test/datastreams/pathway.spec.js b/packages/dd-trace/test/datastreams/pathway.spec.js index b610be672f8..832056667c4 100644 --- a/packages/dd-trace/test/datastreams/pathway.spec.js +++ b/packages/dd-trace/test/datastreams/pathway.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha + +const { describe, it } = require('mocha') require('../setup/core') diff --git a/packages/dd-trace/test/datastreams/processor.spec.js b/packages/dd-trace/test/datastreams/processor.spec.js index 1187eec4f1e..7155c5ec799 100644 --- a/packages/dd-trace/test/datastreams/processor.spec.js +++ b/packages/dd-trace/test/datastreams/processor.spec.js @@ -1,15 +1,13 @@ 'use strict' const assert = require('node:assert/strict') +const { hostname } = require('node:os') -const { expect } = require('chai') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') -const { hostname } = require('node:os') const proxyquire = require('proxyquire') require('../setup/core') - const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js') const HIGH_ACCURACY_DISTRIBUTION = 0.0075 @@ -221,7 +219,7 @@ describe('DataStreamsProcessor', () => { processor = new DataStreamsProcessor(config) clearTimeout(processor.timer) - expect(DataStreamsWriter).to.be.calledWith({ + sinon.assert.calledWith(DataStreamsWriter, { hostname: config.hostname, port: config.port, url: config.url @@ -294,7 +292,7 @@ describe('DataStreamsProcessor', () => { it('should export on interval', () => { processor.recordCheckpoint(mockCheckpoint) processor.onInterval() - expect(writer.flush).to.be.calledWith({ + sinon.assert.calledWith(writer.flush, { Env: 'test', Service: 'service1', Version: 'v1', diff --git a/packages/dd-trace/test/datastreams/schemas/schema_builder.spec.js b/packages/dd-trace/test/datastreams/schemas/schema_builder.spec.js index f62c1922481..c5990b90e64 100644 --- a/packages/dd-trace/test/datastreams/schemas/schema_builder.spec.js +++ b/packages/dd-trace/test/datastreams/schemas/schema_builder.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../../setup/core') +const { describe, it } = require('mocha') +require('../../setup/core') const { SchemaBuilder } = require('../../../src/datastreams/schemas/schema_builder') class Iterator { diff --git a/packages/dd-trace/test/datastreams/schemas/schema_sampler.spec.js b/packages/dd-trace/test/datastreams/schemas/schema_sampler.spec.js index 94ba60d81c1..0ec41fb4a47 100644 --- a/packages/dd-trace/test/datastreams/schemas/schema_sampler.spec.js +++ b/packages/dd-trace/test/datastreams/schemas/schema_sampler.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../../setup/core') +const { describe, it } = require('mocha') +require('../../setup/core') const { SchemaSampler } = require('../../../src/datastreams/schemas/schema_sampler') describe('SchemaSampler', () => { diff --git a/packages/dd-trace/test/datastreams/writer.spec.js b/packages/dd-trace/test/datastreams/writer.spec.js index 1e9a3c68589..1c9b1d99c34 100644 --- a/packages/dd-trace/test/datastreams/writer.spec.js +++ b/packages/dd-trace/test/datastreams/writer.spec.js @@ -1,14 +1,15 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha + +const { describe, it } = require('mocha') const sinon = require('sinon') const msgpack = require('@msgpack/msgpack') const proxyquire = require('proxyquire') require('../setup/core') - const pkg = require('../../../../package.json') + const stubRequest = sinon.stub() const stubZlib = { diff --git a/packages/dd-trace/test/dd-trace.spec.js b/packages/dd-trace/test/dd-trace.spec.js index 8332f141511..15acff3ee21 100644 --- a/packages/dd-trace/test/dd-trace.spec.js +++ b/packages/dd-trace/test/dd-trace.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha -require('./setup/core') +const { describe, it, beforeEach, afterEach } = require('mocha') -const agent = require('./plugins/agent') +require('./setup/core') const { SAMPLING_PRIORITY_KEY, DECISION_MAKER_KEY } = require('../src/constants') +const agent = require('./plugins/agent') describe('dd-trace', () => { let tracer diff --git a/packages/dd-trace/test/debugger/config.spec.js b/packages/dd-trace/test/debugger/config.spec.js index e8a11e01061..612f47124d1 100644 --- a/packages/dd-trace/test/debugger/config.spec.js +++ b/packages/dd-trace/test/debugger/config.spec.js @@ -21,6 +21,7 @@ describe('getDebuggerConfig', function () { 'hostname', 'logLevel', 'port', + 'propagateProcessTags', 'repositoryUrl', 'runtimeId', 'service', 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 3d7f687490c..ea2287f25be 100644 --- a/packages/dd-trace/test/debugger/devtools_client/breakpoints.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/breakpoints.spec.js @@ -22,9 +22,25 @@ describe('breakpoints', function () { * @type {{ * findScriptFromPartialPath: sinon.SinonStub; * clearState: sinon.SinonStub; - * locationToBreakpoint: Map; - * breakpointToProbes: Map; - * probeToLocation: Map; + * locationToBreakpoint: Map; + * breakpointToProbes: Map>; + * probeToLocation: Map; * '@noCallThru': boolean; * }} */ diff --git a/packages/dd-trace/test/debugger/devtools_client/condition-test-cases.js b/packages/dd-trace/test/debugger/devtools_client/condition-test-cases.js index dd932ddfdfb..c069a5c7036 100644 --- a/packages/dd-trace/test/debugger/devtools_client/condition-test-cases.js +++ b/packages/dd-trace/test/debugger/devtools_client/condition-test-cases.js @@ -59,7 +59,7 @@ * InstanceofExpression| * IsDefinedExpression} Expression * - * @typedef {Object.} VariableBindings + * @typedef {Record} VariableBindings * * @typedef {[Expression, VariableBindings, unknown]} TestCaseTuple * @typedef {{ diff --git a/packages/dd-trace/test/debugger/devtools_client/condition.spec.js b/packages/dd-trace/test/debugger/devtools_client/condition.spec.js index 40a841ec979..c0970d1dff8 100644 --- a/packages/dd-trace/test/debugger/devtools_client/condition.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/condition.spec.js @@ -5,6 +5,11 @@ const assert = require('node:assert/strict') const { beforeEach, describe, it } = require('mocha') require('../../setup/mocha') +const { + compile, + compileSegments, + templateRequiresEvaluation +} = require('../../../src/debugger/devtools_client/condition') const { literals, references, @@ -18,11 +23,6 @@ const { membershipAndMatching, typeAndDefinitionChecks } = require('./condition-test-cases') -const { - compile, - compileSegments, - templateRequiresEvaluation -} = require('../../../src/debugger/devtools_client/condition') // Each test case is either a tuple of [ast, vars, expected] where: // - `ast` is the abstract syntax tree to be compiled diff --git a/packages/dd-trace/test/debugger/devtools_client/send.spec.js b/packages/dd-trace/test/debugger/devtools_client/send.spec.js index c75b60d440f..5cdc8769e52 100644 --- a/packages/dd-trace/test/debugger/devtools_client/send.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/send.spec.js @@ -7,9 +7,9 @@ const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') -const { getRequestOptions } = require('./utils') const JSONBuffer = require('../../../src/debugger/devtools_client/json-buffer') const { version } = require('../../../../../package.json') +const { getRequestOptions } = require('./utils') require('../../setup/mocha') diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot-pruner.spec.js b/packages/dd-trace/test/debugger/devtools_client/snapshot-pruner.spec.js index 76bb1f4b92e..055bdb052d7 100644 --- a/packages/dd-trace/test/debugger/devtools_client/snapshot-pruner.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot-pruner.spec.js @@ -434,8 +434,8 @@ describe('snapshot-pruner', function () { * Assert that the pruneSnapshot function successfully prunes the snapshot and returns the expected locals. * @param {number} maxSize - Used to define the max allowed size of the snapshot. If positive, it's the absolute max * size value. If negative, it's redacted from the actual size. - * @param {Object} originalLocals - The locals to use for the snapshot. - * @param {Object} expectedLocals - The expected locals after pruning. + * @param {object} originalLocals - The locals to use for the snapshot. + * @param {object} expectedLocals - The expected locals after pruning. */ function assertPrunedSnapshot (maxSize, originalLocals, expectedLocals) { Object.assign(locals, originalLocals) diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/collector-deadline.spec.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/collector-deadline.spec.js new file mode 100644 index 00000000000..d97229d3a63 --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/collector-deadline.spec.js @@ -0,0 +1,205 @@ +'use strict' + +const assert = require('node:assert/strict') +const { afterEach, beforeEach, describe, it } = require('mocha') +const sinon = require('sinon') +require('../../../setup/mocha') + +const proxyquire = require('proxyquire') +const { timeBudgetSym } = require('../../../../src/debugger/devtools_client/snapshot/symbols') +const session = require('./stub-session') + +describe('debugger -> devtools client -> snapshot collector deadline', function () { + let collectObjectProperties + let clock + + beforeEach(async function () { + clock = sinon.useFakeTimers() + + // Stub the collector with the stubbed session + const collectorWithStub = proxyquire('../../../../src/debugger/devtools_client/snapshot/collector', { + '../session': session + }) + collectObjectProperties = collectorWithStub.collectObjectProperties + + await session.post('Debugger.enable') + }) + + afterEach(async function () { + session.removeAllListeners('Debugger.scriptParsed') + session.removeAllListeners('Debugger.paused') + await session.post('Debugger.disable') + clock.restore() + }) + + it('should not mark properties with timeout when deadline is not exceeded', async function () { + const ctx = { + deadlineReached: false, + captureErrors: [] + } + + const opts = { + maxReferenceDepth: 3, + maxCollectionSize: 100, + maxFieldCount: 100, + deadlineNs: 100_000_000n, // 100ms + ctx + } + + const obj = { a: 1, b: 2, c: 3 } + + const objectId = await getObjectIdForObject(obj) + const properties = await collectObjectProperties(objectId, opts, 0) + + // Verify no properties are marked with timeout symbol + for (const prop of properties) { + assert.strictEqual(prop.value?.[timeBudgetSym], undefined) + } + + // Verify deadline was not reached + assert.strictEqual(ctx.deadlineReached, false) + }) + + it('should mark properties with timeout when deadline is exceeded', async function () { + // Override the hrtime stub to advance time on each call + // This simulates time passing during collection + sinon.restore() + clock = sinon.useFakeTimers() + sinon.stub(process.hrtime, 'bigint').callsFake(() => { + const time = BigInt(clock.now) * 1_000_000n + clock.tick(50) // Advance by 50ms after each call + return time + }) + + const ctx = { + deadlineReached: false, + captureErrors: [] + } + + const opts = { + maxReferenceDepth: 5, // Deep enough to require multiple calls + maxCollectionSize: 100, + maxFieldCount: 100, + deadlineNs: 10_000_000n, // 10ms (very tight deadline) + ctx + } + + // Create a nested object structure that will take time to collect + const nestedObj = { + level1: { + level2: { + level3: { + level4: { + level5: { a: 1, b: 2, c: 3 } + } + } + } + } + } + + const objectId = await getObjectIdForObject(nestedObj) + await collectObjectProperties(objectId, opts, 0) + + // Verify deadline was reached during collection + assert.strictEqual(ctx.deadlineReached, true) + }) + + it('should cache deadline reached state in ctx', async function () { + let hrtimeCallCount = 0 + + // Override the hrtime stub to track calls and advance time + sinon.restore() + clock = sinon.useFakeTimers() + sinon.stub(process.hrtime, 'bigint').callsFake(() => { + const time = BigInt(clock.now) * 1_000_000n + hrtimeCallCount++ + clock.tick(30) // Advance by 30ms after each call + return time + }) + + const ctx = { + deadlineReached: false, + captureErrors: [] + } + + const opts = { + maxReferenceDepth: 5, + maxCollectionSize: 100, + maxFieldCount: 100, + deadlineNs: 50_000_000n, // 50ms (will be exceeded after a few calls) + ctx + } + + // Create an object with multiple nested properties to trigger multiple overBudget checks + const objWithManyProps = { + a: { nested: { deep: 1 } }, + b: { nested: { deep: 2 } }, + c: { nested: { deep: 3 } }, + d: { nested: { deep: 4 } } + } + + const objectId = await getObjectIdForObject(objWithManyProps) + await collectObjectProperties(objectId, opts, 0) + + // Verify deadline was reached + assert.strictEqual(ctx.deadlineReached, true) + + // The hrtime should be called at least a couple times + // but once deadlineReached is cached, it shouldn't be called again + assert.ok(hrtimeCallCount >= 2, `Expected at least 2 hrtime calls, got ${hrtimeCallCount}`) + }) + + it('should immediately return true for overBudget when deadline already reached', async function () { + // Advance time past deadline + clock.tick(200) + + const ctx = { + deadlineReached: true, // Already marked as reached + captureErrors: [] + } + + const opts = { + maxReferenceDepth: 5, + maxCollectionSize: 100, + maxFieldCount: 100, + deadlineNs: 100_000_000n, // 100ms + ctx + } + + // Track CDP calls to verify we short-circuit + let cdpCallCount = 0 + const originalPost = session.post.bind(session) + session.post = function (method, params) { + if (method === 'Runtime.getProperties') { + cdpCallCount++ + } + return originalPost(method, params) + } + + // Create a nested object that would normally require many CDP calls + const nestedObj = { + a: { nested: { deep: 1 } }, + b: { nested: { deep: 2 } }, + c: { nested: { deep: 3 } }, + d: { nested: { deep: 4 } } + } + + const objectId = await getObjectIdForObject(nestedObj) + await collectObjectProperties(objectId, opts, 0) + + // Restore original + session.post = originalPost + + // Verify ctx.deadlineReached remains true + assert.strictEqual(ctx.deadlineReached, true) + + // Verify we made very few CDP calls (should be 1 for the root object only) + // If deadline wasn't already reached, this would make many more calls for nested properties + assert.ok(cdpCallCount <= 1, `Expected at most 1 CDP call due to short-circuit, but made ${cdpCallCount}`) + }) +}) + +async function getObjectIdForObject (obj) { + const { result: { objectId } } = await session.post('Runtime.evaluate', { expression: `(${JSON.stringify(obj)})` }) + return objectId +} diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/complex-types.spec.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/complex-types.spec.js index 9a80f939a54..d86ea978f82 100644 --- a/packages/dd-trace/test/debugger/devtools_client/snapshot/complex-types.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/complex-types.spec.js @@ -3,10 +3,10 @@ const assert = require('node:assert/strict') const { afterEach, beforeEach, describe, it } = require('mocha') +const NODE_20_PLUS = require('semver').gte(process.version, '20.0.0') const { assertObjectContains } = require('../../../../../../integration-tests/helpers') require('../../../setup/mocha') - const { session, getTargetCodePath, @@ -16,7 +16,6 @@ const { getLocalStateForCallFrame } = require('./utils') -const NODE_20_PLUS = require('semver').gte(process.version, '20.0.0') const target = getTargetCodePath(__filename) describe('debugger -> devtools client -> snapshot.getLocalStateForCallFrame', function () { diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/max-collection-size.spec.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/max-collection-size.spec.js index 2421058edd6..42b112cdfd8 100644 --- a/packages/dd-trace/test/debugger/devtools_client/snapshot/max-collection-size.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/max-collection-size.spec.js @@ -7,11 +7,11 @@ const { afterEach, beforeEach, describe, it } = require('mocha') const { assertObjectContains } = require('../../../../../../integration-tests/helpers') require('../../../setup/mocha') -const { getTargetCodePath, enable, teardown, assertOnBreakpoint, setAndTriggerBreakpoint } = require('./utils') const { LARGE_OBJECT_SKIP_THRESHOLD, DEFAULT_MAX_COLLECTION_SIZE } = require('../../../../src/debugger/devtools_client/snapshot/constants') +const { getTargetCodePath, enable, teardown, assertOnBreakpoint, setAndTriggerBreakpoint } = require('./utils') const target = getTargetCodePath(__filename) diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/max-field-count.spec.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/max-field-count.spec.js index 5f3486a9bf5..d94b9492fbc 100644 --- a/packages/dd-trace/test/debugger/devtools_client/snapshot/max-field-count.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/max-field-count.spec.js @@ -5,8 +5,8 @@ const assert = require('node:assert/strict') const { afterEach, beforeEach, describe, it } = require('mocha') require('../../../setup/mocha') -const { getTargetCodePath, enable, teardown, assertOnBreakpoint, setAndTriggerBreakpoint } = require('./utils') const { DEFAULT_MAX_FIELD_COUNT } = require('../../../../src/debugger/devtools_client/snapshot/constants') +const { getTargetCodePath, enable, teardown, assertOnBreakpoint, setAndTriggerBreakpoint } = require('./utils') const target = getTargetCodePath(__filename) diff --git a/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js b/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js index 998348acbd7..3283dd51b14 100644 --- a/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js +++ b/packages/dd-trace/test/debugger/devtools_client/snapshot/utils.js @@ -3,8 +3,8 @@ const assert = require('node:assert') const { join, basename } = require('node:path') -const session = require('./stub-session') const proxyquire = require('proxyquire') +const session = require('./stub-session') const collectorWithStub = proxyquire('../../../../src/debugger/devtools_client/snapshot/collector', { '../session': session diff --git a/packages/dd-trace/test/debugger/devtools_client/status.spec.js b/packages/dd-trace/test/debugger/devtools_client/status.spec.js index 2950ebee499..498fe6e5f49 100644 --- a/packages/dd-trace/test/debugger/devtools_client/status.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/status.spec.js @@ -7,8 +7,8 @@ const proxyquire = require('proxyquire') const sinon = require('sinon') require('../../setup/mocha') -const { getRequestOptions } = require('./utils') const JSONBuffer = require('../../../src/debugger/devtools_client/json-buffer') +const { getRequestOptions } = require('./utils') const ddsource = 'dd_debugger' const service = 'my-service' diff --git a/packages/dd-trace/test/dogstatsd.spec.js b/packages/dd-trace/test/dogstatsd.spec.js index da478435782..8b7594c6dc0 100644 --- a/packages/dd-trace/test/dogstatsd.spec.js +++ b/packages/dd-trace/test/dogstatsd.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha -const sinon = require('sinon') -const proxyquire = require('proxyquire') - const http = require('node:http') const path = require('node:path') const os = require('node:os') +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire') + require('./setup/core') describe('dogstatsd', () => { diff --git a/packages/dd-trace/test/encode/0.4.spec.js b/packages/dd-trace/test/encode/0.4.spec.js index 04380e43bb6..f22beb04c16 100644 --- a/packages/dd-trace/test/encode/0.4.spec.js +++ b/packages/dd-trace/test/encode/0.4.spec.js @@ -1,15 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const msgpack = require('@msgpack/msgpack') const proxyquire = require('proxyquire') const sinon = require('sinon') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - const id = require('../../src/id') function randString (length) { diff --git a/packages/dd-trace/test/encode/0.5.spec.js b/packages/dd-trace/test/encode/0.5.spec.js index c46e0c8af66..d63916c8606 100644 --- a/packages/dd-trace/test/encode/0.5.spec.js +++ b/packages/dd-trace/test/encode/0.5.spec.js @@ -1,14 +1,13 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const msgpack = require('@msgpack/msgpack') const sinon = require('sinon') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - const id = require('../../src/id') function randString (length) { diff --git a/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js b/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js index 63338456b1a..c4cd3cd13a7 100644 --- a/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js +++ b/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js @@ -2,17 +2,15 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { assertObjectContains } = require('../../../../integration-tests/helpers') - -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const msgpack = require('@msgpack/msgpack') const sinon = require('sinon') const proxyquire = require('proxyquire') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - const id = require('../../src/id') + const { MAX_META_KEY_LENGTH, MAX_META_VALUE_LENGTH, @@ -152,7 +150,7 @@ describe('agentless-ci-visibility-encode', () => { const buffer = encoder.makePayload() const decodedTrace = msgpack.decode(buffer, { useBigInt64: true }) - expect(decodedTrace) + assert.ok(decodedTrace) const spanEvent = decodedTrace.events[0] assert.strictEqual(spanEvent.content.type.length, MAX_TYPE_LENGTH) assert.strictEqual(spanEvent.content.name.length, MAX_NAME_LENGTH) @@ -180,7 +178,7 @@ describe('agentless-ci-visibility-encode', () => { const buffer = encoder.makePayload() const decodedTrace = msgpack.decode(buffer, { useBigInt64: true }) - expect(decodedTrace) + assert.ok(decodedTrace) const spanEvent = decodedTrace.events[0] assert.strictEqual(spanEvent.content.service, DEFAULT_SERVICE_NAME) assert.strictEqual(spanEvent.content.name, DEFAULT_SPAN_NAME) diff --git a/packages/dd-trace/test/encode/coverage-ci-visibility.spec.js b/packages/dd-trace/test/encode/coverage-ci-visibility.spec.js index aa47ebd1fc4..1db94b95422 100644 --- a/packages/dd-trace/test/encode/coverage-ci-visibility.spec.js +++ b/packages/dd-trace/test/encode/coverage-ci-visibility.spec.js @@ -1,15 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const msgpack = require('@msgpack/msgpack') const sinon = require('sinon') const proxyquire = require('proxyquire') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - const id = require('../../src/id') /** diff --git a/packages/dd-trace/test/encode/span-stats.spec.js b/packages/dd-trace/test/encode/span-stats.spec.js index d53584477dd..bd1a9faabde 100644 --- a/packages/dd-trace/test/encode/span-stats.spec.js +++ b/packages/dd-trace/test/encode/span-stats.spec.js @@ -2,7 +2,7 @@ const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const msgpack = require('@msgpack/msgpack') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -17,6 +17,7 @@ const { DEFAULT_SPAN_NAME, DEFAULT_SERVICE_NAME } = require('../../src/encode/tags-processors') +const processTags = require('../../src/process-tags') describe('span-stats-encode', () => { let encoder @@ -71,7 +72,8 @@ describe('span-stats-encode', () => { Lang: 'javascript', TracerVersion: '1.2.3', RuntimeID: 'some-runtime-id', - Sequence: 1 + Sequence: 1, + ProcessTags: processTags.serialized } }) diff --git a/packages/dd-trace/test/exporter.spec.js b/packages/dd-trace/test/exporter.spec.js index 969bde07478..ecdfdf9d703 100644 --- a/packages/dd-trace/test/exporter.spec.js +++ b/packages/dd-trace/test/exporter.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha -const sinon = require('sinon') const fs = require('node:fs') -require('./setup/core') +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') +require('./setup/core') const AgentExporter = require('../src/exporters/agent') const LogExporter = require('../src/exporters/log') diff --git a/packages/dd-trace/test/exporters/agent/exporter.spec.js b/packages/dd-trace/test/exporters/agent/exporter.spec.js index 9fc967c978c..c0980f8cb88 100644 --- a/packages/dd-trace/test/exporters/agent/exporter.spec.js +++ b/packages/dd-trace/test/exporters/agent/exporter.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha +const URL = require('url').URL + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../../setup/core') -const URL = require('url').URL - describe('Exporter', () => { let url let flushInterval diff --git a/packages/dd-trace/test/exporters/agent/writer.spec.js b/packages/dd-trace/test/exporters/agent/writer.spec.js index f3a5e885b1f..6823769bbfa 100644 --- a/packages/dd-trace/test/exporters/agent/writer.spec.js +++ b/packages/dd-trace/test/exporters/agent/writer.spec.js @@ -1,18 +1,16 @@ 'use strict' const assert = require('node:assert/strict') +const URL = require('url').URL -const { expect } = require('chai') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') - -const { describe, it, beforeEach, context } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') +const context = describe const sinon = require('sinon') const proxyquire = require('proxyquire') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../setup/core') -const URL = require('url').URL - function describeWriter (protocolVersion) { let Writer let writer @@ -165,9 +163,12 @@ function describeWriter (protocolVersion) { writer.flush() setTimeout(() => { - expect(log.errorWithoutTelemetry) - .to.have.been.calledWith('Error sending payload to the agent (status code: %s)', - error.status, error) + sinon.assert.calledWith( + log.errorWithoutTelemetry, + 'Error sending payload to the agent (status code: %s)', + error.status, + error + ) done() }) }) diff --git a/packages/dd-trace/test/exporters/common/agent-info-exporter.spec.js b/packages/dd-trace/test/exporters/common/agent-info-exporter.spec.js index bffd57612e8..cf85ef3e509 100644 --- a/packages/dd-trace/test/exporters/common/agent-info-exporter.spec.js +++ b/packages/dd-trace/test/exporters/common/agent-info-exporter.spec.js @@ -2,14 +2,11 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') - -const { describe, it } = require('tap').mocha +const { describe, it } = require('mocha') const sinon = require('sinon') const nock = require('nock') require('../../setup/core') - const AgentInfoExporter = require('../../../src/exporters/common/agent-info-exporter') describe('AgentInfoExporter', () => { @@ -74,7 +71,7 @@ describe('AgentInfoExporter', () => { agentInfoExporter.export(trace) sinon.assert.calledWith(writer.append, trace) sinon.assert.notCalled(writer.flush) - expect(agentInfoExporter.getUncodedTraces()).not.to.include(trace) + assert.ok(!(agentInfoExporter.getUncodedTraces()).includes(trace)) setTimeout(() => { sinon.assert.called(writer.flush) done() diff --git a/packages/dd-trace/test/exporters/common/docker.spec.js b/packages/dd-trace/test/exporters/common/docker.spec.js index 717ea240ad3..eb717b3ba44 100644 --- a/packages/dd-trace/test/exporters/common/docker.spec.js +++ b/packages/dd-trace/test/exporters/common/docker.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/exporters/common/form-data.spec.js b/packages/dd-trace/test/exporters/common/form-data.spec.js index 9b71d474bf5..b69ba61ba7f 100644 --- a/packages/dd-trace/test/exporters/common/form-data.spec.js +++ b/packages/dd-trace/test/exporters/common/form-data.spec.js @@ -2,10 +2,9 @@ const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha +const { describe, it } = require('mocha') require('../../setup/core') - const FormData = require('../../../src/exporters/common/form-data') async function streamToString (stream) { diff --git a/packages/dd-trace/test/exporters/common/request.spec.js b/packages/dd-trace/test/exporters/common/request.spec.js index 3ab168d371f..f372979568e 100644 --- a/packages/dd-trace/test/exporters/common/request.spec.js +++ b/packages/dd-trace/test/exporters/common/request.spec.js @@ -1,15 +1,15 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha -const sinon = require('sinon') -const nock = require('nock') const http = require('node:http') const zlib = require('node:zlib') + +const { describe, it, beforeEach, afterEach } = require('mocha') +const sinon = require('sinon') +const nock = require('nock') const proxyquire = require('proxyquire') require('../../setup/core') - const FormData = require('../../../src/exporters/common/form-data') const initHTTPServer = () => { diff --git a/packages/dd-trace/test/exporters/log/exporter.spec.js b/packages/dd-trace/test/exporters/log/exporter.spec.js index e79257c690b..3ff34ced4b0 100644 --- a/packages/dd-trace/test/exporters/log/exporter.spec.js +++ b/packages/dd-trace/test/exporters/log/exporter.spec.js @@ -1,7 +1,6 @@ 'use strict' -const { expect } = require('chai') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -38,7 +37,7 @@ describe('LogExporter', () => { exporter.export([span, span]) log.restore() const result = `${expectedPrefix}${span.tag}${expectedSuffix}` - expect(log).to.have.calledTwice + sinon.assert.calledTwice(log) sinon.assert.calledWithMatch(log, result) }) diff --git a/packages/dd-trace/test/exporters/span-stats/exporter.spec.js b/packages/dd-trace/test/exporters/span-stats/exporter.spec.js index 85723d7cc34..54563140160 100644 --- a/packages/dd-trace/test/exporters/span-stats/exporter.spec.js +++ b/packages/dd-trace/test/exporters/span-stats/exporter.spec.js @@ -1,16 +1,14 @@ 'use strict' const assert = require('node:assert/strict') +const URL = require('url').URL -const { expect } = require('chai') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../../setup/core') -const URL = require('url').URL - describe('span-stats exporter', () => { let url let Exporter @@ -34,8 +32,8 @@ describe('span-stats exporter', () => { it('should flush immediately on export', () => { exporter = new Exporter({ url }) - expect(writer.append).to.have.not.been.called - expect(writer.flush).to.have.not.been.called + sinon.assert.notCalled(writer.append) + sinon.assert.notCalled(writer.flush) exporter.export('') diff --git a/packages/dd-trace/test/exporters/span-stats/writer.spec.js b/packages/dd-trace/test/exporters/span-stats/writer.spec.js index 5e3c4f4e0a7..d1a4dc42e3b 100644 --- a/packages/dd-trace/test/exporters/span-stats/writer.spec.js +++ b/packages/dd-trace/test/exporters/span-stats/writer.spec.js @@ -1,11 +1,10 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../../setup/core') - const pkg = require('../../../../../package.json') let Writer diff --git a/packages/dd-trace/test/external-logger/index.spec.js b/packages/dd-trace/test/external-logger/index.spec.js index 38821afd4cc..809297a9416 100644 --- a/packages/dd-trace/test/external-logger/index.spec.js +++ b/packages/dd-trace/test/external-logger/index.spec.js @@ -1,13 +1,13 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const nock = require('nock') require('../setup/core') - const tracerLogger = require('../../src/log') describe('External Logger', () => { diff --git a/packages/dd-trace/test/flare.spec.js b/packages/dd-trace/test/flare.spec.js index 308ec07ab41..938a0a921a5 100644 --- a/packages/dd-trace/test/flare.spec.js +++ b/packages/dd-trace/test/flare.spec.js @@ -5,14 +5,12 @@ const http = require('node:http') const { channel } = require('dc-polyfill') const express = require('express') -const { assertObjectContains } = require('../../../integration-tests/helpers') - const upload = require('multer')() const proxyquire = require('proxyquire').noCallThru() -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') +const { assertObjectContains } = require('../../../integration-tests/helpers') require('./setup/core') - const { getConfigFresh } = require('./helpers/config') const debugChannel = channel('datadog:log:debug') diff --git a/packages/dd-trace/test/git_metadata_tagger.spec.js b/packages/dd-trace/test/git_metadata_tagger.spec.js index 141d4121a06..9fccf57600d 100644 --- a/packages/dd-trace/test/git_metadata_tagger.spec.js +++ b/packages/dd-trace/test/git_metadata_tagger.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha -require('./setup/core') +const { describe, it, beforeEach, afterEach } = require('mocha') -const agent = require('./plugins/agent') +require('./setup/core') const { SCI_COMMIT_SHA, SCI_REPOSITORY_URL } = require('../src/constants') +const agent = require('./plugins/agent') const DUMMY_GIT_SHA = '13851f2b092e97acebab1b73f6c0e7818e795b50' const DUMMY_REPOSITORY_URL = 'git@github.com:DataDog/sci_git_example.git' diff --git a/packages/dd-trace/test/git_properties.spec.js b/packages/dd-trace/test/git_properties.spec.js index de06aa994bb..c3872484955 100644 --- a/packages/dd-trace/test/git_properties.spec.js +++ b/packages/dd-trace/test/git_properties.spec.js @@ -1,9 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha const path = require('path') +const { describe, it } = require('mocha') + require('./setup/core') const { diff --git a/packages/dd-trace/test/guardrails/telemetry.spec.js b/packages/dd-trace/test/guardrails/telemetry.spec.js index 792fee8788d..fd502327cd5 100644 --- a/packages/dd-trace/test/guardrails/telemetry.spec.js +++ b/packages/dd-trace/test/guardrails/telemetry.spec.js @@ -93,7 +93,7 @@ describe('sendTelemetry', () => { } function assertStdinMetadata (expected) { - assert.ok(capturedStdinData != null) + assert.ok(capturedStdinData) const parsed = JSON.parse(capturedStdinData) assert.strictEqual(parsed.metadata.result, expected.result) assert.strictEqual(parsed.metadata.result_class, expected.result_class) diff --git a/packages/dd-trace/test/heap_snapshots.spec.js b/packages/dd-trace/test/heap_snapshots.spec.js index c4c934288c0..bf3f18c88df 100644 --- a/packages/dd-trace/test/heap_snapshots.spec.js +++ b/packages/dd-trace/test/heap_snapshots.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha const { mkdtempSync, readdirSync } = require('node:fs') const { tmpdir } = require('node:os') const { join } = require('node:path') const { threadId } = require('node:worker_threads') -require('./setup/core') +const { describe, it } = require('mocha') +require('./setup/core') const { start } = require('../src/heap_snapshots') const destination = mkdtempSync(join(tmpdir(), 'dd-trace-heap-snapshot-')) diff --git a/packages/dd-trace/test/histogram.spec.js b/packages/dd-trace/test/histogram.spec.js index 7e17f886a55..926d3645088 100644 --- a/packages/dd-trace/test/histogram.spec.js +++ b/packages/dd-trace/test/histogram.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('./setup/core') +const { describe, it, beforeEach } = require('mocha') +require('./setup/core') const Histogram = require('../src/histogram') describe('Histogram', () => { diff --git a/packages/dd-trace/test/id.spec.js b/packages/dd-trace/test/id.spec.js index b3f718eeef7..6739f881ab2 100644 --- a/packages/dd-trace/test/id.spec.js +++ b/packages/dd-trace/test/id.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/iitm.spec.js b/packages/dd-trace/test/iitm.spec.js index 24fb24a3fe4..2b11c01b79f 100644 --- a/packages/dd-trace/test/iitm.spec.js +++ b/packages/dd-trace/test/iitm.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, before, after } = require('tap').mocha + +const { describe, it, before, after } = require('mocha') const sinon = require('sinon') const dc = require('dc-polyfill') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/lambda/index.spec.js b/packages/dd-trace/test/lambda/index.spec.js index c252b1505ee..b0f5f9787b6 100644 --- a/packages/dd-trace/test/lambda/index.spec.js +++ b/packages/dd-trace/test/lambda/index.spec.js @@ -29,15 +29,14 @@ const restoreEnv = () => { * Loads the test agent and makes sure the hook for the * AWS Lambda function patch is re-registered. * - * @param {*} exporter defines the type of exporter for the test agent. * @returns a promise of the agent to load. */ -const loadAgent = ({ exporter = 'agent' } = {}) => { +const loadAgent = () => { // Make sure the hook is re-registered require('../../src/lambda') return agent.load([], [], { experimental: { - exporter + exporter: 'agent' } }) } diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_chat_completions_post_b8daca8d.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_chat_completions_post_b8daca8d.yaml new file mode 100644 index 00000000000..1cf42cba5f4 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_chat_completions_post_b8daca8d.yaml @@ -0,0 +1,109 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","temperature":0,"messages":[{"role":"user","content":"Invent + a character for a video game"}],"tool_choice":{"type":"function","function":{"name":"json"}},"tools":[{"type":"function","function":{"name":"json","description":"Respond + with a JSON object.","parameters":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false}}}]}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '464' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - node + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-Cxc8cvHFMENkylaqycEpNYMl7MiPs\",\n \"object\": + \"chat.completion\",\n \"created\": 1768323990,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_sdrxtylGQJySVuv8ODVMOTTl\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"json\",\n + \ \"arguments\": \"{\\\"name\\\":\\\"Zara Nightshade\\\",\\\"age\\\":28,\\\"height\\\":\\\"5'7\\\\\\\"\\\"}\"\n + \ }\n }\n ],\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 65,\n \"completion_tokens\": + 19,\n \"total_tokens\": 84,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_29330a9688\"\n}\n" + headers: + CF-RAY: + - 9bd67c088c742093-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 13 Jan 2026 17:06:30 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=Fqc_F7Rowrqreuix92cKbv1MPAO_neQepyE3CrD5g_8-1768323990-1.0.1.1-Fc.8BXyuNvFRX.aZZ0zD9zthOFqFYZhRfPZIk32.baYB7Q_lFFVj8gsMzMwcTrLTWJGuSXTLjMUMzkdxMqeYmGiEpldK.jJbQRBMyWfG2_c; + path=/; expires=Tue, 13-Jan-26 17:36:30 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=97XA0.6ERmaHlYbP4D_97VfE_UzuDrBMZbUItMAT5FE-1768323990918-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '881' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '896' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999987' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_7c8b95f20ebd40c48d7cf16926c4f211 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_chat_completions_post_cedb6b7b.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_chat_completions_post_cedb6b7b.yaml new file mode 100644 index 00000000000..ad2d32a16a7 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_chat_completions_post_cedb6b7b.yaml @@ -0,0 +1,166 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","temperature":0,"messages":[{"role":"user","content":"Invent + a character for a video game"}],"tool_choice":{"type":"function","function":{"name":"json"}},"tools":[{"type":"function","function":{"name":"json","description":"Respond + with a JSON object.","parameters":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false}}}],"stream":true,"stream_options":{"include_usage":true}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '518' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - node + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: 'data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_O3NCxnSPqpfNJSbyGZ5TprnX","type":"function","function":{"name":"json","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EVikkqZ9g7g"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Qc1FIvqm2L7Jz3"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"name"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2j8DsF7lK84MF"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RpwvB5kMO1RD"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Z"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ara"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"p48KmfzRHGcL2N"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":" + Wind"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"kFS1OrPTBXeK"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"r"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ider"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"u1G3vP26il6Tr"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\",\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dyxlDAvaad6o"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"age"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"On2U2q6hoqqtnP"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Ig4tYbRAWjzMUn"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"28"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Z73S6eRQlvt8q30"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":",\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"73QYFzW0YDfvE5"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"height"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wYqQSZENkaw"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"6Nq8cxSlE9op"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"5"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"''"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"7"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":""} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ViZdGZ6i85HVR"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"GIi7VnNE95XoKz"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"RSIrV"} + + + data: {"id":"chatcmpl-Cxc9Rr7xSrEtVgoph8jxJl7twoiid","object":"chat.completion.chunk","created":1768324041,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[],"usage":{"prompt_tokens":65,"completion_tokens":20,"total_tokens":85,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"tJm017bY6q"} + + + data: [DONE] + + + ' + headers: + CF-RAY: + - 9bd67d4a0e5f82f6-IAD + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Tue, 13 Jan 2026 17:07:21 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=7LjIwA0zXbQJNpkJNVcOUwKkl7Vtim6q73sAR_EgS0k-1768324041-1.0.1.1-6cHwErIUCaM.INoq4x2.Ch3IxfQfK1KvUskkrPSR1rhhA2zEbRQP7ri3D7yvP700N8bkzlW5FZT5Y1tz.YmmcnnlWfxgYQNbCbxS7gw5lxE; + path=/; expires=Tue, 13-Jan-26 17:37:21 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=3zPKmerOeIEOylHrOuK2ZIaBmD_uKdKTThcW5tdieu0-1768324041898-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '349' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '364' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999990' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_90a96caaad7541d4b04bba0f4a74ea6a + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_233e177b.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_233e177b.yaml new file mode 100644 index 00000000000..d6dd723fe39 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_233e177b.yaml @@ -0,0 +1,119 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"user","content":[{"type":"input_text","text":"Invent + a character for a video game"}]}],"text":{"format":{"type":"json_schema","strict":true,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false}}}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '383' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai/6.0.26 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0c0b9ab09f340a910169667b97679c81a1a099cd5f447c50b4\",\n + \ \"object\": \"response\",\n \"created_at\": 1768323991,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1768323992,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"output\": [\n {\n \"id\": \"msg_0c0b9ab09f340a910169667b97daa881a18886eacae6be9922\",\n + \ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": + [\n {\n \"type\": \"output_text\",\n \"annotations\": + [],\n \"logprobs\": [],\n \"text\": \"{\\\"name\\\":\\\"Kaelin + Frostwhisper\\\",\\\"age\\\":28,\\\"height\\\":\\\"5'9\\\\\\\"\\\"}\"\n }\n + \ ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n + \ \"format\": {\n \"type\": \"json_schema\",\n \"description\": + null,\n \"name\": \"response\",\n \"schema\": {\n \"type\": + \"object\",\n \"properties\": {\n \"name\": {\n \"type\": + \"string\"\n },\n \"age\": {\n \"type\": \"number\"\n + \ },\n \"height\": {\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"name\",\n \"age\",\n + \ \"height\"\n ],\n \"additionalProperties\": false\n + \ },\n \"strict\": true\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": + 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 50,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 21,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 71\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9bd67c103b9bd46b-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 13 Jan 2026 17:06:32 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=MUHjvGoVsoiPIZVyHiWYYsTEe_uYord6ZnoXT2RO078-1768323992-1.0.1.1-SoC_kZhcA4_cjZ4QcAu5ZkSxfjnvlIanlWuoZPQDgjBE7NDbQEEpHqW48PYwK_fyZ.MA1UcrR.YzRyjK8PDpkyyoaJ2KVJ1NGD48S6oRiXg; + path=/; expires=Tue, 13-Jan-26 17:36:32 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=tqiusFrIfAH_N7CwqJGabB2ueD07rg5M2D48Bi258NU-1768323992337-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '935' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '937' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999932' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_c081de2652a7462d87dd5014de12b0ee + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_295a27b9.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_295a27b9.yaml new file mode 100644 index 00000000000..b27a3a67eaa --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_295a27b9.yaml @@ -0,0 +1,119 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"system","content":"You are a helpful + assistant"},{"role":"user","content":[{"type":"input_text","text":"What is the + weather in Tokyo?"}]}],"store":false,"tools":[{"type":"function","name":"weather","description":"Get + the weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '465' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai/6.0.26 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_00e3d91b9c7429a20169667c69dae88197a1853ae3b5ee0d37\",\n + \ \"object\": \"response\",\n \"created_at\": 1768324201,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1768324202,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"output\": [\n {\n \"id\": \"fc_00e3d91b9c7429a20169667c6a3a9481979abfbe38e42f0711\",\n + \ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\": + \"{\\\"location\\\":\\\"Tokyo\\\"}\",\n \"call_id\": \"call_b4NUuswsekH4heYzWQjvPTKA\",\n + \ \"name\": \"weather\"\n }\n ],\n \"parallel_tool_calls\": true,\n + \ \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n + \ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [\n {\n \"type\": + \"function\",\n \"description\": \"Get the weather in a given location\",\n + \ \"name\": \"weather\",\n \"parameters\": {\n \"type\": \"object\",\n + \ \"properties\": {\n \"location\": {\n \"type\": + \"string\",\n \"description\": \"The location to get the weather + for\"\n }\n },\n \"required\": [\n \"location\"\n + \ ],\n \"additionalProperties\": false\n },\n \"strict\": + true\n }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 63,\n \"input_tokens_details\": + {\n \"cached_tokens\": 0\n },\n \"output_tokens\": 14,\n \"output_tokens_details\": + {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 77\n },\n + \ \"user\": null,\n \"metadata\": {}\n}" + headers: + CF-RAY: + - 9bd6813039540598-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 13 Jan 2026 17:10:02 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=6jrqAtCU7mdbIjJ2Pjtjfx9G9y75YUpF92qWWXFHLcw-1768324202-1.0.1.1-U.QaBnJaLFf5nsVbt2Yo2e1KkFjk67ZzEFXIsUlhoQ3IAl7.5AKim75q_R1m46Sg8wpMhYZNVi2S4TL5HswejI.ClYZZ5hRqj5VjREENZ_o; + path=/; expires=Tue, 13-Jan-26 17:40:02 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=sHlIkjAd.WQFXrUrz35HNM29sQHuQSFYHaWyrnhbv_k-1768324202342-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '486' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '993' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999720' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_25114882dbf84d208db19481a5f92bd8 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_2a4f633b.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_2a4f633b.yaml new file mode 100644 index 00000000000..7918211d463 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_2a4f633b.yaml @@ -0,0 +1,119 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"user","content":[{"type":"input_text","text":"Invent + a character for a video game"}]}],"text":{"format":{"type":"json_schema","strict":false,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false}}}}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '384' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai/5.0.92 ai-sdk/provider-utils/3.0.17 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0563ab5e54fa125f01696680a86c04819fb2991f693f918f9b\",\n + \ \"object\": \"response\",\n \"created_at\": 1768325288,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1768325289,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"output\": [\n {\n \"id\": \"msg_0563ab5e54fa125f01696680a8d0cc819fa5bac6ca4876ef34\",\n + \ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": + [\n {\n \"type\": \"output_text\",\n \"annotations\": + [],\n \"logprobs\": [],\n \"text\": \"{\\\"name\\\":\\\"Kira + Windrider\\\",\\\"age\\\":28,\\\"height\\\":\\\"5'8\\\\\\\"\\\"}\"\n }\n + \ ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": false,\n \"temperature\": 1.0,\n \"text\": {\n + \ \"format\": {\n \"type\": \"json_schema\",\n \"description\": + null,\n \"name\": \"response\",\n \"schema\": {\n \"type\": + \"object\",\n \"properties\": {\n \"name\": {\n \"type\": + \"string\"\n },\n \"age\": {\n \"type\": \"number\"\n + \ },\n \"height\": {\n \"type\": \"string\"\n + \ }\n },\n \"required\": [\n \"name\",\n \"age\",\n + \ \"height\"\n ],\n \"additionalProperties\": false\n + \ },\n \"strict\": false\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": + 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 50,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 21,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 71\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9bd69bbc4980c9ac-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 13 Jan 2026 17:28:09 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=e3TgWpbaDc0sY0K8jLDRi3_dGnOfkrie5bvkphhnBEI-1768325289-1.0.1.1-UKbbag.JbYiFw3AOi2LbDOI.oW.XkBkXJzWOPPjHAvcIsmQEs3PLMuWyvuZ8VqwSP8gFr6t2ehCBvULbwMPmLmTuMSFkG7kprOvCzS1NO4Y; + path=/; expires=Tue, 13-Jan-26 17:58:09 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=suwEnuQQMbiUPkMoQf37gDOaXmM3UmZP2qX9my7rECY-1768325289246-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '828' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '831' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999930' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_ae2041d7655a4510a5be8c570511b057 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_633c9fe3.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_633c9fe3.yaml new file mode 100644 index 00000000000..2458ee05233 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_633c9fe3.yaml @@ -0,0 +1,122 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"system","content":"You are a helpful + assistant"},{"role":"user","content":[{"type":"input_text","text":"What is the + weather in Tokyo?"}]},{"type":"function_call","call_id":"call_b4NUuswsekH4heYzWQjvPTKA","name":"weather","arguments":"{\"location\":\"Tokyo\"}","id":"fc_00e3d91b9c7429a20169667c6a3a9481979abfbe38e42f0711"},{"type":"function_call_output","call_id":"call_b4NUuswsekH4heYzWQjvPTKA","output":"{\"location\":\"Tokyo\",\"temperature\":72}"}],"store":false,"tools":[{"type":"function","name":"weather","description":"Get + the weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '778' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai/6.0.26 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_00e3d91b9c7429a20169667c6ab2608197a7df27992deb7418\",\n + \ \"object\": \"response\",\n \"created_at\": 1768324202,\n \"status\": + \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": + \"developer\"\n },\n \"completed_at\": 1768324204,\n \"error\": null,\n + \ \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"output\": [\n {\n \"id\": \"msg_00e3d91b9c7429a20169667c6b6cc881979a4e2cbf0fe2e13e\",\n + \ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": + [\n {\n \"type\": \"output_text\",\n \"annotations\": + [],\n \"logprobs\": [],\n \"text\": \"The current temperature + in Tokyo is 72\\u00b0F. If you need more detailed weather information, just + let me know!\"\n }\n ],\n \"role\": \"assistant\"\n }\n + \ ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": + null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": null,\n + \ \"reasoning\": {\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": + null,\n \"service_tier\": \"default\",\n \"store\": false,\n \"temperature\": + 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n + \ \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tools\": + [\n {\n \"type\": \"function\",\n \"description\": \"Get the + weather in a given location\",\n \"name\": \"weather\",\n \"parameters\": + {\n \"type\": \"object\",\n \"properties\": {\n \"location\": + {\n \"type\": \"string\",\n \"description\": \"The location + to get the weather for\"\n }\n },\n \"required\": [\n + \ \"location\"\n ],\n \"additionalProperties\": false\n + \ },\n \"strict\": true\n }\n ],\n \"top_logprobs\": 0,\n \"top_p\": + 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": + 90,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 25,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 115\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + CF-RAY: + - 9bd681392ce2c93c-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 13 Jan 2026 17:10:04 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=0zKmG3a4WgGlCwe2muk4XY0VnjK6ScXAG0_ybBTXZhQ-1768324204-1.0.1.1-UFFKkFSdFwoFmE7JkYhUTImRwDCrOjWMpn.3gBYJ0dTEhnI8VpS2okC.mdu5nZ0v3D6KHAXKkd4YKsZa59974BnxEHbqLyRfzN1KytNbCrE; + path=/; expires=Tue, 13-Jan-26 17:40:04 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=imRgMOqNnohahGrzRyJ.VQrsPt.EUZWmgE05yGDBYdU-1768324204233-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '1704' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '1737' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999692' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_33099c5148654126a4460d0461afbd4f + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_69f0ebec.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_69f0ebec.yaml new file mode 100644 index 00000000000..a403c0dbce5 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_69f0ebec.yaml @@ -0,0 +1,215 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"user","content":[{"type":"input_text","text":"Invent + a character for a video game"}]}],"text":{"format":{"type":"json_schema","strict":true,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false}}},"stream":true}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '397' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai-sdk/openai/3.0.7 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: 'event: response.created + + data: {"type":"response.created","response":{"id":"resp_02107f7db09be5790169667bcab01c81a1b99501802204e1cf","object":"response","created_at":1768324042,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + + event: response.in_progress + + data: {"type":"response.in_progress","response":{"id":"resp_02107f7db09be5790169667bcab01c81a1b99501802204e1cf","object":"response","created_at":1768324042,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + + event: response.output_item.added + + data: {"type":"response.output_item.added","item":{"id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + + event: response.content_part.added + + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"{\"","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"8W0VL8LuBGmah4","output_index":0,"sequence_number":4} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"name","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"6s9oKemSfd9N","output_index":0,"sequence_number":5} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\":\"","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"Rt2jGrCHMEEOs","output_index":0,"sequence_number":6} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"Za","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"8WntH0GmyLWEqN","output_index":0,"sequence_number":7} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"ra","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"19WL4O3i2oyLMA","output_index":0,"sequence_number":8} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" Wind","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"FmEISwUef","output_index":0,"sequence_number":9} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"rider","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"6XAHOCyuQP","output_index":0,"sequence_number":10} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\",\"","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"gjrO44iESpYNi","output_index":0,"sequence_number":11} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"age","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"qI1P8JPrAJ0PL","output_index":0,"sequence_number":12} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\":","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"sk2a2My3vnDoGR","output_index":0,"sequence_number":13} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"28","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"jgLyGKu50CMUZe","output_index":0,"sequence_number":14} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":",\"","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"NGh0kXru3ctlkA","output_index":0,"sequence_number":15} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"height","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"aAr5YCC9hR","output_index":0,"sequence_number":16} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\":\"","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"Eh8fLwNdaAA0z","output_index":0,"sequence_number":17} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"5","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"1efkB6TyzwAo4kT","output_index":0,"sequence_number":18} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"''","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"hXtDBqCCON4RnOP","output_index":0,"sequence_number":19} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"7","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"QIbqbRZZ9rFBkEG","output_index":0,"sequence_number":20} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\\\"","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"uztUf3niIjdIg2","output_index":0,"sequence_number":21} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\"}","item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"obfuscation":"xsngz2jgRbTPL3","output_index":0,"sequence_number":22} + + + event: response.output_text.done + + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","logprobs":[],"output_index":0,"sequence_number":23,"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"} + + + event: response.content_part.done + + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"},"sequence_number":24} + + + event: response.output_item.done + + data: {"type":"response.output_item.done","item":{"id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"}],"role":"assistant"},"output_index":0,"sequence_number":25} + + + event: response.completed + + data: {"type":"response.completed","response":{"id":"resp_02107f7db09be5790169667bcab01c81a1b99501802204e1cf","object":"response","created_at":1768324042,"status":"completed","background":false,"completed_at":1768324043,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_02107f7db09be5790169667bcaed0481a180416f1667524903","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false},"strict":true},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":50,"input_tokens_details":{"cached_tokens":0},"output_tokens":20,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":70},"user":null,"metadata":{}},"sequence_number":26} + + + ' + headers: + CF-RAY: + - 9bd67d527b5be601-IAD + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Tue, 13 Jan 2026 17:07:22 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=Y9ngFzwXlqq62DAzxTdhCRY9NA97kVx4HC8OzbuXr_0-1768324042-1.0.1.1-IXkIKC0Y4ZIzo8sNlzEixJpYgi2nv86nVwkA9xAQy_V5fDoa204oSCJcdZFGaHaaifBHz2Wgbr0fg3altBB8QWFN2.Yg05V9x45AZzSN4r4; + path=/; expires=Tue, 13-Jan-26 17:37:22 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=xHVk8nG6HLvQQy38tvMPu1t8DuDxCZFJaX5aae1eVLc-1768324042735-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '41' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '44' + x-request-id: + - req_022bd3fe23ac4ad48e0026f83f3c5bac + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c4a90328.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c4a90328.yaml new file mode 100644 index 00000000000..3fdd2321f86 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_c4a90328.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"user","content":[{"type":"input_text","text":"Invent + a character for a video game"}]}],"text":{"format":{"type":"json_schema","strict":true,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"]}}},"stream":true}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '368' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai-sdk/openai/3.0.7 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"error\": {\n \"message\": \"Invalid schema for response_format + 'response': In context=(), 'additionalProperties' is required to be supplied + and to be false.\",\n \"type\": \"invalid_request_error\",\n \"param\": + \"text.format.schema\",\n \"code\": \"invalid_json_schema\"\n }\n}" + headers: + CF-RAY: + - 9bd6812f28c71980-IAD + Connection: + - keep-alive + Content-Length: + - '273' + Content-Type: + - application/json + Date: + - Tue, 13 Jan 2026 17:10:00 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=h8GJQ2FDA.2vMsUUj3qfYv_uviO73uWn49mOTI83o7Q-1768324200-1.0.1.1-8gPW4uuLJUPMRLfrH6ehubXJ1SohL.ZGPIWNEIYcwsB3qVI5MwwCvf1aXCpnalvJeGftedMiIBa0HuoAS_3d4nfpHOOMM9W2ctM9Y9mpknQ; + path=/; expires=Tue, 13-Jan-26 17:40:00 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=oYiTy7Nk6kuuNh5qn8VxW4p.NPTINP5vamQ5LLyUiOg-1768324200928-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '15' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '18' + x-request-id: + - req_3788ac2fc3c94d358011e32029585208 + status: + code: 400 + message: Bad Request +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_deeb1df7.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_deeb1df7.yaml new file mode 100644 index 00000000000..13f22abaee4 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_deeb1df7.yaml @@ -0,0 +1,215 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"user","content":[{"type":"input_text","text":"Invent + a character for a video game"}]}],"text":{"format":{"type":"json_schema","strict":false,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false}}},"stream":true}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '398' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai-sdk/openai/2.0.65 ai-sdk/provider-utils/3.0.17 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: 'event: response.created + + data: {"type":"response.created","response":{"id":"resp_0a3e424bc772f62601696680a96a6c8191a028758172af6d05","object":"response","created_at":1768325289,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false},"strict":false},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + + event: response.in_progress + + data: {"type":"response.in_progress","response":{"id":"resp_0a3e424bc772f62601696680a96a6c8191a028758172af6d05","object":"response","created_at":1768325289,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false},"strict":false},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + + event: response.output_item.added + + data: {"type":"response.output_item.added","item":{"id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + + event: response.content_part.added + + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"{\"","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"1KhbNEYXcfa0fr","output_index":0,"sequence_number":4} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"name","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"8GJxAn8ABWQJ","output_index":0,"sequence_number":5} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\":\"","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"gGPfbpmR6tI0L","output_index":0,"sequence_number":6} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"Z","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"sJkPDLOmnvnwhoz","output_index":0,"sequence_number":7} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"ara","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"erIlVA7RutPix","output_index":0,"sequence_number":8} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" Wind","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"mNTNDdKzF","output_index":0,"sequence_number":9} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"rider","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"KQRZdTDgNhA9","output_index":0,"sequence_number":10} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\",\"","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"GnZlFvR0olvQJ","output_index":0,"sequence_number":11} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"age","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"zZgL4qTz5SXml","output_index":0,"sequence_number":12} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\":","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"xRz6ih5eZyjnuL","output_index":0,"sequence_number":13} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"28","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"Vkaow34CtM6Axd","output_index":0,"sequence_number":14} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":",\"","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"WMaH1MkJ3kyXk7","output_index":0,"sequence_number":15} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"height","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"R2BBT08tCp","output_index":0,"sequence_number":16} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\":\"","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"MUHoiFyadI1Se","output_index":0,"sequence_number":17} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"5","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"aPLCCE8LAtUDTrG","output_index":0,"sequence_number":18} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"''","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"BjHG1jQi08D1uXU","output_index":0,"sequence_number":19} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"7","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"Nml2PImc7GZpiZa","output_index":0,"sequence_number":20} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\\\"","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"cK416dzZ4tGC65","output_index":0,"sequence_number":21} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"\"}","item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"obfuscation":"N4JcY9A8N2JzZD","output_index":0,"sequence_number":22} + + + event: response.output_text.done + + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","logprobs":[],"output_index":0,"sequence_number":23,"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"} + + + event: response.content_part.done + + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"},"sequence_number":24} + + + event: response.output_item.done + + data: {"type":"response.output_item.done","item":{"id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"}],"role":"assistant"},"output_index":0,"sequence_number":25} + + + event: response.completed + + data: {"type":"response.completed","response":{"id":"resp_0a3e424bc772f62601696680a96a6c8191a028758172af6d05","object":"response","created_at":1768325289,"status":"completed","background":false,"completed_at":1768325290,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0a3e424bc772f62601696680a9b17481919a0dd51bc3b4f380","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"{\"name\":\"Zara + Windrider\",\"age\":28,\"height\":\"5''7\\\"\"}"}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"json_schema","description":null,"name":"response","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"},"height":{"type":"string"}},"required":["name","age","height"],"additionalProperties":false},"strict":false},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":50,"input_tokens_details":{"cached_tokens":0},"output_tokens":20,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":70},"user":null,"metadata":{}},"sequence_number":26} + + + ' + headers: + CF-RAY: + - 9bd69bc27a933897-IAD + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Tue, 13 Jan 2026 17:28:09 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=xqPzOHELjqiHcUGR154IYn4cS3FhgtnFPH0bqUqaz4A-1768325289-1.0.1.1-PX8Z4rhF7UKDM9UDxyMyPzcbMDy50BC36VrpOHSR59.mIamETvgQ_q4Duh_IxHkDrcgB5_fE7OwSP35qVlloI.vrSA4hqI1C_TMEK8tDi60; + path=/; expires=Tue, 13-Jan-26 17:58:09 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=mWalwpwInicPir8l6aq86AL_sNShOevA8hIR2ERu1Vw-1768325289442-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '28' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '31' + x-request-id: + - req_d820f7a739214d53b4ede6fb1aa6aaa8 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_ee5f8dde.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_ee5f8dde.yaml new file mode 100644 index 00000000000..3c6dff1420e --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_ee5f8dde.yaml @@ -0,0 +1,140 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"system","content":"You are a helpful + assistant"},{"role":"user","content":[{"type":"input_text","text":"What is the + weather in Tokyo?"}]},{"type":"function_call","call_id":"call_p7s2ST4AQhzbmgxvqttsgfv1","name":"weather","arguments":"{\"location\":\"Tokyo\"}","id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402"},{"type":"function_call_output","call_id":"call_p7s2ST4AQhzbmgxvqttsgfv1","output":"{\"location\":\"Tokyo\",\"temperature\":72}"}],"store":false,"tools":[{"type":"function","name":"weather","description":"Get + the weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"]}}],"tool_choice":"auto","stream":true}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '792' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai-sdk/openai/3.0.7 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_02521a77faa7f21d0169667c7700e081929e65ab5c59d5b441\",\"object\":\"response\",\"created_at\":1768324215,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Get + the weather in a given location\",\"name\":\"weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"The + location to get the weather for\"}},\"required\":[\"location\"],\"additionalProperties\":false},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: + response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_02521a77faa7f21d0169667c7700e081929e65ab5c59d5b441\",\"object\":\"response\",\"created_at\":1768324215,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Get + the weather in a given location\",\"name\":\"weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"The + location to get the weather for\"}},\"required\":[\"location\"],\"additionalProperties\":false},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: + response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":2}\n\nevent: + response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":3}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"The\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"x3lcMnZwKSdES\",\"output_index\":0,\"sequence_number\":4}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + current\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"ZXEwYBXR\",\"output_index\":0,\"sequence_number\":5}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + temperature\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"dW1Z\",\"output_index\":0,\"sequence_number\":6}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + in\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"FlFzxjNHOdY2J\",\"output_index\":0,\"sequence_number\":7}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + Tokyo\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"UwzhNIAdOe\",\"output_index\":0,\"sequence_number\":8}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + is\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"BWHM7SJgIp9f4\",\"output_index\":0,\"sequence_number\":9}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + \",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"KST1N4Bc56yn6qT\",\"output_index\":0,\"sequence_number\":10}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"72\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"vJYVfhaYZg9XCQ\",\"output_index\":0,\"sequence_number\":11}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"\xB0F\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"ASCnkahMAmqwSL\",\"output_index\":0,\"sequence_number\":12}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\".\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"W98RbgqwttdYKJy\",\"output_index\":0,\"sequence_number\":13}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + If\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"5UH0ojjvZrijQ\",\"output_index\":0,\"sequence_number\":14}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + you\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"hpGgwKJ12ufk\",\"output_index\":0,\"sequence_number\":15}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + need\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"4O2bKmCackZ\",\"output_index\":0,\"sequence_number\":16}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + more\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"ip3F7IIZN5a\",\"output_index\":0,\"sequence_number\":17}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + specific\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"82xUcud\",\"output_index\":0,\"sequence_number\":18}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + weather\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"ijrGEYkm\",\"output_index\":0,\"sequence_number\":19}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + details\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"bxwc2yw3\",\"output_index\":0,\"sequence_number\":20}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\",\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"bHgpgjIWWh3keer\",\"output_index\":0,\"sequence_number\":21}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + just\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"nx7OZ9iMDki\",\"output_index\":0,\"sequence_number\":22}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + let\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"TjgPhdcHqlSj\",\"output_index\":0,\"sequence_number\":23}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + me\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"s5IPlH1YsM0GP\",\"output_index\":0,\"sequence_number\":24}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + know\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"yFdndh7j40M\",\"output_index\":0,\"sequence_number\":25}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"!\",\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"obfuscation\":\"eMZ0I4UXF9d0fkR\",\"output_index\":0,\"sequence_number\":26}\n\nevent: + response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"logprobs\":[],\"output_index\":0,\"sequence_number\":27,\"text\":\"The + current temperature in Tokyo is 72\xB0F. If you need more specific weather + details, just let me know!\"}\n\nevent: response.content_part.done\ndata: + {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"output_index\":0,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The + current temperature in Tokyo is 72\xB0F. If you need more specific weather + details, just let me know!\"},\"sequence_number\":28}\n\nevent: response.output_item.done\ndata: + {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The + current temperature in Tokyo is 72\xB0F. If you need more specific weather + details, just let me know!\"}],\"role\":\"assistant\"},\"output_index\":0,\"sequence_number\":29}\n\nevent: + response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_02521a77faa7f21d0169667c7700e081929e65ab5c59d5b441\",\"object\":\"response\",\"created_at\":1768324215,\"status\":\"completed\",\"background\":false,\"completed_at\":1768324215,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4o-mini-2024-07-18\",\"output\":[{\"id\":\"msg_02521a77faa7f21d0169667c778674819295536f27ef2a6b19\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"The + current temperature in Tokyo is 72\xB0F. If you need more specific weather + details, just let me know!\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":null,\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"function\",\"description\":\"Get + the weather in a given location\",\"name\":\"weather\",\"parameters\":{\"type\":\"object\",\"properties\":{\"location\":{\"type\":\"string\",\"description\":\"The + location to get the weather for\"}},\"required\":[\"location\"],\"additionalProperties\":false},\"strict\":true}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":90,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":25,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":115},\"user\":null,\"metadata\":{}},\"sequence_number\":30}\n\n" + headers: + CF-RAY: + - 9bd6818788a2d88a-IAD + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Tue, 13 Jan 2026 17:10:15 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=2R4.T5Ov8TNKfBRJ9xCoKBwLoBgY11m3Nlah5Yvehb8-1768324215-1.0.1.1-4K1Z4YP0op7UaGSkVrgI_T3yRxkt9f18YHCbepKxHIqnGwiEL94votqrXzpKe554PhLrspqBjjtw03v0Z_Cp1de5LZwsUB_8P8pn06zd8sI; + path=/; expires=Tue, 13-Jan-26 17:40:15 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=jve4PzD18XP823xB_6nKU.kY6PaWvE9sLQgmMZf.gws-1768324215056-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '54' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '57' + x-request-id: + - req_7a37d88a209b4df4b1423063ffa2bea8 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_f370abdf.yaml b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_f370abdf.yaml new file mode 100644 index 00000000000..109d01b93cf --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/openai/openai_responses_post_f370abdf.yaml @@ -0,0 +1,140 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","input":[{"role":"system","content":"You are a helpful + assistant"},{"role":"user","content":[{"type":"input_text","text":"What is the + weather in Tokyo?"}]}],"store":false,"tools":[{"type":"function","name":"weather","description":"Get + the weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"]}}],"tool_choice":"auto","stream":true}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '479' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - ai-sdk/openai/3.0.7 ai-sdk/provider-utils/4.0.4 runtime/node.js/22 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: 'event: response.created + + data: {"type":"response.created","response":{"id":"resp_02521a77faa7f21d0169667c76507081928ced189a68b20523","object":"response","created_at":1768324214,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Get + the weather in a given location","name":"weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + + event: response.in_progress + + data: {"type":"response.in_progress","response":{"id":"resp_02521a77faa7f21d0169667c76507081928ced189a68b20523","object":"response","created_at":1768324214,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Get + the weather in a given location","name":"weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + + event: response.output_item.added + + data: {"type":"response.output_item.added","item":{"id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","type":"function_call","status":"in_progress","arguments":"","call_id":"call_p7s2ST4AQhzbmgxvqttsgfv1","name":"weather"},"output_index":0,"sequence_number":2} + + + event: response.function_call_arguments.delta + + data: {"type":"response.function_call_arguments.delta","delta":"{\"","item_id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","obfuscation":"uSXMbuTBXbngsP","output_index":0,"sequence_number":3} + + + event: response.function_call_arguments.delta + + data: {"type":"response.function_call_arguments.delta","delta":"location","item_id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","obfuscation":"6ShtXwFF","output_index":0,"sequence_number":4} + + + event: response.function_call_arguments.delta + + data: {"type":"response.function_call_arguments.delta","delta":"\":\"","item_id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","obfuscation":"2NAMgZuGhQ6ER","output_index":0,"sequence_number":5} + + + event: response.function_call_arguments.delta + + data: {"type":"response.function_call_arguments.delta","delta":"Tokyo","item_id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","obfuscation":"jtMhE3ZViq8","output_index":0,"sequence_number":6} + + + event: response.function_call_arguments.delta + + data: {"type":"response.function_call_arguments.delta","delta":"\"}","item_id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","obfuscation":"1wtAc8bwisZTjp","output_index":0,"sequence_number":7} + + + event: response.function_call_arguments.done + + data: {"type":"response.function_call_arguments.done","arguments":"{\"location\":\"Tokyo\"}","item_id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","output_index":0,"sequence_number":8} + + + event: response.output_item.done + + data: {"type":"response.output_item.done","item":{"id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","type":"function_call","status":"completed","arguments":"{\"location\":\"Tokyo\"}","call_id":"call_p7s2ST4AQhzbmgxvqttsgfv1","name":"weather"},"output_index":0,"sequence_number":9} + + + event: response.completed + + data: {"type":"response.completed","response":{"id":"resp_02521a77faa7f21d0169667c76507081928ced189a68b20523","object":"response","created_at":1768324214,"status":"completed","background":false,"completed_at":1768324214,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"fc_02521a77faa7f21d0169667c76c7888192b0b30c4ddc836402","type":"function_call","status":"completed","arguments":"{\"location\":\"Tokyo\"}","call_id":"call_p7s2ST4AQhzbmgxvqttsgfv1","name":"weather"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"function","description":"Get + the weather in a given location","name":"weather","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + location to get the weather for"}},"required":["location"],"additionalProperties":false},"strict":true}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":63,"input_tokens_details":{"cached_tokens":0},"output_tokens":14,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":77},"user":null,"metadata":{}},"sequence_number":10} + + + ' + headers: + CF-RAY: + - 9bd681830a7ad6f1-IAD + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Tue, 13 Jan 2026 17:10:14 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=U.qbGLMvsEK6Us0xafcs3QYvzzV7NDNdEh81cPHPREs-1768324214-1.0.1.1-EUNR8Z1i_1ZbLh.9oB9uyUs33QU9BKaILRrugMDqu_vRnbLGlcaz6QqcC9mLxe2P2WY6w_cMoi1yJyObl6VDdLP1FLeYHmmDPpoWtRF3NSg; + path=/; expires=Tue, 13-Jan-26 17:40:14 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=7.7tZa51rVUB397LpgGKAuKBe1UOtR7OMi28rTAvkrg-1768324214362-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-staging + openai-processing-ms: + - '61' + openai-project: + - proj_gt6TQZPRbZfoY2J9AQlEJMpd + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '66' + x-request-id: + - req_8af56e8a512f4e548690d0058e0d9543 + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/index.spec.js b/packages/dd-trace/test/llmobs/index.spec.js index 170f100ab7e..e62056b1f71 100644 --- a/packages/dd-trace/test/llmobs/index.spec.js +++ b/packages/dd-trace/test/llmobs/index.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { channel } = require('dc-polyfill') const { after, afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') @@ -138,8 +137,8 @@ describe('module', () => { site: 'datadoghq.com' }) - expect(LLMObsSpanWriterSpy().setAgentless).to.have.been.calledWith(true) - expect(LLMObsEvalMetricsWriterSpy().setAgentless).to.have.been.calledWith(true) + sinon.assert.calledWith(LLMObsSpanWriterSpy().setAgentless, true) + sinon.assert.calledWith(LLMObsEvalMetricsWriterSpy().setAgentless, true) }) }) }) @@ -152,8 +151,8 @@ describe('module', () => { } }) - expect(LLMObsSpanWriterSpy().setAgentless).to.have.been.calledWith(false) - expect(LLMObsEvalMetricsWriterSpy().setAgentless).to.have.been.calledWith(false) + sinon.assert.calledWith(LLMObsSpanWriterSpy().setAgentless, false) + sinon.assert.calledWith(LLMObsEvalMetricsWriterSpy().setAgentless, false) }) }) @@ -190,8 +189,8 @@ describe('module', () => { site: 'datadoghq.com' }) - expect(LLMObsSpanWriterSpy().setAgentless).to.have.been.calledWith(true) - expect(LLMObsEvalMetricsWriterSpy().setAgentless).to.have.been.calledWith(true) + sinon.assert.calledWith(LLMObsSpanWriterSpy().setAgentless, true) + sinon.assert.calledWith(LLMObsEvalMetricsWriterSpy().setAgentless, true) }) }) @@ -206,8 +205,8 @@ describe('module', () => { it('configures the agent-proxy writers', () => { llmobsModule.enable({ llmobs: { mlApp: 'test' } }) - expect(LLMObsSpanWriterSpy().setAgentless).to.have.been.calledWith(false) - expect(LLMObsEvalMetricsWriterSpy().setAgentless).to.have.been.calledWith(false) + sinon.assert.calledWith(LLMObsSpanWriterSpy().setAgentless, false) + sinon.assert.calledWith(LLMObsEvalMetricsWriterSpy().setAgentless, false) }) }) }) @@ -243,8 +242,8 @@ describe('module', () => { it('configures the agentless writers', () => { llmobsModule.enable({ llmobs: {}, apiKey: 'test', site: 'datadoghq.com' }) - expect(LLMObsSpanWriterSpy().setAgentless).to.have.been.calledWith(true) - expect(LLMObsEvalMetricsWriterSpy().setAgentless).to.have.been.calledWith(true) + sinon.assert.calledWith(LLMObsSpanWriterSpy().setAgentless, true) + sinon.assert.calledWith(LLMObsEvalMetricsWriterSpy().setAgentless, true) }) }) }) @@ -257,7 +256,7 @@ describe('module', () => { evalMetricAppendCh.publish(payload) - expect(LLMObsEvalMetricsWriterSpy().append).to.have.been.calledWith(payload) + sinon.assert.calledWith(LLMObsEvalMetricsWriterSpy().append, payload) }) it('removes all subscribers when disabling', () => { diff --git a/packages/dd-trace/test/llmobs/noop.spec.js b/packages/dd-trace/test/llmobs/noop.spec.js index f30eb2986c1..921eb1b9027 100644 --- a/packages/dd-trace/test/llmobs/noop.spec.js +++ b/packages/dd-trace/test/llmobs/noop.spec.js @@ -8,8 +8,8 @@ const LLMObsSDK = require('../../../dd-trace/src/llmobs/sdk') /** * Get the methods of a class - * @param {Object} clsProto - The prototype of the class - * @param {Object} [options] - The options + * @param {object} clsProto - The prototype of the class + * @param {object} [options] - The options * @param {string[]} options.ignore - The methods to ignore * @returns {string[]} The methods of the class */ diff --git a/packages/dd-trace/test/llmobs/plugins/ai/index.spec.js b/packages/dd-trace/test/llmobs/plugins/ai/index.spec.js index 28759dafa1b..512f2c5df86 100644 --- a/packages/dd-trace/test/llmobs/plugins/ai/index.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/ai/index.spec.js @@ -1,7 +1,7 @@ 'use strict' -const { useEnv } = require('../../../../../../integration-tests/helpers') const semifies = require('semifies') +const { useEnv } = require('../../../../../../integration-tests/helpers') const { withVersions } = require('../../../setup/mocha') const { NODE_MAJOR } = require('../../../../../../version') @@ -26,7 +26,7 @@ describe('Plugin', () => { OPENAI_API_KEY: '' }) - const getEvents = useLlmObs({ plugin: 'ai' }) + const { getEvents } = useLlmObs({ plugin: 'ai' }) withVersions('ai', 'ai', range, (version, _, realVersion) => { let ai @@ -109,7 +109,8 @@ describe('Plugin', () => { age: { type: 'number' }, height: { type: 'string' } }, - required: ['name', 'age', 'height'] + required: ['name', 'age', 'height'], + additionalProperties: false }) await ai.generateObject({ @@ -294,7 +295,8 @@ describe('Plugin', () => { age: { type: 'number' }, height: { type: 'string' } }, - required: ['name', 'age', 'height'] + required: ['name', 'age', 'height'], + additionalProperties: false }) const result = await ai.streamObject({ @@ -309,7 +311,7 @@ describe('Plugin', () => { const { apmSpans, llmobsSpans } = await getEvents() - const expectedCharacter = { name: 'Zara Nightshade', age: 28, height: "5'7\"" } + const expectedCharacter = { name: 'Zara Windrider', age: 28, height: "5'7\"" } const expectedWorkflowMetadata = { schema: MOCK_OBJECT, @@ -404,17 +406,6 @@ describe('Plugin', () => { const { apmSpans, llmobsSpans } = await getEvents(4) - let expectedFinalOutput - - if (semifies(openaiVersion, '>=2.0.50')) { - expectedFinalOutput = 'The current temperature in Tokyo is 72°F.' - } else if (semifies(realVersion, '>=5.0.0')) { - expectedFinalOutput = - 'The current temperature in Tokyo is 72°F. If you need more details about the weather, just let me know!' - } else { - expectedFinalOutput = 'The current weather in Tokyo is 72°F.' - } - const expectedWorkflowMetadata = {} if (semifies(realVersion, '>=5.0.0')) { expectedWorkflowMetadata.maxRetries = MOCK_NUMBER @@ -427,7 +418,7 @@ describe('Plugin', () => { name: 'generateText', spanKind: 'workflow', inputValue: 'What is the weather in Tokyo?', - outputValue: expectedFinalOutput, + outputValue: MOCK_STRING, metadata: expectedWorkflowMetadata, tags: { ml_app: 'test', integration: 'ai' }, }) @@ -496,7 +487,7 @@ describe('Plugin', () => { tool_id: toolCallId } ], - outputMessages: [{ content: expectedFinalOutput, role: 'assistant' }], + outputMessages: [{ content: MOCK_STRING, role: 'assistant' }], metrics: { input_tokens: MOCK_NUMBER, output_tokens: MOCK_NUMBER, total_tokens: MOCK_NUMBER }, tags: { ml_app: 'test', integration: 'ai' }, }) @@ -566,18 +557,6 @@ describe('Plugin', () => { const { apmSpans, llmobsSpans } = await getEvents(4) - let expectedFinalOutput - - if (semifies(openaiVersion, '>=2.0.50')) { - expectedFinalOutput = - 'The current temperature in Tokyo is 72°F. If you need more detailed weather information, feel free to ask!' - } else if (semifies(realVersion, '>=5.0.0')) { - expectedFinalOutput = - 'The current temperature in Tokyo is 72°F. If you need more details or specific forecasts, feel free to ask!' - } else { - expectedFinalOutput = 'The current weather in Tokyo is 72°F.' - } - const expectedWorkflowMetadata = {} if (semifies(realVersion, '>=5.0.0')) { expectedWorkflowMetadata.maxRetries = MOCK_NUMBER @@ -590,7 +569,7 @@ describe('Plugin', () => { name: 'streamText', spanKind: 'workflow', inputValue: 'What is the weather in Tokyo?', - outputValue: expectedFinalOutput, + outputValue: MOCK_STRING, metadata: expectedWorkflowMetadata, tags: { ml_app: 'test', integration: 'ai' }, }) @@ -667,7 +646,7 @@ describe('Plugin', () => { tool_id: toolCallId } ], - outputMessages: [{ content: expectedFinalOutput, role: 'assistant' }], + outputMessages: [{ content: MOCK_STRING, role: 'assistant' }], metrics: { input_tokens: MOCK_NUMBER, output_tokens: MOCK_NUMBER, total_tokens: MOCK_NUMBER }, tags: { ml_app: 'test', integration: 'ai' }, }) diff --git a/packages/dd-trace/test/llmobs/plugins/anthropic/index.spec.js b/packages/dd-trace/test/llmobs/plugins/anthropic/index.spec.js index 3d6884db63d..f0b36470a00 100644 --- a/packages/dd-trace/test/llmobs/plugins/anthropic/index.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/anthropic/index.spec.js @@ -1,9 +1,9 @@ 'use strict' -const { describe, before, it } = require('mocha') -const { withVersions } = require('../../../setup/mocha') const assert = require('node:assert') +const { describe, before, it } = require('mocha') const semifies = require('semifies') +const { withVersions } = require('../../../setup/mocha') const { useEnv } = require('../../../../../../integration-tests/helpers') const { @@ -42,7 +42,7 @@ describe('Plugin', () => { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '' }) - const getEvents = useLlmObs({ plugin: 'anthropic' }) + const { getEvents } = useLlmObs({ plugin: 'anthropic' }) withVersions('anthropic', '@anthropic-ai/sdk', (version, moduleName, realVersion) => { let client 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 c2c3eda6fa7..950d72a27a9 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 @@ -22,7 +22,7 @@ describe('Plugin', () => { AWS_ACCESS_KEY_ID: '00000000000000000000' }) - const getEvents = useLlmObs({ plugin: 'aws-sdk' }) + const { getEvents } = useLlmObs({ plugin: 'aws-sdk' }) withVersions('aws-sdk', ['@aws-sdk/smithy-client', 'aws-sdk'], '>=3', (version, moduleName) => { let AWS diff --git a/packages/dd-trace/test/llmobs/plugins/google-cloud-vertexai/index.spec.js b/packages/dd-trace/test/llmobs/plugins/google-cloud-vertexai/index.spec.js index 6d85555d6f4..c5ca6190bf5 100644 --- a/packages/dd-trace/test/llmobs/plugins/google-cloud-vertexai/index.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/google-cloud-vertexai/index.spec.js @@ -1,5 +1,7 @@ 'use strict' +const fs = require('node:fs') +const path = require('node:path') const { describe, it, beforeEach, afterEach, before, after } = require('mocha') const sinon = require('sinon') @@ -9,9 +11,6 @@ const { useLlmObs } = require('../../util') -const fs = require('node:fs') -const path = require('node:path') - /** * `@google-cloud/vertexai` uses `fetch` to call against their API, which cannot * be stubbed with `nock`. This function allows us to stub the `fetch` function @@ -76,7 +75,7 @@ describe('integrations', () => { } describe('vertexai', () => { - const getEvents = useLlmObs({ plugin: 'google-cloud-vertexai' }) + const { getEvents } = useLlmObs({ plugin: 'google-cloud-vertexai' }) withVersions('google-cloud-vertexai', '@google-cloud/vertexai', '>=1', version => { before(() => { diff --git a/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js b/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js index 928ac0eafac..1069e84947d 100644 --- a/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js @@ -1,8 +1,8 @@ 'use strict' +const assert = require('node:assert') const { describe, before, it } = require('mocha') const { withVersions } = require('../../../setup/mocha') -const assert = require('node:assert') const { useLlmObs, @@ -12,7 +12,7 @@ const { } = require('../../util') describe('Plugin', () => { - const getEvents = useLlmObs({ plugin: 'google-genai' }) + const { getEvents } = useLlmObs({ plugin: 'google-genai' }) withVersions('google-genai', '@google/genai', (version) => { let client diff --git a/packages/dd-trace/test/llmobs/plugins/langchain/index.spec.js b/packages/dd-trace/test/llmobs/plugins/langchain/index.spec.js index 628ebd40ee8..f4f056f7c32 100644 --- a/packages/dd-trace/test/llmobs/plugins/langchain/index.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/langchain/index.spec.js @@ -1,11 +1,12 @@ 'use strict' +const assert = require('node:assert') const { describe, it, beforeEach, before, after } = require('mocha') +const semifies = require('semifies') const { useEnv } = require('../../../../../../integration-tests/helpers') const iastFilter = require('../../../../src/appsec/iast/taint-tracking/filter') const { withVersions } = require('../../../setup/mocha') -const assert = require('node:assert') const { assertLlmObsSpanEvent, @@ -13,7 +14,6 @@ const { MOCK_STRING, useLlmObs } = require('../../util') -const semifies = require('semifies') const isDdTrace = iastFilter.isDdTrace @@ -74,7 +74,7 @@ describe('integrations', () => { } describe('langchain', () => { - const getEvents = useLlmObs({ plugin: 'langchain' }) + const { getEvents } = useLlmObs({ plugin: 'langchain' }) before(async () => { iastFilter.isDdTrace = file => { diff --git a/packages/dd-trace/test/llmobs/plugins/openai/openaiv3.spec.js b/packages/dd-trace/test/llmobs/plugins/openai/openaiv3.spec.js index 81db0fe11be..d4f9bfd7775 100644 --- a/packages/dd-trace/test/llmobs/plugins/openai/openaiv3.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/openai/openaiv3.spec.js @@ -16,7 +16,7 @@ describe('integrations', () => { let openai describe('openai', () => { - const getEvents = useLlmObs({ plugin: 'openai', closeOptions: { wipe: true } }) + const { getEvents } = useLlmObs({ plugin: 'openai', closeOptions: { wipe: true } }) withVersions('openai', 'openai', '<4', version => { const moduleRequirePath = `../../../../../../versions/openai@${version}` diff --git a/packages/dd-trace/test/llmobs/plugins/openai/openaiv4.spec.js b/packages/dd-trace/test/llmobs/plugins/openai/openaiv4.spec.js index cf88ef58e78..f276f1a7ae8 100644 --- a/packages/dd-trace/test/llmobs/plugins/openai/openaiv4.spec.js +++ b/packages/dd-trace/test/llmobs/plugins/openai/openaiv4.spec.js @@ -1,5 +1,6 @@ 'use strict' +const assert = require('node:assert') const { describe, it, beforeEach } = require('mocha') const semifies = require('semifies') @@ -13,15 +14,13 @@ const { MOCK_NUMBER } = require('../../util') -const assert = require('node:assert') - describe('integrations', () => { let openai let azureOpenai let deepseekOpenai describe('openai', () => { - const getEvents = useLlmObs({ plugin: 'openai', closeOptions: { wipe: true } }) + const { getEvents } = useLlmObs({ plugin: 'openai', closeOptions: { wipe: true } }) withVersions('openai', 'openai', '>=4', version => { const moduleRequirePath = `../../../../../../versions/openai@${version}` @@ -919,7 +918,7 @@ describe('integrations', () => { 'Image reference 2: file-BCuhT1HQ24kmtsuuzF1mh2\n\n' + 'Please provide a comprehensive analysis.' } - ]) + ], { promptMultimodal: true }) }) it('submits a response span with prompt tracking - mixed input types (url preserved)', async function () { @@ -969,7 +968,7 @@ describe('integrations', () => { 'Image reference 2: file-BCuhT1HQ24kmtsuuzF1mh2\n\n' + 'Please provide a comprehensive analysis.' } - ]) + ], { promptMultimodal: true }) }) }) diff --git a/packages/dd-trace/test/llmobs/sdk/index.spec.js b/packages/dd-trace/test/llmobs/sdk/index.spec.js index af502a19341..6217a3ecac4 100644 --- a/packages/dd-trace/test/llmobs/sdk/index.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/index.spec.js @@ -265,7 +265,7 @@ describe('sdk', () => { it.skip('starts a span with a distinct trace id', () => { llmobs.trace({ kind: 'workflow', name: 'test' }, span => { const traceId = LLMObsTagger.tagMap.get(span)['_ml_obs.trace_id'] - assert.ok(traceId != null) + assert.ok(traceId) assert.notStrictEqual(traceId, span.context().toTraceId(true)) }) }) @@ -578,7 +578,7 @@ describe('sdk', () => { const wrappedMyWorkflow = llmobs.wrap({ kind: 'workflow' }, myWorkflow) wrappedMyWorkflow('input', (err, res) => { - assert.ok(err != null) + assert.ok(err) assert.strictEqual(res, 'output') }) @@ -670,7 +670,7 @@ describe('sdk', () => { const span = llmobs._active() const traceId = span.context()._tags['_ml_obs.trace_id'] - assert.ok(traceId != null) + assert.ok(traceId) assert.notStrictEqual(traceId, span.context().toTraceId(true)) }) diff --git a/packages/dd-trace/test/llmobs/sdk/integration.spec.js b/packages/dd-trace/test/llmobs/sdk/integration.spec.js index 8fe471b90b0..53a61bece9d 100644 --- a/packages/dd-trace/test/llmobs/sdk/integration.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/integration.spec.js @@ -6,7 +6,7 @@ const { after, afterEach, before, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') const agent = require('../../plugins/agent') -const { useLlmObs, assertLlmObsSpanEvent } = require('../util') +const { useLlmObs, assertLlmObsSpanEvent, assertLlmObsEvaluationMetric } = require('../util') function getTag (llmobsSpan, tagName) { const tag = llmobsSpan.tags.find(tag => tag.split(':')[0] === tagName) return tag?.split(':')[1] @@ -16,7 +16,7 @@ describe('end to end sdk integration tests', () => { let tracer let llmobs - const getEvents = useLlmObs() + const { getEvents, getEvaluationMetrics } = useLlmObs() before(() => { tracer = require('../../../../dd-trace') @@ -120,38 +120,38 @@ describe('end to end sdk integration tests', () => { Date.now.restore() }) - // TODO(sabrenner): follow-up on re-enabling this test in a different PR - it.skip('submits evaluations', () => { + it('submits evaluations', async () => { llmobs.trace({ kind: 'agent', name: 'myAgent' }, () => { llmobs.annotate({ inputData: 'hello', outputData: 'world' }) const spanCtx = llmobs.exportSpan() llmobs.submitEvaluation(spanCtx, { label: 'foo', metricType: 'categorical', - value: 'bar' + value: 'bar', + tags: { + foo: 'bar' + } }) }) - // const { spans, llmobsSpans, evaluationMetrics } = run(payloadGenerator) - // assert.strictEqual(spans.length, 1) - // assert.strictEqual(llmobsSpans.length, 1) - // assert.strictEqual(evaluationMetrics.length, 1) - - // // check eval metrics content - // const expected = [ - // { - // trace_id: spans[0].context().toTraceId(true), - // span_id: spans[0].context().toSpanId(), - // label: 'foo', - // metric_type: 'categorical', - // categorical_value: 'bar', - // ml_app: 'test', - // timestamp_ms: 1234567890, - // tags: [`ddtrace.version:${tracerVersion}`, 'ml_app:test'] - // } - // ] - - // check(expected, evaluationMetrics) + const { apmSpans, llmobsSpans } = await getEvents() + const llmobsEvaluationMetrics = await getEvaluationMetrics() + + assert.equal(apmSpans.length, 1) + assert.equal(llmobsSpans.length, 1) + assert.equal(llmobsEvaluationMetrics.length, 1) + + assertLlmObsEvaluationMetric(llmobsEvaluationMetrics[0], { + traceId: llmobsSpans[0].trace_id, + spanId: llmobsSpans[0].span_id, + label: 'foo', + metricType: 'categorical', + mlApp: 'test', + value: 'bar', + tags: { + foo: 'bar' + } + }) }) }) diff --git a/packages/dd-trace/test/llmobs/sdk/typescript/index.spec.js b/packages/dd-trace/test/llmobs/sdk/typescript/index.spec.js index 8f5c64dd418..8fafac23fdc 100644 --- a/packages/dd-trace/test/llmobs/sdk/typescript/index.spec.js +++ b/packages/dd-trace/test/llmobs/sdk/typescript/index.spec.js @@ -1,14 +1,14 @@ 'use strict' -const { describe, it, beforeEach, afterEach } = require('mocha') const path = require('node:path') const { execSync } = require('node:child_process') +const { describe, it, beforeEach, afterEach } = require('mocha') const { FakeAgent, sandboxCwd, useSandbox, - spawnProc + spawnProcAndExpectExit } = require('../../../../../../integration-tests/helpers') const { assertLlmObsSpanEvent } = require('../../util') @@ -103,7 +103,7 @@ describe('typescript', () => { { cwd, stdio: 'inherit' } ) - proc = await spawnProc( + proc = await spawnProcAndExpectExit( path.join(cwd, `${file}.js`), { cwd, env: { DD_TRACE_AGENT_PORT: agent.port, DD_TAGS: 'foo:bar, bar:baz' } } ) diff --git a/packages/dd-trace/test/llmobs/span_processor.spec.js b/packages/dd-trace/test/llmobs/span_processor.spec.js index f406eafe194..48502609477 100644 --- a/packages/dd-trace/test/llmobs/span_processor.spec.js +++ b/packages/dd-trace/test/llmobs/span_processor.spec.js @@ -280,7 +280,7 @@ describe('span processor', () => { assert.strictEqual(payload.meta['error.message'], 'error message') assert.strictEqual(payload.meta['error.type'], 'Error') - assert.ok(payload.meta['error.stack'] != null) + assert.ok(payload.meta['error.stack']) assert.strictEqual(payload.status, 'error') assertObjectContains(payload.tags, ['error_type:Error']) diff --git a/packages/dd-trace/test/llmobs/util.js b/packages/dd-trace/test/llmobs/util.js index bc238c2c805..f320e6b0ad6 100644 --- a/packages/dd-trace/test/llmobs/util.js +++ b/packages/dd-trace/test/llmobs/util.js @@ -1,9 +1,9 @@ 'use strict' -const { before, beforeEach, after } = require('mocha') const util = require('node:util') -const agent = require('../plugins/agent') const assert = require('node:assert') +const { before, beforeEach, after } = require('mocha') +const agent = require('../plugins/agent') const { useEnv } = require('../../../../integration-tests/helpers') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../src/constants') @@ -18,25 +18,38 @@ const MOCK_NOT_NULLISH = Symbol('not-nullish') * @typedef {{ * spanKind: 'llm' | 'embedding' | 'agent' | 'workflow' | 'task' | 'tool' | 'retrieval', * name: string, - * inputMessages: { [key: string]: any }, - * outputMessages: { [key: string]: any }, - * inputDocuments: { [key: string]: any }, - * outputDocuments: { [key: string]: any }, - * inputValue: { [key: string]: any }, - * outputValue: { [key: string]: any }, + * inputMessages: Record, + * outputMessages: Record, + * inputDocuments: Record, + * outputDocuments: Record, + * inputValue: Record, + * outputValue: Record, * metrics: { [key: string]: number }, - * metadata: { [key: string]: any }, + * metadata: Record, * modelName?: string, * modelProvider?: string, * parentId?: string, * error?: { message: string, type: string, stack: string }, * span: unknown, * sessionId?: string, - * tags: { [key: string]: any }, + * tags: Record, * traceId?: string, * }} ExpectedLLMObsSpanEvent */ +/** + * @typedef {{ + * label: string, + * traceId: string, + * spanId: string, + * metricType: 'categorical' | 'score', + * mlApp: string, + * timestamp?: number, + * value: string | number, + * tags?: { [key: string]: string }, + * }} ExpectedLLMObsEvaluationMetrics + */ + /** * * @param {object} actual @@ -202,6 +215,11 @@ function assertLlmObsSpanEvent (actual, expected) { const expectedTags = expectedLLMObsTags({ span, tags, error, sessionId }) const sortedExpectedTags = [...expectedTags.sort()] const sortedActualTags = [...actualTags.sort()] + if (sortedExpectedTags.length !== sortedActualTags.length) { + assert.fail( + `tags have different length than expected (${sortedExpectedTags.length} !== ${sortedActualTags.length})` + ) + } for (let i = 0; i < sortedExpectedTags.length; i++) { assert.equal( sortedActualTags[i], @@ -251,6 +269,66 @@ function assertLlmObsSpanEvent (actual, expected) { assert.deepStrictEqual(actual, expectedSpanEvent) } +/** + * Asserts that the actual LLMObs evaluation metric matches the evaluation metric created from the expected fields. + * + * Dynamic fields, like tags and timestamp, can be asserted with mock values. + * @param {object} actual + * @param {ExpectedLLMObsEvaluationMetrics} expected + */ +function assertLlmObsEvaluationMetric (actual, expected) { + const { + label, + traceId = MOCK_STRING, + spanId = MOCK_STRING, + metricType, + mlApp, + timestamp = MOCK_NUMBER, + value, + tags + } = expected + + const actualTags = actual.tags + const actualTimestamp = actual.timestamp_ms + + delete actual.tags + delete actual.timestamp_ms + + assertWithMockValues(actualTimestamp, timestamp, 'timestamp_ms') + + const expectedTags = [ + `ddtrace.version:${tracerVersion}`, + `ml_app:${mlApp}`, + ...Object.entries(tags ?? {}).map(([key, value]) => `${key}:${value}`), + ] + const sortedExpectedTags = [...expectedTags.sort()] + const sortedActualTags = [...actualTags.sort()] + if (sortedExpectedTags.length !== sortedActualTags.length) { + assert.fail( + `Tags have different length than expected (${sortedExpectedTags.length} !== ${sortedActualTags.length}). + Diff: ${util.inspect(sortedExpectedTags)} !== ${util.inspect(sortedActualTags)}.` + ) + } + for (let i = 0; i < sortedExpectedTags.length; i++) { + assert.equal( + sortedActualTags[i], + sortedExpectedTags[i], + `tags[${i}] does not match expected (${sortedExpectedTags[i]} !== ${sortedActualTags[i]})` + ) + } + + const expectedEvaluationMetric = { + span_id: spanId, + trace_id: traceId, + label, + metric_type: metricType, + ml_app: mlApp, + [`${metricType}_value`]: value, + } + + assert.deepStrictEqual(actual, expectedEvaluationMetric) +} + function expectedLLMObsTags ({ span, error, @@ -290,18 +368,21 @@ function fromBuffer (spanProperty, isNumber = false) { } /** - * @param {Object} options + * @param {object} options * @param {string} options.plugin - * @param {Object} options.tracerConfigOptions - * @param {Object} options.closeOptions - * @returns {function(): Promise<{ apmSpans: Array, llmobsSpans: Array }>} + * @param {object} options.tracerConfigOptions + * @param {object} options.closeOptions + * @returns {{ + * getEvents: () => Promise<{ apmSpans: Array, llmobsSpans: Array }>, + * getEvaluationMetrics: () => Promise> + * }} */ function useLlmObs ({ plugin, tracerConfigOptions = {}, closeOptions = {} } = {}) { - /** @type {Promise>>} */ + /** @type {Promise>>} */ let apmTracesPromise const resetTracesPromises = () => { @@ -332,24 +413,33 @@ function useLlmObs ({ return agent.close({ ritmReset: false, ...closeOptions }) }) - return async function (numLlmObsSpans = 1) { - // get apm spans from the agent - const apmSpans = await apmTracesPromise - resetTracesPromises() - - // get llmobs span events requests from the agent - // because llmobs process spans on span finish and submits periodically, - // we need to aggregate all of the span events - // tests should know how many spans they expect to see, otherwise tests will timeout - const llmobsSpans = [] - - while (llmobsSpans.length < numLlmObsSpans) { - await new Promise(resolve => setImmediate(resolve)) - const llmobsSpanEventsRequests = agent.getLlmObsSpanEventsRequests(true) - llmobsSpans.push(...getLlmObsSpansFromRequests(llmobsSpanEventsRequests)) + return { + getEvents: async function (numLlmObsSpans = 1) { + // get apm spans from the agent + const apmSpans = await apmTracesPromise + resetTracesPromises() + + // get llmobs span events requests from the agent + // because llmobs process spans on span finish and submits periodically, + // we need to aggregate all of the span events + // tests should know how many spans they expect to see, otherwise tests will timeout + const llmobsSpans = [] + + while (llmobsSpans.length < numLlmObsSpans) { + await new Promise(resolve => setImmediate(resolve)) + const llmobsSpanEventsRequests = agent.getLlmObsSpanEventsRequests(true) + llmobsSpans.push(...getLlmObsSpansFromRequests(llmobsSpanEventsRequests)) + } + + return { apmSpans, llmobsSpans: llmobsSpans.sort((a, b) => a.start_ns - b.start_ns) } + }, + + getEvaluationMetrics: function () { + const evaluationMetricsRequests = agent.getLlmObsEvaluationMetricsRequests(true) + return evaluationMetricsRequests + .flatMap(request => request.data.attributes.metrics) + .sort((a, b) => a.timestamp_ms - b.timestamp_ms) } - - return { apmSpans, llmobsSpans: llmobsSpans.sort((a, b) => a.start_ns - b.start_ns) } } } @@ -366,8 +456,18 @@ function getLlmObsSpansFromRequests (llmobsSpanEventsRequests) { * @param {object} spanEvent - The LLMObs span event to verify * @param {object} expectedPrompt - Expected prompt metadata (id, version, variables, chat_template) * @param {Array<{role: string, content: string}>} expectedInputMessages - Expected input messages + * @param {object} options - Optional configuration + * @param {string} options.promptTrackingInstrumentationMethod - Expected prompt tracking instrumentation method + * ('auto' or 'manual'), defaults to 'auto' + * @param {boolean} options.promptMultimodal - Whether prompt contains multimodal inputs, + * defaults to false */ -function assertPromptTracking (spanEvent, expectedPrompt, expectedInputMessages) { +function assertPromptTracking ( + spanEvent, + expectedPrompt, + expectedInputMessages, + { promptTrackingInstrumentationMethod = 'auto', promptMultimodal = false } = {} +) { // Verify input messages are captured from instructions assert(spanEvent.meta.input.messages, 'Input messages should be present') assert(Array.isArray(spanEvent.meta.input.messages), 'Input messages should be an array') @@ -385,9 +485,17 @@ function assertPromptTracking (spanEvent, expectedPrompt, expectedInputMessages) assert.strictEqual(prompt.version, expectedPrompt.version) assert.deepStrictEqual(prompt.variables, expectedPrompt.variables) assert.deepStrictEqual(prompt.chat_template, expectedPrompt.chat_template) + + // Verify tags + assert(spanEvent.tags, 'Span event should include tags') + assert(spanEvent.tags.includes(`prompt_tracking_instrumentation_method:${promptTrackingInstrumentationMethod}`)) + if (promptMultimodal) { + assert(spanEvent.tags.includes('prompt_multimodal:true')) + } } module.exports = { + assertLlmObsEvaluationMetric, assertLlmObsSpanEvent, assertPromptTracking, useLlmObs, diff --git a/packages/dd-trace/test/llmobs/writers/base.spec.js b/packages/dd-trace/test/llmobs/writers/base.spec.js index 2ac8950ee63..22f925c323c 100644 --- a/packages/dd-trace/test/llmobs/writers/base.spec.js +++ b/packages/dd-trace/test/llmobs/writers/base.spec.js @@ -2,7 +2,6 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') const { afterEach, beforeEach, describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') @@ -267,8 +266,7 @@ describe('BaseLLMObsWriter', () => { sinon.assert.calledWith(clearInterval, writer._periodic) sinon.assert.calledWith(process.removeListener, 'beforeExit', writer._beforeExitHandler) sinon.assert.calledOnce(writer.flush) - expect(logger.debug) - .to.have.been.calledWith('Stopping BaseLLMObsWriter') + sinon.assert.calledWith(logger.debug, 'Stopping BaseLLMObsWriter') }) it('does not destroy more than once', () => { diff --git a/packages/dd-trace/test/log.spec.js b/packages/dd-trace/test/log.spec.js index 376d29c8b54..998a37821df 100644 --- a/packages/dd-trace/test/log.spec.js +++ b/packages/dd-trace/test/log.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('./setup/core') - const { storage } = require('../../datadog-core') /* eslint-disable no-console */ @@ -205,7 +205,7 @@ describe('log', () => { sinon.assert.calledOnce(console.debug) assert.match(console.debug.firstCall.args[0], - /^Trace: Test.foo\('argument', { hello: 'world' }, Foo { bar: 'baz' }\)/ + /^Trace: Context.foo\('argument', { hello: 'world' }, Foo { bar: 'baz' }\)/ ) assert.ok(console.debug.firstCall.args[0].split('\n').length >= 3) }) diff --git a/packages/dd-trace/test/msgpack/encoder.spec.js b/packages/dd-trace/test/msgpack/encoder.spec.js index 705dc6f16dd..5f81c418c5a 100644 --- a/packages/dd-trace/test/msgpack/encoder.spec.js +++ b/packages/dd-trace/test/msgpack/encoder.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const msgpack = require('@msgpack/msgpack') require('../setup/core') - const { MsgpackEncoder } = require('../../src/msgpack/encoder') function randString (length) { diff --git a/packages/dd-trace/test/noop.spec.js b/packages/dd-trace/test/noop.spec.js index f7a32d18679..db954392b34 100644 --- a/packages/dd-trace/test/noop.spec.js +++ b/packages/dd-trace/test/noop.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('./setup/core') +const { describe, it, beforeEach } = require('mocha') +require('./setup/core') const Span = require('../src/noop/span') describe('NoopTracer', () => { diff --git a/packages/dd-trace/test/openfeature/flagging_provider.spec.js b/packages/dd-trace/test/openfeature/flagging_provider.spec.js index ebcf9bca624..505559dd775 100644 --- a/packages/dd-trace/test/openfeature/flagging_provider.spec.js +++ b/packages/dd-trace/test/openfeature/flagging_provider.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -63,14 +64,14 @@ describe('FlaggingProvider', () => { it('should create exposure channel', () => { const provider = new FlaggingProvider(mockTracer, mockConfig) - assert.ok(provider != null) + assert.ok(provider) sinon.assert.calledWith(channelStub, 'ffe:exposure:submit') }) it('should log debug message on creation', () => { const provider = new FlaggingProvider(mockTracer, mockConfig) - assert.ok(provider != null) + assert.ok(provider) sinon.assert.calledWith(log.debug, 'FlaggingProvider created with timeout: 30000ms') }) }) diff --git a/packages/dd-trace/test/openfeature/flagging_provider_timeout.spec.js b/packages/dd-trace/test/openfeature/flagging_provider_timeout.spec.js index adeec79c0ba..da6741fb9eb 100644 --- a/packages/dd-trace/test/openfeature/flagging_provider_timeout.spec.js +++ b/packages/dd-trace/test/openfeature/flagging_provider_timeout.spec.js @@ -76,7 +76,7 @@ describe('FlaggingProvider Initialization Timeout', () => { }) // Verify initialization is in progress - assert.ok(provider.initController != null) + assert.ok(provider.initController) assert.strictEqual(provider.initController.isInitializing(), true) // Advance time by 30 seconds (default timeout) and run pending promises diff --git a/packages/dd-trace/test/openfeature/index.spec.js b/packages/dd-trace/test/openfeature/index.spec.js index a68cf1c0137..aadf4ee0697 100644 --- a/packages/dd-trace/test/openfeature/index.spec.js +++ b/packages/dd-trace/test/openfeature/index.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') +const { channel } = require('dc-polyfill') require('../setup/core') -const { channel } = require('dc-polyfill') - describe('OpenFeature Module', () => { let config let openfeatureModule diff --git a/packages/dd-trace/test/openfeature/noop.spec.js b/packages/dd-trace/test/openfeature/noop.spec.js index 7b7bd5f7e23..c6a95c7c2ac 100644 --- a/packages/dd-trace/test/openfeature/noop.spec.js +++ b/packages/dd-trace/test/openfeature/noop.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../setup/core') const NoopFlaggingProvider = require('../../src/openfeature/noop') describe('NoopFlaggingProvider', () => { diff --git a/packages/dd-trace/test/openfeature/remote_config.spec.js b/packages/dd-trace/test/openfeature/remote_config.spec.js new file mode 100644 index 00000000000..2cdc50c3e6d --- /dev/null +++ b/packages/dd-trace/test/openfeature/remote_config.spec.js @@ -0,0 +1,122 @@ +'use strict' + +const { describe, it, beforeEach } = require('mocha') +const sinon = require('sinon') + +const RemoteConfigCapabilities = require('../../src/remote_config/capabilities') +const { enable } = require('../../src/openfeature/remote_config') + +require('../setup/mocha') + +describe('OpenFeature Remote Config', () => { + let rc + let config + let openfeatureProxy + let getOpenfeatureProxy + let handlers + + beforeEach(() => { + handlers = new Map() + + rc = { + updateCapabilities: sinon.spy(), + setProductHandler: sinon.spy((product, handler) => { + handlers.set(product, handler) + }) + } + + config = { + experimental: { + flaggingProvider: { + enabled: true + } + } + } + + openfeatureProxy = { + _setConfiguration: sinon.spy() + } + + getOpenfeatureProxy = sinon.stub().returns(openfeatureProxy) + }) + + describe('enable', () => { + it('should enable FFE_FLAG_CONFIGURATION_RULES capability', () => { + enable(rc, config, getOpenfeatureProxy) + + sinon.assert.calledOnceWithExactly( + rc.updateCapabilities, + RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, + true + ) + }) + + it('should register FFE_FLAGS product handler', () => { + enable(rc, config, getOpenfeatureProxy) + + sinon.assert.calledOnceWithExactly(rc.setProductHandler, 'FFE_FLAGS', sinon.match.func) + }) + + it('should call _setConfiguration on apply action when feature is enabled', () => { + enable(rc, config, getOpenfeatureProxy) + + const flagConfig = { flags: { 'test-flag': {} } } + const handler = handlers.get('FFE_FLAGS') + + handler('apply', flagConfig) + + sinon.assert.calledOnceWithExactly(openfeatureProxy._setConfiguration, flagConfig) + }) + + it('should call _setConfiguration on modify action when feature is enabled', () => { + enable(rc, config, getOpenfeatureProxy) + + const flagConfig = { flags: { 'modified-flag': {} } } + const handler = handlers.get('FFE_FLAGS') + + handler('modify', flagConfig) + + sinon.assert.calledOnceWithExactly(openfeatureProxy._setConfiguration, flagConfig) + }) + + it('should not call _setConfiguration on unapply action', () => { + enable(rc, config, getOpenfeatureProxy) + + const flagConfig = { flags: { 'test-flag': {} } } + const handler = handlers.get('FFE_FLAGS') + + handler('unapply', flagConfig) + + sinon.assert.notCalled(openfeatureProxy._setConfiguration) + }) + + it('should not call _setConfiguration on unknown action', () => { + enable(rc, config, getOpenfeatureProxy) + + const flagConfig = { flags: { 'test-flag': {} } } + const handler = handlers.get('FFE_FLAGS') + + handler('unknown', flagConfig) + + sinon.assert.notCalled(openfeatureProxy._setConfiguration) + }) + + it('should not register product handler when experimental feature is disabled', () => { + config.experimental.flaggingProvider.enabled = false + enable(rc, config, getOpenfeatureProxy) + + sinon.assert.notCalled(rc.setProductHandler) + }) + + it('should still enable capability even when experimental feature is disabled', () => { + config.experimental.flaggingProvider.enabled = false + enable(rc, config, getOpenfeatureProxy) + + sinon.assert.calledOnceWithExactly( + rc.updateCapabilities, + RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, + true + ) + }) + }) +}) diff --git a/packages/dd-trace/test/openfeature/writers/exposures.spec.js b/packages/dd-trace/test/openfeature/writers/exposures.spec.js index 7fea6ce05fb..55f817417e8 100644 --- a/packages/dd-trace/test/openfeature/writers/exposures.spec.js +++ b/packages/dd-trace/test/openfeature/writers/exposures.spec.js @@ -1,7 +1,8 @@ 'use strict' -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const assert = require('node:assert/strict') + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -71,14 +72,14 @@ describe('OpenFeature Exposures Writer', () => { describe('constructor', () => { it('should initialize with correct defaults', () => { - expect(writer._interval).to.equal(1000) - expect(writer._timeout).to.equal(5000) - expect(writer._bufferLimit).to.equal(1000) - expect(writer._buffer).to.be.an('array').that.is.empty + assert.strictEqual(writer._interval, 1000) + assert.strictEqual(writer._timeout, 5000) + assert.strictEqual(writer._bufferLimit, 1000) + assert.deepStrictEqual(writer._buffer, []) }) it('should set up periodic flushing', () => { - expect(writer._periodic).to.exist + assert.ok(writer._periodic) }) }) @@ -90,8 +91,8 @@ describe('OpenFeature Exposures Writer', () => { it('should add exposure event to buffer', () => { writer.append(exposureEvent) - expect(writer._buffer).to.have.length(1) - expect(writer._buffer[0]).to.equal(exposureEvent) + assert.strictEqual(writer._buffer?.length, 1) + assert.strictEqual(writer._buffer[0], exposureEvent) }) it('should track buffer size', () => { @@ -99,7 +100,7 @@ describe('OpenFeature Exposures Writer', () => { writer.append(exposureEvent) - expect(writer._bufferSize).to.be.greaterThan(initialSize) + assert.ok(writer._bufferSize > initialSize) }) it('should drop events when buffer is full', () => { @@ -109,9 +110,9 @@ describe('OpenFeature Exposures Writer', () => { writer.append(exposureEvent) writer.append(exposureEvent) // Should be dropped - expect(writer._buffer).to.have.length(2) - expect(writer._droppedEvents).to.equal(1) - expect(log.warn).to.have.been.calledOnce + assert.strictEqual(writer._buffer?.length, 2) + assert.strictEqual(writer._droppedEvents, 1) + sinon.assert.calledOnce(log.warn) }) it('should drop events exceeding 1MB size limit', () => { @@ -122,11 +123,9 @@ describe('OpenFeature Exposures Writer', () => { writer.append(largeEvent) - expect(writer._buffer).to.have.length(0) - expect(writer._droppedEvents).to.equal(1) - expect(log.warn).to.have.been.calledWith( - sinon.match(/event size[\s\S]*bytes exceeds limit/) - ) + assert.strictEqual(writer._buffer?.length, 0) + assert.strictEqual(writer._droppedEvents, 1) + sinon.assert.calledWith(log.warn, sinon.match(/event size[\s\S]*bytes exceeds limit/)) }) it('should flush when payload would exceed 5MB limit', () => { @@ -137,18 +136,16 @@ describe('OpenFeature Exposures Writer', () => { } writer.append(largeEvent) // First event, buffer = ~2MB - expect(writer._buffer).to.have.length(1) + assert.strictEqual(writer._buffer?.length, 1) writer.append(largeEvent) // Second event, buffer = ~4MB - expect(writer._buffer).to.have.length(2) + assert.strictEqual(writer._buffer?.length, 2) writer.append(largeEvent) // Third event would make buffer ~6MB, should trigger flush - expect(log.debug).to.have.been.calledWith( - sinon.match(/buffer size would exceed .* bytes, flushing first/) - ) + sinon.assert.calledWith(log.debug, sinon.match(/buffer size would exceed .* bytes, flushing first/)) // After flush is triggered, buffer should be cleared and new event added - expect(writer._buffer).to.have.length(1) + assert.strictEqual(writer._buffer?.length, 1) }) it('should buffer events when disabled', () => { @@ -156,9 +153,9 @@ describe('OpenFeature Exposures Writer', () => { writer.append(exposureEvent) - expect(writer._buffer).to.have.length(0) // Event should not be in main buffer - expect(writer._pendingEvents).to.have.length(1) // Should be in pending events - expect(writer._pendingEvents[0].event).to.equal(exposureEvent) + assert.strictEqual(writer._buffer?.length, 0) // Event should not be in main buffer + assert.strictEqual(writer._pendingEvents?.length, 1) // Should be in pending events + assert.strictEqual(writer._pendingEvents[0].event, exposureEvent) }) }) @@ -167,17 +164,17 @@ describe('OpenFeature Exposures Writer', () => { const events = [exposureEvent] const payload = writer.makePayload(events) - expect(payload).to.be.an('object') - expect(payload).to.have.property('context') - expect(payload).to.have.property('exposures') - expect(payload.exposures).to.be.an('array').with.length(1) + assert.ok(payload !== null && typeof payload === 'object' && !Array.isArray(payload)) + assert.ok(Object.hasOwn(payload, 'context')) + assert.ok(Object.hasOwn(payload, 'exposures')) + assert.strictEqual(payload.exposures?.length, 1) }) it('should include service metadata in context', () => { const events = [exposureEvent] const payload = writer.makePayload(events) - expect(payload.context).to.deep.equal({ + assert.deepStrictEqual(payload.context, { service: 'test-service', version: '1.0.0', env: 'test' @@ -189,7 +186,7 @@ describe('OpenFeature Exposures Writer', () => { const payload = writer.makePayload(events) const formattedEvent = payload.exposures[0] - expect(formattedEvent).to.deep.equal({ + assert.deepStrictEqual(formattedEvent, { timestamp: 1672531200000, allocation: { key: 'allocation_123' }, flag: { key: 'test_flag' }, @@ -212,11 +209,11 @@ describe('OpenFeature Exposures Writer', () => { const events = [exposureEvent] const payload = writerWithoutOptionals.makePayload(events) - expect(payload.context).to.deep.equal({ + assert.deepStrictEqual(payload.context, { service: 'test-service' }) - expect(payload.context).to.not.have.property('version') - expect(payload.context).to.not.have.property('env') + assert.ok(!(Object.hasOwn(payload.context, 'version'))) + assert.ok(!(Object.hasOwn(payload.context, 'env'))) }) it('should handle flat format with dot notation', () => { @@ -231,12 +228,12 @@ describe('OpenFeature Exposures Writer', () => { const payload = writer.makePayload([flatEvent]) const formattedEvent = payload.exposures[0] - expect(formattedEvent.allocation.key).to.equal('allocation_123') - expect(formattedEvent.flag.key).to.equal('test_flag') - expect(formattedEvent.variant.key).to.equal('A') - expect(formattedEvent.subject.id).to.equal('user_123') - expect(formattedEvent.subject.type).to.be.undefined - expect(formattedEvent.subject.attributes).to.be.undefined + assert.strictEqual(formattedEvent.allocation.key, 'allocation_123') + assert.strictEqual(formattedEvent.flag.key, 'test_flag') + assert.strictEqual(formattedEvent.variant.key, 'A') + assert.strictEqual(formattedEvent.subject.id, 'user_123') + assert.strictEqual(formattedEvent.subject.type, undefined) + assert.strictEqual(formattedEvent.subject.attributes, undefined) }) }) @@ -248,7 +245,7 @@ describe('OpenFeature Exposures Writer', () => { it('should skip flushing when buffer is empty', () => { writer.flush() - expect(request).to.not.have.been.called + sinon.assert.called(request) }) it('should skip flushing when writer is disabled', () => { @@ -257,7 +254,7 @@ describe('OpenFeature Exposures Writer', () => { writer.flush() - expect(request).to.not.have.been.called + sinon.assert.called(request) }) it('should flush events to agent via EVP proxy', () => { @@ -265,31 +262,31 @@ describe('OpenFeature Exposures Writer', () => { writer.flush() - expect(request).to.have.been.calledOnce + sinon.assert.calledOnce(request) const [payload, options] = request.getCall(0).args - expect(options.method).to.equal('POST') - expect(options.path).to.include('/evp_proxy/v2/') - expect(options.headers['Content-Type']).to.equal('application/json') - expect(options.headers['X-Datadog-EVP-Subdomain']).to.equal('event-platform-intake') + assert.strictEqual(options.method, 'POST') + assert.match(options.path, /\/evp_proxy\/v2\//) + assert.strictEqual(options.headers['Content-Type'], 'application/json') + assert.strictEqual(options.headers['X-Datadog-EVP-Subdomain'], 'event-platform-intake') const parsedPayload = JSON.parse(payload) - expect(parsedPayload).to.be.an('object') - expect(parsedPayload).to.have.property('context') - expect(parsedPayload).to.have.property('exposures') - expect(parsedPayload.exposures).to.be.an('array').with.length(1) - expect(parsedPayload.exposures[0].timestamp).to.exist - expect(parsedPayload.context.service).to.equal('test-service') + assert.ok(parsedPayload !== null && typeof parsedPayload === 'object' && !Array.isArray(parsedPayload)) + assert.ok(Object.hasOwn(parsedPayload, 'context')) + assert.ok(Object.hasOwn(parsedPayload, 'exposures')) + assert.strictEqual(parsedPayload.exposures?.length, 1) + assert.ok(parsedPayload.exposures[0].timestamp) + assert.strictEqual(parsedPayload.context.service, 'test-service') }) it('should empty buffer after flushing', () => { writer.append(exposureEvent) - expect(writer._buffer).to.have.length(1) + assert.strictEqual(writer._buffer?.length, 1) writer.flush() - expect(writer._buffer).to.have.length(0) - expect(writer._bufferSize).to.equal(0) + assert.strictEqual(writer._buffer?.length, 0) + assert.strictEqual(writer._bufferSize, 0) }) it('should log errors on request failure', (done) => { @@ -299,7 +296,7 @@ describe('OpenFeature Exposures Writer', () => { writer.flush() clock.tickAsync(0).then(() => { - expect(log.error).to.have.been.calledOnce + sinon.assert.calledOnce(log.error) done() }) }) @@ -309,7 +306,7 @@ describe('OpenFeature Exposures Writer', () => { writer.flush() - expect(log.debug).to.have.been.called + sinon.assert.called(log.debug) }) it('should warn on non-2xx response', (done) => { @@ -319,7 +316,7 @@ describe('OpenFeature Exposures Writer', () => { writer.flush() clock.tickAsync(0).then(() => { - expect(log.warn).to.have.been.calledOnce + sinon.assert.calledOnce(log.warn) done() }) }) @@ -335,13 +332,13 @@ describe('OpenFeature Exposures Writer', () => { clock.tick(1000) // Advance by flush interval - expect(request).to.have.been.calledOnce + sinon.assert.calledOnce(request) }) it('should not flush empty buffer periodically', () => { clock.tick(1000) - expect(request).to.not.have.been.called + sinon.assert.called(request) }) }) @@ -351,7 +348,7 @@ describe('OpenFeature Exposures Writer', () => { writer.destroy() - expect(clearIntervalSpy).to.have.been.calledOnce + sinon.assert.calledOnce(clearIntervalSpy) clearIntervalSpy.restore() }) @@ -361,7 +358,7 @@ describe('OpenFeature Exposures Writer', () => { writer.destroy() - expect(request).to.have.been.calledOnce + sinon.assert.calledOnce(request) }) it('should log dropped events count', () => { @@ -369,16 +366,14 @@ describe('OpenFeature Exposures Writer', () => { writer.destroy() - expect(log.warn).to.have.been.calledWith( - sinon.match(/dropped 5 events/) - ) + sinon.assert.calledWith(log.warn, sinon.match(/dropped 5 events/)) }) it('should prevent multiple destruction', () => { writer.destroy() writer.destroy() // Should not throw or cause issues - expect(writer._destroyed).to.be.true + assert.strictEqual(writer._destroyed, true) }) }) }) diff --git a/packages/dd-trace/test/opentelemetry/context_manager.spec.js b/packages/dd-trace/test/opentelemetry/context_manager.spec.js index e7d3bfaeda6..5142c83bcce 100644 --- a/packages/dd-trace/test/opentelemetry/context_manager.spec.js +++ b/packages/dd-trace/test/opentelemetry/context_manager.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const { context, propagation, trace, ROOT_CONTEXT } = require('@opentelemetry/api') const api = require('@opentelemetry/api') require('../setup/core') - const ContextManager = require('../../src/opentelemetry/context_manager') const TracerProvider = require('../../src/opentelemetry/tracer_provider') const tracer = require('../../').init() diff --git a/packages/dd-trace/test/opentelemetry/logs.spec.js b/packages/dd-trace/test/opentelemetry/logs.spec.js index 37ad96d83d1..cb087fe49a2 100644 --- a/packages/dd-trace/test/opentelemetry/logs.spec.js +++ b/packages/dd-trace/test/opentelemetry/logs.spec.js @@ -3,15 +3,17 @@ // Increase max listeners to avoid warnings in tests process.setMaxListeners(50) -require('../setup/core') const assert = require('assert') const os = require('os') const http = require('http') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const { logs } = require('@opentelemetry/api-logs') const { trace, context } = require('@opentelemetry/api') + +require('../setup/core') const { protoLogsService } = require('../../src/opentelemetry/otlp/protobuf_loader').getProtobufTypes() const { getConfigFresh } = require('../helpers/config') @@ -496,7 +498,7 @@ describe('OpenTelemetry Logs', () => { const { loggerProvider } = setupTracer() assert.strictEqual(loggerProvider.processor.exporter.transformer.protocol, 'http/protobuf') - assert(logMock.getMessage().includes('OTLP gRPC protocol is not supported')) + assert.match(logMock.getMessage(), /OTLP gRPC protocol is not supported/) logMock.restore() }) diff --git a/packages/dd-trace/test/opentelemetry/metrics.spec.js b/packages/dd-trace/test/opentelemetry/metrics.spec.js index 6c574c46358..391a05bc0cc 100644 --- a/packages/dd-trace/test/opentelemetry/metrics.spec.js +++ b/packages/dd-trace/test/opentelemetry/metrics.spec.js @@ -2,13 +2,15 @@ process.setMaxListeners(50) -require('../setup/core') const assert = require('assert') const http = require('http') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const { metrics } = require('@opentelemetry/api') + +require('../setup/core') const { protoMetricsService } = require('../../src/opentelemetry/otlp/protobuf_loader').getProtobufTypes() const { getConfigFresh } = require('../helpers/config') const { DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE } = require('../../src/opentelemetry/metrics/constants') diff --git a/packages/dd-trace/test/opentelemetry/sampler.spec.js b/packages/dd-trace/test/opentelemetry/sampler.spec.js index 9e9058dcec2..b5bea456eda 100644 --- a/packages/dd-trace/test/opentelemetry/sampler.spec.js +++ b/packages/dd-trace/test/opentelemetry/sampler.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../setup/core') +const { describe, it } = require('mocha') +require('../setup/core') const Sampler = require('../../src/opentelemetry/sampler') describe('OTel Sampler', () => { diff --git a/packages/dd-trace/test/opentelemetry/span.spec.js b/packages/dd-trace/test/opentelemetry/span.spec.js index 8062148ace3..2a7a55657f2 100644 --- a/packages/dd-trace/test/opentelemetry/span.spec.js +++ b/packages/dd-trace/test/opentelemetry/span.spec.js @@ -1,11 +1,12 @@ 'use strict' const assert = require('node:assert/strict') +const { performance } = require('perf_hooks') -const { expect } = require('chai') -const { describe, it } = require('tap').mocha +const api = require('@opentelemetry/api') +const { describe, it } = require('mocha') const sinon = require('sinon') -const { performance } = require('perf_hooks') + const { timeOrigin } = performance const { timeInputToHrTime } = require('../../../../vendor/dist/@opentelemetry/core') @@ -13,7 +14,6 @@ require('../setup/core') const tracer = require('../../').init() -const api = require('@opentelemetry/api') const TracerProvider = require('../../src/opentelemetry/tracer_provider') const SpanContext = require('../../src/opentelemetry/span_context') const { NoopSpanProcessor } = require('../../src/opentelemetry/span_processor') @@ -494,13 +494,13 @@ describe('OTel Span', () => { processor.onEnd = sinon.stub() tracerProvider.addSpanProcessor(processor) - expect(processor.onStart).to.have.not.been.called - expect(processor.onEnd).to.have.not.been.called + sinon.assert.notCalled(processor.onStart) + sinon.assert.notCalled(processor.onEnd) const span = tracer.startSpan('name') sinon.assert.calledWith(processor.onStart, span, span._context) - expect(processor.onEnd).to.have.not.been.called + sinon.assert.notCalled(processor.onEnd) span.end() diff --git a/packages/dd-trace/test/opentelemetry/span_context.spec.js b/packages/dd-trace/test/opentelemetry/span_context.spec.js index e1aa77cdeeb..3d5fafd775c 100644 --- a/packages/dd-trace/test/opentelemetry/span_context.spec.js +++ b/packages/dd-trace/test/opentelemetry/span_context.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../setup/core') +const { describe, it } = require('mocha') +require('../setup/core') const SpanContext = require('../../src/opentelemetry/span_context') const DDSpanContext = require('../../src/opentracing/span_context') const id = require('../../src/id') diff --git a/packages/dd-trace/test/opentelemetry/span_processor.spec.js b/packages/dd-trace/test/opentelemetry/span_processor.spec.js index 328503cca25..4694453f2fd 100644 --- a/packages/dd-trace/test/opentelemetry/span_processor.spec.js +++ b/packages/dd-trace/test/opentelemetry/span_processor.spec.js @@ -1,6 +1,6 @@ 'use strict' -const { describe, it } = require('tap').mocha +const { describe, it } = require('mocha') const sinon = require('sinon') require('../setup/core') diff --git a/packages/dd-trace/test/opentelemetry/tracer.spec.js b/packages/dd-trace/test/opentelemetry/tracer.spec.js index 1229ae51e72..a058fc74861 100644 --- a/packages/dd-trace/test/opentelemetry/tracer.spec.js +++ b/packages/dd-trace/test/opentelemetry/tracer.spec.js @@ -1,20 +1,18 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -const sinon = require('sinon') const { performance } = require('perf_hooks') + +const { describe, it } = require('mocha') +const sinon = require('sinon') const api = require('@opentelemetry/api') -const { hrTime, timeInputToHrTime } = require('../../../../vendor/dist/@opentelemetry/core') +const { hrTime, timeInputToHrTime } = require('../../../../vendor/dist/@opentelemetry/core') require('../setup/core') - require('../../').init() - const TracerProvider = require('../../src/opentelemetry/tracer_provider') const Tracer = require('../../src/opentelemetry/tracer') const Span = require('../../src/opentelemetry/span') - const DatadogSpan = require('../../src/opentracing/span') const tracer = require('../../') diff --git a/packages/dd-trace/test/opentelemetry/tracer_provider.spec.js b/packages/dd-trace/test/opentelemetry/tracer_provider.spec.js index 5f7a92aa466..48866034055 100644 --- a/packages/dd-trace/test/opentelemetry/tracer_provider.spec.js +++ b/packages/dd-trace/test/opentelemetry/tracer_provider.spec.js @@ -1,17 +1,15 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha + +const { describe, it } = require('mocha') const sinon = require('sinon') const { trace } = require('@opentelemetry/api') require('../setup/core') - const TracerProvider = require('../../src/opentelemetry/tracer_provider') const Tracer = require('../../src/opentelemetry/tracer') - const { MultiSpanProcessor, NoopSpanProcessor } = require('../../src/opentelemetry/span_processor') - require('../../index').init() describe('OTel TracerProvider', () => { diff --git a/packages/dd-trace/test/opentracing/api.spec.js b/packages/dd-trace/test/opentracing/api.spec.js index 3604bbcf73f..f3970e65259 100644 --- a/packages/dd-trace/test/opentracing/api.spec.js +++ b/packages/dd-trace/test/opentracing/api.spec.js @@ -1,15 +1,60 @@ 'use strict' -require('tap').mochaGlobals() +const assert = require('node:assert/strict') +const Module = require('node:module') + +const { describe } = require('mocha') + require('../setup/core') -const apiCompatibilityChecks = require('opentracing/lib/test/api_compatibility').default +// OpenTracing's upstream API compatibility checks depend on `chai`, but this +// repo intentionally does not. We provide a minimal shim just for these checks +// (assert.equal, expect(...).to.not.throw(), expect(...).to.be.a()). +function createChaiShim () { + return { + assert: { equal: assert.strictEqual }, + expect (value) { + return { + to: { + not: { + throw: value, + }, + be: { + a: (type) => { + assert.strictEqual(typeof value, type) + } + } + } + } + } + } +} + +// @ts-expect-error - `Module._load` is an internal Node API used only for this test shim. +const originalLoad = Module._load +// @ts-expect-error - `Module._load` is an internal Node API used only for this test shim. +Module._load = function (request, parent, isMain) { + if (request === 'chai') return createChaiShim() + return originalLoad.call(this, request, parent, isMain) +} + +let apiCompatibilityChecks +try { + apiCompatibilityChecks = require('opentracing/lib/test/api_compatibility').default +} finally { + // Restore immediately after loading OpenTracing's test helper. + // @ts-expect-error - `Module._load` is an internal Node API used only for this test shim. + Module._load = originalLoad +} + const tracer = require('../..') -apiCompatibilityChecks(() => { - return tracer.init({ - service: 'test', - flushInterval: 0, - plugins: false +describe('OpenTracing API', () => { + apiCompatibilityChecks(() => { + return tracer.init({ + service: 'test', + flushInterval: 0, + plugins: false + }) }) }) diff --git a/packages/dd-trace/test/opentracing/propagation/binary.spec.js b/packages/dd-trace/test/opentracing/propagation/binary.spec.js index e1ad04d2903..340dc35ff3c 100644 --- a/packages/dd-trace/test/opentracing/propagation/binary.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/binary.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('../../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../../setup/core') const id = require('../../../src/id') const SpanContext = require('../../../src/opentracing/span_context') diff --git a/packages/dd-trace/test/opentracing/propagation/log.spec.js b/packages/dd-trace/test/opentracing/propagation/log.spec.js index 36cac9aa92f..f8f989e9a8a 100644 --- a/packages/dd-trace/test/opentracing/propagation/log.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/log.spec.js @@ -1,12 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../setup/core') - const id = require('../../../src/id') const SpanContext = require('../../../src/opentracing/span_context') 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 9c8fab6151e..e1176708105 100644 --- a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js @@ -2,22 +2,18 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') - -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') +const { channel } = require('dc-polyfill') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../setup/core') - const { getConfigFresh } = require('../../helpers/config') const id = require('../../../src/id') const SpanContext = require('../../../src/opentracing/span_context') const TraceState = require('../../../src/opentracing/propagation/tracestate') -const { channel } = require('dc-polyfill') const { setBaggageItem, getBaggageItem, getAllBaggageItems, removeAllBaggageItems } = require('../../../src/baggage') - const { AUTO_KEEP, AUTO_REJECT, USER_KEEP } = require('../../../../../ext/priority') const { SAMPLING_MECHANISM_MANUAL } = require('../../../src/constants') @@ -96,6 +92,7 @@ describe('TextMapPropagator', () => { }) it('should inject the span context into the carrier', () => { + /** @type {Record} */ const carrier = {} const spanContext = createContext() @@ -129,6 +126,25 @@ describe('TextMapPropagator', () => { assert.strictEqual(carrier.baggage, undefined) }) + it('should skip legacy baggage items that cannot be encoded as a valid HTTP header name', () => { + const carrier = {} + const tracerMetrics = telemetryMetrics.manager.namespace('tracers') + const spanContext = createContext({ + baggageItems: { + ok: 'yes', + 'not ok': 'no' + } + }) + + propagator.inject(spanContext, carrier) + + assert.strictEqual(carrier['ot-baggage-ok'], 'yes') + assert.strictEqual(carrier['ot-baggage-not ok'], undefined) + + sinon.assert.calledWith(tracerMetrics.count, 'context_header_style.malformed', ['header_style:baggage']) + sinon.assert.called(tracerMetrics.count().inc) + }) + it('should handle special characters in baggage', () => { const carrier = {} setBaggageItem('",;\\()/:<=>?@[]{}🐶é我', '",;\\🐶é我') @@ -138,12 +154,17 @@ describe('TextMapPropagator', () => { }) it('should drop excess baggage items when there are too many pairs', () => { + /** @type {Record} */ const carrier = {} for (let i = 0; i < config.baggageMaxItems + 1; i++) { - setBaggageItem(`key-${i}`, i) + setBaggageItem(`key-${i}`, String(i)) } propagator.inject(undefined, carrier) - assert.strictEqual(carrier.baggage.split(',').length, config.baggageMaxItems) + const baggage = carrier.baggage + if (typeof baggage !== 'string') { + throw new Error('Expected baggage header to be a string') + } + assert.strictEqual(baggage.split(',').length, config.baggageMaxItems) }) it('should drop excess baggage items when the resulting baggage header contains many bytes', () => { @@ -438,7 +459,7 @@ describe('TextMapPropagator', () => { propagator.inject(undefined, carrier) sinon.assert.calledWith(tracerMetrics.count, 'context_header_style.injected', ['header_style:baggage']) - expect(tracerMetrics.count().inc).to.have.been.called + sinon.assert.called(tracerMetrics.count().inc) assert.strictEqual(carrier.baggage, 'test-key=test-value') }) @@ -458,7 +479,7 @@ describe('TextMapPropagator', () => { 'context_header.truncated', ['truncation_reason:baggage_item_count_exceeded'] ) - expect(tracerMetrics.count().inc).to.have.been.called + sinon.assert.called(tracerMetrics.count().inc) // Restore original config config.baggageMaxItems = originalMaxItems @@ -478,7 +499,7 @@ describe('TextMapPropagator', () => { 'context_header.truncated', ['truncation_reason:baggage_byte_count_exceeded'] ) - expect(tracerMetrics.count().inc).to.have.been.called + sinon.assert.called(tracerMetrics.count().inc) // Restore original config config.baggageMaxBytes = originalMaxBytes @@ -977,7 +998,7 @@ describe('TextMapPropagator', () => { propagator.extract(carrier) sinon.assert.calledWith(tracerMetrics.count, 'context_header_style.extracted', ['header_style:baggage']) - expect(tracerMetrics.count().inc).to.have.been.called + sinon.assert.called(tracerMetrics.count().inc) assert.strictEqual(getBaggageItem('test-key'), 'test-value') }) @@ -991,7 +1012,7 @@ describe('TextMapPropagator', () => { propagator.extract(carrier) sinon.assert.calledWith(tracerMetrics.count, 'context_header_style.malformed', ['header_style:baggage']) - expect(tracerMetrics.count().inc).to.have.been.called + sinon.assert.called(tracerMetrics.count().inc) assert.deepStrictEqual(getAllBaggageItems(), {}) }) }) diff --git a/packages/dd-trace/test/opentracing/propagation/tracestate.spec.js b/packages/dd-trace/test/opentracing/propagation/tracestate.spec.js index e72281fbfd1..172ee6c7c09 100644 --- a/packages/dd-trace/test/opentracing/propagation/tracestate.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/tracestate.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') require('../../setup/core') diff --git a/packages/dd-trace/test/opentracing/span.spec.js b/packages/dd-trace/test/opentracing/span.spec.js index 3a79546ac68..b16d8dedfcf 100644 --- a/packages/dd-trace/test/opentracing/span.spec.js +++ b/packages/dd-trace/test/opentracing/span.spec.js @@ -2,14 +2,13 @@ const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const { channel } = require('dc-polyfill') const proxyquire = require('proxyquire') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - const getConfig = require('../../src/config') const TextMapPropagator = require('../../src/opentracing/propagation/text_map') @@ -318,18 +317,18 @@ describe('Span', () => { 'ptr.hash': 'abc123', 'link.kind': 'span-pointer' }) - }) - span.addSpanPointer('another_kind', 'd', '1234567') - assert.strictEqual(span._links.length, 2) - assert.deepStrictEqual(span._links[1].attributes, { - 'ptr.kind': 'another_kind', - 'ptr.dir': 'd', - 'ptr.hash': '1234567', - 'link.kind': 'span-pointer' + span.addSpanPointer('another_kind', 'd', '1234567') + assert.strictEqual(span._links.length, 2) + assert.deepStrictEqual(span._links[1].attributes, { + 'ptr.kind': 'another_kind', + 'ptr.dir': 'd', + 'ptr.hash': '1234567', + 'link.kind': 'span-pointer' + }) + assert.strictEqual(span._links[1].context.toTraceId(), '0') + assert.strictEqual(span._links[1].context.toSpanId(), '0') }) - assert.strictEqual(span._links[1].context.toTraceId(), '0') - assert.strictEqual(span._links[1].context.toSpanId(), '0') }) describe('events', () => { diff --git a/packages/dd-trace/test/opentracing/span_context.spec.js b/packages/dd-trace/test/opentracing/span_context.spec.js index f44b27cc6d7..87ca34170fc 100644 --- a/packages/dd-trace/test/opentracing/span_context.spec.js +++ b/packages/dd-trace/test/opentracing/span_context.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../setup/core') const id = require('../../src/id') describe('SpanContext', () => { diff --git a/packages/dd-trace/test/opentracing/tracer.spec.js b/packages/dd-trace/test/opentracing/tracer.spec.js index 07514a609b1..6a6904f12b0 100644 --- a/packages/dd-trace/test/opentracing/tracer.spec.js +++ b/packages/dd-trace/test/opentracing/tracer.spec.js @@ -1,17 +1,17 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha +const os = require('node:os') + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') -const opentracing = require('../../../../vendor/dist/opentracing') const proxyquire = require('proxyquire') -const os = require('node:os') - +const opentracing = require('../../../../vendor/dist/opentracing') require('../setup/core') - const SpanContext = require('../../src/opentracing/span_context') const formats = require('../../../../ext/formats') + const Reference = opentracing.Reference describe('Tracer', () => { diff --git a/packages/dd-trace/test/payload-tagging/index.spec.js b/packages/dd-trace/test/payload-tagging/index.spec.js index 0501b6c3f71..534cb4b57ad 100644 --- a/packages/dd-trace/test/payload-tagging/index.spec.js +++ b/packages/dd-trace/test/payload-tagging/index.spec.js @@ -199,6 +199,40 @@ describe('Tagging orchestration', () => { assert.strictEqual(tags[`${PAYLOAD_TAG_RESPONSE_PREFIX}.request`], 'foo') }) + it('should not fail if the response config contains invalid config', () => { + const config = { + request: ['invalid,request'], + response: ['invalid,$.foo,$.response'], + expand: [] + } + const input = { + request: 'foo', + response: 'bar', + } + const tags = computeTags(config, input, { maxDepth: 10, prefix: PAYLOAD_TAG_RESPONSE_PREFIX }) + assert.strictEqual(tags[`${PAYLOAD_TAG_RESPONSE_PREFIX}.response`], 'redacted') + assert.strictEqual(tags[`${PAYLOAD_TAG_RESPONSE_PREFIX}.request`], 'foo') + }) + + it('should apply expansion rules with dollar identical to an empty input', () => { + const config = { + request: ['$'], + response: ['$'], + expand: ['$.request', '$.response', '$.invalid'], + } + const input = { + request: '{ "foo": "bar" }', + response: '{ "baz": "quux" }', + invalid: '{ invalid JSON }', + untargeted: '{ "foo": "bar" }', + } + const tags = computeTags(config, input, { maxDepth: 10, prefix: 'foo' }) + assert.strictEqual(tags['foo.request.foo'], 'bar') + assert.strictEqual(tags['foo.response.baz'], 'quux') + assert.strictEqual(tags['foo.invalid'], '{ invalid JSON }') + assert.strictEqual(tags['foo.untargeted'], '{ "foo": "bar" }') + }) + it('should apply expansion rules', () => { const config = { request: [], diff --git a/packages/dd-trace/test/payload_tagging.spec.js b/packages/dd-trace/test/payload_tagging.spec.js index 9800546a9db..7bd345caac7 100644 --- a/packages/dd-trace/test/payload_tagging.spec.js +++ b/packages/dd-trace/test/payload_tagging.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha + +const { describe, it } = require('mocha') require('./setup/core') diff --git a/packages/dd-trace/test/pkg.spec.js b/packages/dd-trace/test/pkg.spec.js index c5f88d6a652..d736a812720 100644 --- a/packages/dd-trace/test/pkg.spec.js +++ b/packages/dd-trace/test/pkg.spec.js @@ -1,9 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha const os = require('node:os') const { execSync } = require('node:child_process') + +const { describe, it, beforeEach } = require('mocha') + const proxyquire = require('proxyquire').noPreserveCache() require('./setup/core') diff --git a/packages/dd-trace/test/plugin_manager.spec.js b/packages/dd-trace/test/plugin_manager.spec.js index c6993529a13..20d0a259747 100644 --- a/packages/dd-trace/test/plugin_manager.spec.js +++ b/packages/dd-trace/test/plugin_manager.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const { channel } = require('dc-polyfill') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/plugins/agent.js b/packages/dd-trace/test/plugins/agent.js index 8f4d443d90a..0ebf9fac5f5 100644 --- a/packages/dd-trace/test/plugins/agent.js +++ b/packages/dd-trace/test/plugins/agent.js @@ -17,6 +17,7 @@ const ritm = require('../../src/ritm') const traceHandlers = new Set() const statsHandlers = new Set() let llmobsSpanEventsRequests = [] +let llmobsEvaluationMetricsRequests = [] let sockets = [] let agent = null let listener = null @@ -387,7 +388,7 @@ module.exports = { * Load the plugin on the tracer with an optional config and start a mock agent. * * @overload - * @param {String|String[]} pluginNames - Name or list of names of plugins to load + * @param {string | string[]} pluginNames - Name or list of names of plugins to load * @param {Record} [config] * @param {Record} [tracerConfig={}] * @returns Promise @@ -396,7 +397,7 @@ module.exports = { * Load the plugin on the tracer with an optional config and start a mock agent. * * @overload - * @param {String[]} pluginNames - Name or list of names of plugins to load + * @param {string[]} pluginNames - Name or list of names of plugins to load * @param {Record[]} config * @param {Record} [tracerConfig={}] * @returns Promise @@ -468,6 +469,12 @@ module.exports = { res.status(200).send() }) + // LLM Observability evaluation metrics endpoint + agent.post('/evp_proxy/v2/api/intake/llm-obs/v1/eval-metric', (req, res) => { + llmobsEvaluationMetricsRequests.push(JSON.parse(req.body)) + res.status(200).send() + }) + // DSM Checkpoint endpoint dsmStats = [] agent.post('/v0.1/pipeline_stats', (req, res) => { @@ -615,7 +622,7 @@ module.exports = { /** * Get the LLM Observability span events requests. * @param {boolean} clear - Clear the requests after getting them. - * @returns {Array} The LLM Observability span events requests. + * @returns {Array} The LLM Observability span events requests. */ getLlmObsSpanEventsRequests (clear = false) { const requests = llmobsSpanEventsRequests @@ -626,6 +633,20 @@ module.exports = { return requests }, + /** + * Get the LLM Observability evaluation metrics requests. + * @param {boolean} clear - Clear the requests after getting them. + * @returns {Array} The LLM Observability evaluation metrics requests. + */ + getLlmObsEvaluationMetricsRequests (clear = false) { + const requests = llmobsEvaluationMetricsRequests + if (clear) { + llmobsEvaluationMetricsRequests = [] + } + + return requests + }, + /** * Unregister any outstanding expectation callbacks. */ @@ -633,6 +654,7 @@ module.exports = { traceHandlers.clear() statsHandlers.clear() llmobsSpanEventsRequests = [] + llmobsEvaluationMetricsRequests = [] }, /** @@ -642,7 +664,7 @@ module.exports = { * - ritmReset: true * - wipe: false * - * @param {Object} [options] + * @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. @@ -662,6 +684,7 @@ module.exports = { traceHandlers.clear() statsHandlers.clear() llmobsSpanEventsRequests = [] + llmobsEvaluationMetricsRequests = [] for (const plugin of plugins) { tracer.use(plugin, { enabled: false }) } diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index 2be21d0236f..8c9eb0f1884 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -69,6 +69,12 @@ "versions": [">=3.422.0"] } ], + "bullmq": [ + { + "name": "redis", + "versions": [">=4"] + } + ], "body-parser": [ { "name": "express", @@ -482,6 +488,17 @@ { "name": "prisma", "dep": "@prisma/client" + }, + { + "name": "typescript", + "dep": true, + "versions": [">=5.4.0"] + }, + { + "name": "@prisma/adapter-pg", + "dep": "@prisma/adapter-pg", + "versions": [">=7.0.0"], + "node": ">=20" } ], "q": [ diff --git a/packages/dd-trace/test/plugins/helpers.js b/packages/dd-trace/test/plugins/helpers.js index c0298626f17..74548e1185f 100644 --- a/packages/dd-trace/test/plugins/helpers.js +++ b/packages/dd-trace/test/plugins/helpers.js @@ -1,10 +1,10 @@ 'use strict' -const { AssertionError } = require('assert') const { inspect } = require('util') const { AsyncResource } = require('../../../datadog-instrumentations/src/helpers/instrument') const Nomenclature = require('../../src/service-naming') +const { assertObjectContains } = require('../../../../integration-tests/helpers') function resolveNaming (namingSchema) { return new Proxy(namingSchema, { @@ -20,7 +20,7 @@ function expectSomeSpan (agent, expected, timeout) { for (const trace of traces) { for (const span of trace) { try { - deepInclude(expected, span) + assertObjectContains(span, expected) return } catch (err) { scoredErrors.push({ err, score: compare(expected, span) }) @@ -39,37 +39,13 @@ function expectSomeSpan (agent, expected, timeout) { }, { timeoutMs: timeout }) } -// This is a bit like chai's `expect(expected).to.deep.include(actual)`, except -// that when it recurses it uses the same inclusion check, rather than deep -// equality. Some nice output is included. -function deepInclude (expected, actual, path = []) { - for (const propName in expected) { - path.push(propName.includes('.') ? `['${propName}']` : propName) - if (isObject(expected[propName]) && isObject(actual[propName])) { - if (typeof expected[propName] === 'bigint') { - deepInclude(expected[propName].toString(), actual[propName].toString(), path) - } else { - deepInclude(expected[propName], actual[propName], path) - } - } else if (actual[propName] !== expected[propName]) { - const pathStr = path.join('.').replace(/\.\[/g, '[') - throw new AssertionError({ - expected: expected[propName], - actual: actual[propName], - message: `expected.${pathStr} !== actual.${pathStr}` - }) - } - path.pop() - } -} - // How "wrong" are we? This gives the edit distance from the actual to the // expected. Here, an edit means adding or changing a property. function compare (expected, actual) { let score = 0 for (const name in expected) { // If both the expected property and the actual property are objects, then - // we can do a deeper comparison. Otherise we just compare strict equality. + // we can do a deeper comparison. Otherwise we just compare strict equality. if (isObject(expected[name]) && isObject(actual[name])) { score += compare(expected[name], actual[name]) } else if (expected[name] !== actual[name]) { @@ -123,7 +99,6 @@ function getNextLineNumber () { module.exports = { breakThen, compare, - deepInclude, expectSomeSpan, getNextLineNumber, resolveNaming, diff --git a/packages/dd-trace/test/plugins/log_plugin.spec.js b/packages/dd-trace/test/plugins/log_plugin.spec.js index 8e2147c1738..41cec14d88e 100644 --- a/packages/dd-trace/test/plugins/log_plugin.spec.js +++ b/packages/dd-trace/test/plugins/log_plugin.spec.js @@ -1,13 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it } = require('tap').mocha +const { describe, it } = require('mocha') const { channel } = require('dc-polyfill') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - const LogPlugin = require('../../src/plugins/log_plugin') const Tracer = require('../../src/tracer') const getConfig = require('../../src/config') diff --git a/packages/dd-trace/test/plugins/outbound.spec.js b/packages/dd-trace/test/plugins/outbound.spec.js index d726e387ee3..860b96c45c6 100644 --- a/packages/dd-trace/test/plugins/outbound.spec.js +++ b/packages/dd-trace/test/plugins/outbound.spec.js @@ -1,16 +1,15 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach, before } = require('tap').mocha +const { describe, it, beforeEach, afterEach, before } = require('mocha') const sinon = require('sinon') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') - -const { getNextLineNumber } = require('./helpers') const OutboundPlugin = require('../../src/plugins/outbound') const parseTags = require('../../../datadog-core/src/utils/src/parse-tags') +const { getNextLineNumber } = require('./helpers') describe('OuboundPlugin', () => { describe('peer service decision', () => { diff --git a/packages/dd-trace/test/plugins/plugin-structure.spec.js b/packages/dd-trace/test/plugins/plugin-structure.spec.js index 7f7de8377f6..6cc3604eeae 100644 --- a/packages/dd-trace/test/plugins/plugin-structure.spec.js +++ b/packages/dd-trace/test/plugins/plugin-structure.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, before } = require('tap').mocha const fs = require('node:fs') const path = require('node:path') -require('../setup/core') +const { describe, it } = require('mocha') +require('../setup/core') const hooks = require('../../../datadog-instrumentations/src/helpers/hooks') const abstractPlugins = [ @@ -48,22 +48,16 @@ describe('Plugin Structure Validation', () => { const packagesDir = path.join(__dirname, '..', '..', '..') const instrumentationsDir = path.join(packagesDir, 'datadog-instrumentations', 'src') - let pluginDirs - let instrumentationFiles - let allPluginIds + const pluginDirs = fs.readdirSync(packagesDir) + .filter(dir => dir.startsWith('datadog-plugin-') && !missingPlugins.includes(dir)) - before(() => { - pluginDirs = fs.readdirSync(packagesDir) - .filter(dir => dir.startsWith('datadog-plugin-') && !missingPlugins.includes(dir)) + const instrumentationFiles = new Set( + fs.readdirSync(instrumentationsDir) + .filter(file => file.endsWith('.js')) + .map(file => file.replace('.js', '')) + ) - instrumentationFiles = new Set( - fs.readdirSync(instrumentationsDir) - .filter(file => file.endsWith('.js')) - .map(file => file.replace('.js', '')) - ) - - allPluginIds = new Set(pluginDirs.map(dir => dir.replace('datadog-plugin-', ''))) - }) + const allPluginIds = new Set(pluginDirs.map(dir => dir.replace('datadog-plugin-', ''))) pluginDirs.forEach(pluginDir => { const expectedId = pluginDir.replace('datadog-plugin-', '') diff --git a/packages/dd-trace/test/plugins/plugin.spec.js b/packages/dd-trace/test/plugins/plugin.spec.js index c91e507d62e..697e20d7e5b 100644 --- a/packages/dd-trace/test/plugins/plugin.spec.js +++ b/packages/dd-trace/test/plugins/plugin.spec.js @@ -2,7 +2,7 @@ const assert = require('node:assert') -const { describe, it, after } = require('tap').mocha +const { describe, it, after } = require('mocha') const { channel } = require('dc-polyfill') const proxyquire = require('proxyquire') const sinon = require('sinon') diff --git a/packages/dd-trace/test/plugins/tracing.spec.js b/packages/dd-trace/test/plugins/tracing.spec.js index af03a4e52e5..5f0e545ad6f 100644 --- a/packages/dd-trace/test/plugins/tracing.spec.js +++ b/packages/dd-trace/test/plugins/tracing.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, before, after } = require('tap').mocha + +const { describe, it, before, after } = require('mocha') const sinon = require('sinon') const { channel } = require('dc-polyfill') require('../setup/core') - const TracingPlugin = require('../../src/plugins/tracing') const agent = require('../plugins/agent') const plugins = require('../../src/plugins') diff --git a/packages/dd-trace/test/plugins/util/env.spec.js b/packages/dd-trace/test/plugins/util/env.spec.js index b4a00d7dd9b..ff99974e31b 100644 --- a/packages/dd-trace/test/plugins/util/env.spec.js +++ b/packages/dd-trace/test/plugins/util/env.spec.js @@ -1,11 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha +const os = require('os') + +const { describe, it } = require('mocha') require('../../setup/core') -const os = require('os') const { getRuntimeAndOSMetadata, OS_ARCHITECTURE, diff --git a/packages/dd-trace/test/plugins/util/git.spec.js b/packages/dd-trace/test/plugins/util/git.spec.js index 1d7f9846f64..2a81ebde7f9 100644 --- a/packages/dd-trace/test/plugins/util/git.spec.js +++ b/packages/dd-trace/test/plugins/util/git.spec.js @@ -1,19 +1,19 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') - -const { describe, it, afterEach, beforeEach } = require('tap').mocha -const sinon = require('sinon') -const proxyquire = require('proxyquire') const { execSync } = require('node:child_process') const os = require('node:os') const fs = require('node:fs') const path = require('node:path') -require('../../setup/core') +const { describe, it, afterEach, beforeEach } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') +require('../../setup/core') const { GIT_REV_LIST_MAX_BUFFER, isGitAvailable } = require('../../../src/plugins/util/git') + const cachedExecStub = sinon.stub().returns('') const { diff --git a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js b/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js index 1e4b9f6a521..d54cf57a5e4 100644 --- a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js +++ b/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js @@ -1,13 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, afterEach } = require('tap').mocha -const axios = require('axios') const { Agent } = require('node:http') -require('../../setup/core') +const { describe, it, afterEach } = require('mocha') +const axios = require('axios') +require('../../setup/core') const agent = require('../agent') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') // Create axios instance with no connection pooling const httpClient = axios.create({ @@ -21,7 +22,7 @@ describe('Inferred Proxy Spans', function () { let controller let port - // tap was throwing timeout errors when trying to use hooks like `before`, so instead we just use this function + // Timeout errors occurred when trying to use hooks like `before`, so instead we just use this function // and call before the test starts const loadTest = async function ({ inferredProxyServicesEnabled = true } = {}) { const options = { @@ -61,7 +62,7 @@ describe('Inferred Proxy Spans', function () { }) }) - return new Promise((resolve, reject) => { + return new Promise(/** @type {() => void} */ (resolve, reject) => { appListener = server.listen(0, '127.0.0.1', () => { port = (/** @type {import('net').AddressInfo} */ (server.address())).port appListener._connections = connections @@ -81,7 +82,7 @@ describe('Inferred Proxy Spans', function () { } } - await new Promise((resolve, reject) => { + await new Promise(/** @type {() => void} */ (resolve, reject) => { appListener.close((err) => { if (err) { reject(err) @@ -126,26 +127,35 @@ describe('Inferred Proxy Spans', function () { assert.strictEqual(spans[0].service, 'example.com') assert.strictEqual(spans[0].resource, 'GET /test') assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].meta['http.url'], 'example.com/test') - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta.component, 'aws-apigateway') - assert.strictEqual(spans[0].meta['_dd.integration'], 'aws-apigateway') - assert.strictEqual(spans[0].metrics['_dd.inferred_span'], 1) + assertObjectContains(spans[0], { + meta: { + 'http.url': 'example.com/test', + 'http.method': 'GET', + 'http.status_code': '200', + component: 'aws-apigateway', + '_dd.integration': 'aws-apigateway' + }, + metrics: { + '_dd.inferred_span': 1 + } + }) assert.strictEqual(spans[0].start.toString(), '1729780025472999936') assert.strictEqual(spans[0].span_id.toString(), spans[1].parent_id.toString()) - assert.strictEqual(spans[1].name, 'web.request') - assert.strictEqual(spans[1].service, 'aws-server') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET') - assert.strictEqual(spans[1].meta.component, 'http') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.url'], `http://127.0.0.1:${port}/`) - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '200') - assert.strictEqual(spans[1].meta['span.kind'], 'server') + assertObjectContains(spans[1], { + name: 'web.request', + service: 'aws-server', + type: 'web', + resource: 'GET', + meta: { + component: 'http', + 'span.kind': 'server', + 'http.url': `http://127.0.0.1:${port}/`, + 'http.method': 'GET', + 'http.status_code': '200' + } + }) }) }) @@ -163,28 +173,37 @@ describe('Inferred Proxy Spans', function () { const spans = traces[0] assert.strictEqual(spans.length, 2) - assert.strictEqual(spans[0].name, 'aws.apigateway') - assert.strictEqual(spans[0].service, 'example.com') - assert.strictEqual(spans[0].resource, 'GET /test') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].meta['http.url'], 'example.com/test') - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '500') - assert.strictEqual(spans[0].meta.component, 'aws-apigateway') + assertObjectContains(spans[0], { + name: 'aws.apigateway', + service: 'example.com', + resource: 'GET /test', + type: 'web', + meta: { + 'http.url': 'example.com/test', + 'http.method': 'GET', + 'http.status_code': '500', + component: 'aws-apigateway' + } + }) + assert.strictEqual(spans[0].error, 1) assert.strictEqual(spans[0].start.toString(), '1729780025472999936') assert.strictEqual(spans[0].span_id.toString(), spans[1].parent_id.toString()) - assert.strictEqual(spans[1].name, 'web.request') - assert.strictEqual(spans[1].service, 'aws-server') - assert.strictEqual(spans[1].type, 'web') - assert.strictEqual(spans[1].resource, 'GET') - assert.strictEqual(spans[1].meta.component, 'http') - assert.strictEqual(spans[1].meta['span.kind'], 'server') - assert.strictEqual(spans[1].meta['http.url'], `http://127.0.0.1:${port}/error`) - assert.strictEqual(spans[1].meta['http.method'], 'GET') - assert.strictEqual(spans[1].meta['http.status_code'], '500') - assert.strictEqual(spans[1].meta['span.kind'], 'server') + assertObjectContains(spans[1], { + name: 'web.request', + service: 'aws-server', + type: 'web', + resource: 'GET', + meta: { + component: 'http', + 'span.kind': 'server', + 'http.url': `http://127.0.0.1:${port}/error`, + 'http.method': 'GET', + 'http.status_code': '500' + } + }) + assert.strictEqual(spans[1].error, 1) }) }) @@ -200,16 +219,20 @@ describe('Inferred Proxy Spans', function () { const spans = traces[0] assert.strictEqual(spans.length, 1) - assert.strictEqual(spans[0].name, 'web.request') - assert.strictEqual(spans[0].service, 'aws-server') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET') - assert.strictEqual(spans[0].meta.component, 'http') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://127.0.0.1:${port}/no-aws-headers`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta['span.kind'], 'server') + assertObjectContains(spans[0], { + name: 'web.request', + service: 'aws-server', + type: 'web', + resource: 'GET', + meta: { + component: 'http', + 'span.kind': 'server', + 'http.url': `http://127.0.0.1:${port}/no-aws-headers`, + 'http.method': 'GET', + 'http.status_code': '200' + } + }) + assert.strictEqual(spans[0].error, 0) }) }) @@ -228,16 +251,20 @@ describe('Inferred Proxy Spans', function () { const spans = traces[0] assert.strictEqual(spans.length, 1) - assert.strictEqual(spans[0].name, 'web.request') - assert.strictEqual(spans[0].service, 'aws-server') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET') - assert.strictEqual(spans[0].meta.component, 'http') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://127.0.0.1:${port}/a-few-aws-headers`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta['span.kind'], 'server') + assertObjectContains(spans[0], { + name: 'web.request', + service: 'aws-server', + type: 'web', + resource: 'GET', + meta: { + component: 'http', + 'span.kind': 'server', + 'http.url': `http://127.0.0.1:${port}/a-few-aws-headers`, + 'http.method': 'GET', + 'http.status_code': '200' + } + }) + assert.strictEqual(spans[0].error, 0) }) }) @@ -256,16 +283,19 @@ describe('Inferred Proxy Spans', function () { assert.strictEqual(spans.length, 1) - assert.strictEqual(spans[0].name, 'web.request') - assert.strictEqual(spans[0].service, 'aws-server') - assert.strictEqual(spans[0].type, 'web') - assert.strictEqual(spans[0].resource, 'GET') - assert.strictEqual(spans[0].meta.component, 'http') - assert.strictEqual(spans[0].meta['span.kind'], 'server') - assert.strictEqual(spans[0].meta['http.url'], `http://127.0.0.1:${port}/configured-off`) - assert.strictEqual(spans[0].meta['http.method'], 'GET') - assert.strictEqual(spans[0].meta['http.status_code'], '200') - assert.strictEqual(spans[0].meta['span.kind'], 'server') + assertObjectContains(spans[0], { + name: 'web.request', + service: 'aws-server', + type: 'web', + resource: 'GET', + meta: { + component: 'http', + 'span.kind': 'server', + 'http.url': `http://127.0.0.1:${port}/configured-off`, + 'http.method': 'GET', + 'http.status_code': '200' + } + }) }) }) }) diff --git a/packages/dd-trace/test/plugins/util/ip_extractor.spec.js b/packages/dd-trace/test/plugins/util/ip_extractor.spec.js index 1ca2b02ff07..113b960053b 100644 --- a/packages/dd-trace/test/plugins/util/ip_extractor.spec.js +++ b/packages/dd-trace/test/plugins/util/ip_extractor.spec.js @@ -1,13 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, before, after } = require('tap').mocha -const axios = require('axios') const http = require('node:http') -require('../../setup/core') +const { describe, it, before, after } = require('mocha') +const axios = require('axios') +require('../../setup/core') const { extractIp } = require('../../../src/plugins/util/ip_extractor') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') describe('ip extractor', () => { let port, appListener, controller @@ -193,7 +194,7 @@ describe('ip extractor', () => { controller = function (req) { const ip = extractIp({}, req) try { - assert.ok(['::1', '127.0.0.1'].includes(ip)) + assertObjectContains(['::1', '127.0.0.1'], [ip]) done() } catch (e) { done(e) @@ -219,7 +220,7 @@ describe('ip extractor', () => { controller = function (req) { const ip = extractIp({}, req) try { - assert.ok(['::1', '127.0.0.1'].includes(ip)) + assertObjectContains(['::1', '127.0.0.1'], [ip]) done() } catch (e) { done(e) diff --git a/packages/dd-trace/test/plugins/util/llm.spec.js b/packages/dd-trace/test/plugins/util/llm.spec.js index 13ab2846e9d..b5282326003 100644 --- a/packages/dd-trace/test/plugins/util/llm.spec.js +++ b/packages/dd-trace/test/plugins/util/llm.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('../../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../../setup/core') const makeUtilities = require('../../../src/plugins/util/llm') const SpanContext = require('../../../src/opentracing/span_context') const id = require('../../../src/id') diff --git a/packages/dd-trace/test/plugins/util/stacktrace.spec.js b/packages/dd-trace/test/plugins/util/stacktrace.spec.js index ca3c826f728..3e8e2319ed0 100644 --- a/packages/dd-trace/test/plugins/util/stacktrace.spec.js +++ b/packages/dd-trace/test/plugins/util/stacktrace.spec.js @@ -1,9 +1,10 @@ 'use strict' -const { describe, it } = require('tap').mocha const assert = require('node:assert') const { join } = require('node:path') +const { describe, it } = require('mocha') + require('../../setup/core') const { @@ -36,7 +37,7 @@ describe('stacktrace utils', () => { const { stack } = new Error('foo') const [frame] = parseUserLandFrames(stack) assert.deepStrictEqual(frame, { - typeName: 'Test', + typeName: 'Context', functionName: 'outerFunction', methodName: undefined, fileName: __filename, 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 234131c8a1a..0c7ccdf87fc 100644 --- a/packages/dd-trace/test/plugins/util/test-environment.spec.js +++ b/packages/dd-trace/test/plugins/util/test-environment.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') - -const { assertObjectContains } = require('../../../../../integration-tests/helpers') -const { describe, it } = require('tap').mocha const fs = require('node:fs') const path = require('node:path') + +const { describe, it } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../setup/core') const cachedExecStub = sinon.stub().returns('') diff --git a/packages/dd-trace/test/plugins/util/test.spec.js b/packages/dd-trace/test/plugins/util/test.spec.js index a8c3ea18bee..71676607dce 100644 --- a/packages/dd-trace/test/plugins/util/test.spec.js +++ b/packages/dd-trace/test/plugins/util/test.spec.js @@ -1,12 +1,14 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, context } = require('tap').mocha const path = require('node:path') -const istanbul = require('../../../../../vendor/dist/istanbul-lib-coverage') + +const { describe, it, beforeEach } = require('mocha') +const context = describe const sinon = require('sinon') const proxyquire = require('proxyquire') +const istanbul = require('../../../../../vendor/dist/istanbul-lib-coverage') require('../../setup/core') const { diff --git a/packages/dd-trace/test/plugins/util/url.spec.js b/packages/dd-trace/test/plugins/util/url.spec.js index fe9bbc1a03e..4395a7c2d78 100644 --- a/packages/dd-trace/test/plugins/util/url.spec.js +++ b/packages/dd-trace/test/plugins/util/url.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') require('../../setup/core') diff --git a/packages/dd-trace/test/plugins/util/web.spec.js b/packages/dd-trace/test/plugins/util/web.spec.js index f0195d7817e..0017aa0b2f1 100644 --- a/packages/dd-trace/test/plugins/util/web.spec.js +++ b/packages/dd-trace/test/plugins/util/web.spec.js @@ -2,12 +2,11 @@ const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') +const { assertObjectContains } = require('../../../../../integration-tests/helpers') require('../../setup/core') - const types = require('../../../../../ext/types') const kinds = require('../../../../../ext/kinds') const tags = require('../../../../../ext/tags') diff --git a/packages/dd-trace/test/plugins/versions/package.json b/packages/dd-trace/test/plugins/versions/package.json index a7aa936ce57..2f850a8acc3 100644 --- a/packages/dd-trace/test/plugins/versions/package.json +++ b/packages/dd-trace/test/plugins/versions/package.json @@ -6,8 +6,6 @@ "dependencies": { "@ai-sdk/openai": "2.0.65", "@anthropic-ai/sdk": "0.68.0", - "@openai/agents": "0.3.0", - "@openai/agents-core": "0.3.0", "@apollo/gateway": "2.12.0", "@apollo/server": "5.1.0", "@apollo/subgraph": "2.12.0", @@ -29,12 +27,12 @@ "@datadog/openfeature-node-server": "0.2.0", "@elastic/elasticsearch": "9.2.0", "@elastic/transport": "9.2.1", + "@fast-check/jest": "2.1.1", "@fastify/cookie": "11.0.2", "@fastify/multipart": "9.3.0", - "@fast-check/jest": "2.1.1", - "@google/genai": "1.19.0", "@google-cloud/pubsub": "5.2.0", "@google-cloud/vertexai": "1.10.0", + "@google/genai": "1.19.0", "@graphql-tools/executor": "1.4.11", "@grpc/grpc-js": "1.14.1", "@grpc/proto-loader": "0.8.0", @@ -55,6 +53,8 @@ "@langchain/google-genai": "1.0.0", "@langchain/openai": "1.0.0", "@node-redis/client": "1.0.6", + "@openai/agents": "0.3.0", + "@openai/agents-core": "0.3.0", "@openfeature/core": "^1.9.0", "@openfeature/server-sdk": "~1.20.0", "@opensearch-project/opensearch": "3.5.1", @@ -64,7 +64,8 @@ "@opentelemetry/instrumentation-http": "0.208.0", "@opentelemetry/sdk-node": "0.208.0", "@playwright/test": "1.56.1", - "@prisma/client": "6.19.0", + "@prisma/client": "7.0.0", + "@prisma/adapter-pg": "7.0.0", "@redis/client": "5.9.0", "@smithy/smithy-client": "4.9.3", "@types/node": "18.19.130", @@ -85,9 +86,9 @@ "bluebird": "3.7.2", "body-parser": "2.2.0", "bson": "7.0.0", + "bullmq": "5.66.0", "bunyan": "2.0.5", "cassandra-driver": "4.8.0", - "chai": "6.2.0", "collections": "5.1.13", "connect": "3.7.0", "cookie": "1.0.2", @@ -133,6 +134,7 @@ "langchain": "1.0.3", "ldapjs": "3.0.7", "ldapjs-promise": "3.0.7", + "light-my-request": "6.6.0", "limitd-client": "2.14.1", "lodash": "4.17.21", "loopback": "3.28.0", @@ -150,7 +152,7 @@ "multer": "2.0.2", "mysql": "2.18.1", "mysql2": "3.15.3", - "next": "16.0.5", + "next": "16.0.10", "nock": "14.0.10", "node-serialize": "0.0.4", "npm": "11.6.2", @@ -169,8 +171,8 @@ "pino-pretty": "13.1.2", "playwright": "1.56.1", "playwright-core": "1.56.1", - "pnpm": "10.21.0", - "prisma": "6.19.0", + "pnpm": "10.27.0", + "prisma": "7.0.0", "promise": "8.3.0", "promise-js": "0.0.7", "protobufjs": "7.5.4", diff --git a/packages/dd-trace/test/priority_sampler.spec.js b/packages/dd-trace/test/priority_sampler.spec.js index bb80dce4e6a..b27392528dc 100644 --- a/packages/dd-trace/test/priority_sampler.spec.js +++ b/packages/dd-trace/test/priority_sampler.spec.js @@ -2,13 +2,11 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('./setup/core') - const ext = require('../../../ext') const { @@ -533,7 +531,7 @@ describe('PrioritySampler', () => { PrioritySampler.keepTrace(span, SAMPLING_MECHANISM_APPSEC) - expect(setPriority).to.be.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(setPriority, span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) }) }) diff --git a/packages/dd-trace/test/process-tags.spec.js b/packages/dd-trace/test/process-tags.spec.js index ac014f24008..ddc33d209bf 100644 --- a/packages/dd-trace/test/process-tags.spec.js +++ b/packages/dd-trace/test/process-tags.spec.js @@ -1,273 +1,236 @@ 'use strict' -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const assert = require('node:assert/strict') +const { describe, it, beforeEach, afterEach } = require('mocha') + +const { assertObjectContains } = require('../../../integration-tests/helpers') +const { getConfigFresh } = require('./helpers/config') require('./setup/core') describe('process-tags', () => { - const getProcessTags = require('../src/process-tags') - const { serialize, sanitize } = require('../src/process-tags') + const processTags = require('../src/process-tags') + const { sanitize } = require('../src/process-tags') + + describe('field name constants', () => { + it('should define field names for different subsystems', () => { + assertObjectContains(processTags, { + TRACING_FIELD_NAME: '_dd.tags.process', + DSM_FIELD_NAME: 'ProcessTags', + PROFILING_FIELD_NAME: 'process_tags', + DYNAMIC_INSTRUMENTATION_FIELD_NAME: 'process_tags', + TELEMETRY_FIELD_NAME: 'process_tags', + REMOTE_CONFIG_FIELD_NAME: 'process_tags', + CRASH_TRACKING_FIELD_NAME: 'process_tags' + }) + }) + }) + + describe('processTags', () => { + it('should return an object with tags, serialized, and tagsObject properties', () => { + assert.ok(Object.hasOwn(processTags, 'tags')) + assert.ok(Object.hasOwn(processTags, 'serialized')) + assert.ok(Object.hasOwn(processTags, 'tagsObject')) + assert.ok(Array.isArray(processTags.tags)) + assert.strictEqual(typeof processTags.serialized, 'string') + assert.strictEqual(typeof processTags.tagsObject, 'object') + }) + + it('should have tagsObject with only defined values', () => { + const { tagsObject } = processTags - describe('getProcessTags', () => { - it('should return an object with tags and serialized properties', () => { - const result = getProcessTags() + // All values in tagsObject should be defined + Object.values(tagsObject).forEach(value => { + assert.notStrictEqual(value, undefined) + }) - expect(result).to.have.property('tags') - expect(result).to.have.property('serialized') - expect(result.tags).to.be.an('array') - expect(result.serialized).to.be.a('string') + // tagsObject should have the same keys as defined tags + const definedTags = processTags.tags.filter(([, value]) => value !== undefined) + assert.strictEqual(Object.keys(tagsObject).length, definedTags.length) }) it('should include all expected tag names', () => { - const result = getProcessTags() - const tagNames = result.tags.map(([name]) => name) - - expect(tagNames).to.include('entrypoint.basedir') - expect(tagNames).to.include('entrypoint.name') - expect(tagNames).to.include('entrypoint.type') - expect(tagNames).to.include('entrypoint.workdir') - expect(tagNames).to.include('package.json.name') + const tagNames = processTags.tags.map(([name]) => name) + + assertObjectContains( + tagNames, + [ + 'entrypoint.basedir', + 'entrypoint.name', + 'entrypoint.type', + 'entrypoint.workdir', + 'package.json.name' + ] + ) }) it('should have entrypoint.type set to "script"', () => { - const result = getProcessTags() - const typeTag = result.tags.find(([name]) => name === 'entrypoint.type') + const typeTag = processTags.tags.find(([name]) => name === 'entrypoint.type') - expect(typeTag).to.exist - expect(typeTag[1]).to.equal('script') + assert.ok(Array.isArray(typeTag)) + assert.strictEqual(typeTag[1], 'script') }) it('should set entrypoint.workdir to the basename of cwd', () => { - const result = getProcessTags() - const workdirTag = result.tags.find(([name]) => name === 'entrypoint.workdir') + const workdirTag = processTags.tags.find(([name]) => name === 'entrypoint.workdir') - expect(workdirTag).to.exist - expect(workdirTag[1]).to.be.a('string') - expect(workdirTag[1]).to.not.include('/') + assert.ok(Array.isArray(workdirTag)) + assert.strictEqual(typeof workdirTag[1], 'string') + assert.doesNotMatch(workdirTag[1], /\//) }) - // note that these tests may fail if the tracer folder structure changes - it('should set sensible values based on tracer project structure', () => { - const result = getProcessTags() + it('should set sensible values', () => { + const basedirTag = processTags.tags[0] + const nameTag = processTags.tags[1] + const typeTag = processTags.tags[2] + const workdirTag = processTags.tags[3] + const packageNameTag = processTags.tags[4] + + // Entrypoint values should be set (may vary depending on test runner) + assert.ok(basedirTag) + assert.strictEqual(typeof basedirTag[1], 'string') + assert.ok(nameTag) + assert.strictEqual(typeof nameTag[1], 'string') + + assert.ok(typeTag) + assert.strictEqual(typeTag[1], 'script') - expect(result.tags.find(([name]) => name === 'entrypoint.basedir')[1]).to.equal('test') - expect(result.tags.find(([name]) => name === 'entrypoint.name')[1]).to.equal('process-tags.spec') - expect(result.tags.find(([name]) => name === 'entrypoint.type')[1]).to.equal('script') - expect(result.tags.find(([name]) => name === 'entrypoint.workdir')[1]).to.equal('dd-trace-js') - expect(result.tags.find(([name]) => name === 'package.json.name')[1]).to.equal('dd-trace') + assert.ok(workdirTag) + assert.strictEqual(workdirTag[1], 'dd-trace-js') + + // Package name should exist but may vary depending on test runner + assert.ok(packageNameTag) + assert.strictEqual(typeof packageNameTag[1], 'string') }) it('should sort tags alphabetically', () => { - const result = getProcessTags() - - expect(result.tags[0][0]).to.equal('entrypoint.basedir') - expect(result.tags[1][0]).to.equal('entrypoint.name') - expect(result.tags[2][0]).to.equal('entrypoint.type') - expect(result.tags[3][0]).to.equal('entrypoint.workdir') - expect(result.tags[4][0]).to.equal('package.json.name') + assertObjectContains(processTags.tags, [ + ['entrypoint.basedir'], + ['entrypoint.name'], + ['entrypoint.type'], + ['entrypoint.workdir'], + ['package.json.name'] + ]) }) it('should serialize tags correctly', () => { - const result = getProcessTags() - // serialized should be comma-separated and not include undefined values - if (result.serialized) { - const parts = result.serialized.split(',') - expect(parts.length).to.be.greaterThan(0) + if (processTags.serialized) { + const parts = processTags.serialized.split(',') + assert.ok(parts.length > 0) parts.forEach(part => { - expect(part).to.include(':') - expect(part).to.not.include('undefined') + assert.match(part, /:/) + assert.doesNotMatch(part, /undefined/) }) } }) }) - describe('serialize', () => { - it('should serialize tags as name:value pairs joined by commas', () => { - const tags = [ - ['tag1', 'value1'], - ['tag2', 'value2'], - ['tag3', 'value3'] - ] - - const result = serialize(tags) - - expect(result).to.equal('tag1:value1,tag2:value2,tag3:value3') - }) - - it('should filter out tags with undefined values', () => { - const tags = [ - ['tag1', 'value1'], - ['tag2', undefined], - ['tag3', 'value3'], - ['tag4', undefined] - ] - - const result = serialize(tags) - - expect(result).to.equal('tag1:value1,tag3:value3') - expect(result).to.not.include('undefined') - }) - - it('should sanitize tag values', () => { - const tags = [ - ['tag1', 'Value With Spaces'], - ['tag2', 'UPPERCASE'], - ['tag3', 'special@chars!'] - ] - - const result = serialize(tags) - - expect(result).to.equal('tag1:value_with_spaces,tag2:uppercase,tag3:special_chars_') - }) - - it('should return empty string when all values are undefined', () => { - const tags = [ - ['tag1', undefined], - ['tag2', undefined] - ] - - const result = serialize(tags) - - expect(result).to.equal('') - }) - - it('should handle empty tags array', () => { - const result = serialize([]) - - expect(result).to.equal('') - }) - - it('should handle numeric values', () => { - const tags = [ - ['tag1', 123], - ['tag2', 456] - ] - - const result = serialize(tags) - - expect(result).to.equal('tag1:123,tag2:456') - }) - - it('should handle mixed defined and undefined values', () => { - const tags = [ - ['tag1', 'value1'], - ['tag2', undefined], - ['tag3', 'value3'], - ['tag4', undefined], - ['tag5', 'value5'] - ] - - const result = serialize(tags) - - expect(result).to.equal('tag1:value1,tag3:value3,tag5:value5') - }) - }) - describe('sanitize', () => { it('should convert to lowercase', () => { - expect(sanitize('UPPERCASE')).to.equal('uppercase') - expect(sanitize('MixedCase')).to.equal('mixedcase') - expect(sanitize('CamelCase')).to.equal('camelcase') + assert.strictEqual(sanitize('UPPERCASE'), 'uppercase') + assert.strictEqual(sanitize('MixedCase'), 'mixedcase') + assert.strictEqual(sanitize('CamelCase'), 'camelcase') }) it('should replace spaces with underscores', () => { - expect(sanitize('hello world')).to.equal('hello_world') - expect(sanitize('multiple spaces')).to.equal('multiple_spaces') + assert.strictEqual(sanitize('hello world'), 'hello_world') + assert.strictEqual(sanitize('multiple spaces'), 'multiple_spaces') }) it('should replace special characters with underscores', () => { - expect(sanitize('hello@world')).to.equal('hello_world') - expect(sanitize('hello!world')).to.equal('hello_world') - expect(sanitize('hello#world')).to.equal('hello_world') - expect(sanitize('hello$world')).to.equal('hello_world') - expect(sanitize('hello%world')).to.equal('hello_world') - expect(sanitize('hello&world')).to.equal('hello_world') - expect(sanitize('hello*world')).to.equal('hello_world') + assert.strictEqual(sanitize('hello@world'), 'hello_world') + assert.strictEqual(sanitize('hello!world'), 'hello_world') + assert.strictEqual(sanitize('hello#world'), 'hello_world') + assert.strictEqual(sanitize('hello$world'), 'hello_world') + assert.strictEqual(sanitize('hello%world'), 'hello_world') + assert.strictEqual(sanitize('hello&world'), 'hello_world') + assert.strictEqual(sanitize('hello*world'), 'hello_world') }) it('should preserve forward slashes', () => { - expect(sanitize('path/to/file')).to.equal('path/to/file') - expect(sanitize('foo/bar/baz')).to.equal('foo/bar/baz') + assert.strictEqual(sanitize('path/to/file'), 'path/to/file') + assert.strictEqual(sanitize('foo/bar/baz'), 'foo/bar/baz') }) it('should preserve underscores', () => { - expect(sanitize('hello_world')).to.equal('hello_world') - expect(sanitize('foo_bar_baz')).to.equal('foo_bar_baz') + assert.strictEqual(sanitize('hello_world'), 'hello_world') + assert.strictEqual(sanitize('foo_bar_baz'), 'foo_bar_baz') }) it('should preserve dots', () => { - expect(sanitize('file.txt')).to.equal('file.txt') - expect(sanitize('my.package.name')).to.equal('my.package.name') + assert.strictEqual(sanitize('file.txt'), 'file.txt') + assert.strictEqual(sanitize('my.package.name'), 'my.package.name') }) it('should preserve hyphens', () => { - expect(sanitize('my-package')).to.equal('my-package') - expect(sanitize('foo-bar-baz')).to.equal('foo-bar-baz') + assert.strictEqual(sanitize('my-package'), 'my-package') + assert.strictEqual(sanitize('foo-bar-baz'), 'foo-bar-baz') }) it('should preserve alphanumeric characters', () => { - expect(sanitize('abc123')).to.equal('abc123') - expect(sanitize('ABC123')).to.equal('abc123') - expect(sanitize('test123abc')).to.equal('test123abc') + assert.strictEqual(sanitize('abc123'), 'abc123') + assert.strictEqual(sanitize('ABC123'), 'abc123') + assert.strictEqual(sanitize('test123abc'), 'test123abc') }) it('should handle multiple consecutive special characters', () => { - expect(sanitize('hello!!!world')).to.equal('hello_world') - expect(sanitize('foo@@@bar')).to.equal('foo_bar') - expect(sanitize('test spaces')).to.equal('test_spaces') + assert.strictEqual(sanitize('hello!!!world'), 'hello_world') + assert.strictEqual(sanitize('foo@@@bar'), 'foo_bar') + assert.strictEqual(sanitize('test spaces'), 'test_spaces') }) it('should handle complex combinations', () => { - expect(sanitize('My-Package_Name/v1.2.3')).to.equal('my-package_name/v1.2.3') - expect(sanitize('foo@bar#baz.txt')).to.equal('foo_bar_baz.txt') - expect(sanitize('Test File (Copy).js')).to.equal('test_file_copy_.js') + assert.strictEqual(sanitize('My-Package_Name/v1.2.3'), 'my-package_name/v1.2.3') + assert.strictEqual(sanitize('foo@bar#baz.txt'), 'foo_bar_baz.txt') + assert.strictEqual(sanitize('Test File (Copy).js'), 'test_file_copy_.js') }) it('should convert non-string values to strings first', () => { - expect(sanitize(123)).to.equal('123') - expect(sanitize(true)).to.equal('true') - expect(sanitize(false)).to.equal('false') + // @ts-expect-error: intentionally passing invalid types to test robustness + assert.strictEqual(sanitize(123), '123') + // @ts-expect-error: intentionally passing invalid types to test robustness + assert.strictEqual(sanitize(true), 'true') + // @ts-expect-error: intentionally passing invalid types to test robustness + assert.strictEqual(sanitize(false), 'false') }) it('should handle empty string', () => { - expect(sanitize('')).to.equal('') + assert.strictEqual(sanitize(''), '') }) it('should handle strings with only special characters', () => { - expect(sanitize('!!!')).to.equal('_') - expect(sanitize('@@@')).to.equal('_') - expect(sanitize(' ')).to.equal('_') + assert.strictEqual(sanitize('!!!'), '_') + assert.strictEqual(sanitize('@@@'), '_') + assert.strictEqual(sanitize(' '), '_') }) it('should handle unicode characters', () => { - expect(sanitize('hello™world')).to.equal('hello_world') - expect(sanitize('café')).to.equal('caf_') - expect(sanitize('日本語')).to.equal('_') + assert.strictEqual(sanitize('hello™world'), 'hello_world') + assert.strictEqual(sanitize('café'), 'caf_') + assert.strictEqual(sanitize('日本語'), '_') }) it('should handle brackets and parentheses', () => { - expect(sanitize('func()')).to.equal('func_') - expect(sanitize('array[0]')).to.equal('array_0_') - expect(sanitize('{object}')).to.equal('_object_') + assert.strictEqual(sanitize('func()'), 'func_') + assert.strictEqual(sanitize('array[0]'), 'array_0_') + assert.strictEqual(sanitize('{object}'), '_object_') }) it('should handle quotes and backticks', () => { - expect(sanitize('"quoted"')).to.equal('_quoted_') - expect(sanitize("'quoted'")).to.equal('_quoted_') - expect(sanitize('`backtick`')).to.equal('_backtick_') + assert.strictEqual(sanitize('"quoted"'), '_quoted_') + assert.strictEqual(sanitize("'quoted'"), '_quoted_') + assert.strictEqual(sanitize('`backtick`'), '_backtick_') }) it('should preserve allowed characters in combination', () => { - expect(sanitize('my_file-v1.0/test.js')).to.equal('my_file-v1.0/test.js') - expect(sanitize('package_name-2.4.6/lib/index.js')).to.equal('package_name-2.4.6/lib/index.js') + assert.strictEqual(sanitize('my_file-v1.0/test.js'), 'my_file-v1.0/test.js') + assert.strictEqual(sanitize('package_name-2.4.6/lib/index.js'), 'package_name-2.4.6/lib/index.js') }) }) describe('DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED', () => { let env - let getConfig let SpanProcessor beforeEach(() => { @@ -277,54 +240,53 @@ describe('process-tags', () => { afterEach(() => { process.env = env - delete require.cache[require.resolve('../src/config')] delete require.cache[require.resolve('../src/span_processor')] + delete require.cache[require.resolve('../src/process-tags')] }) it('should enable process tags propagation when set to true', () => { process.env.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED = 'true' - getConfig = require('../src/config') - const config = getConfig() + // Need to reload config first, then process-tags (which reads from config) + delete require.cache[require.resolve('../src/process-tags')] + + const config = getConfigFresh() - expect(config.propagateProcessTags).to.exist - expect(config.propagateProcessTags.enabled).to.equal(true) + assert.ok(config.propagateProcessTags) + assert.strictEqual(config.propagateProcessTags.enabled, true) SpanProcessor = require('../src/span_processor') const processor = new SpanProcessor(undefined, undefined, config) - expect(processor._processTags).to.be.a('string') - expect(processor._processTags).to.not.be.false - expect(processor._processTags).to.include('entrypoint') + assert.ok(typeof processor._processTags === 'string') + assert.match(processor._processTags, /entrypoint/) }) it('should disable process tags propagation when set to false', () => { process.env.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED = 'false' - getConfig = require('../src/config') - const config = getConfig() + const config = getConfigFresh() - expect(config.propagateProcessTags).to.exist - expect(config.propagateProcessTags.enabled).to.equal(false) + assert.ok(config.propagateProcessTags) + assert.strictEqual(config.propagateProcessTags.enabled, false) SpanProcessor = require('../src/span_processor') const processor = new SpanProcessor(undefined, undefined, config) - expect(processor._processTags).to.equal(false) + assert.strictEqual(processor._processTags, false) }) it('should disable process tags propagation when not set', () => { // Don't set the environment variable - getConfig = require('../src/config') - const config = getConfig() + const config = getConfigFresh() - expect(config.propagateProcessTags?.enabled).to.not.equal(true) + assert.notStrictEqual(config.propagateProcessTags?.enabled, true) SpanProcessor = require('../src/span_processor') const processor = new SpanProcessor(undefined, undefined, config) - expect(processor._processTags).to.equal(false) + assert.strictEqual(processor._processTags, false) }) }) }) diff --git a/packages/dd-trace/test/profiling/config.spec.js b/packages/dd-trace/test/profiling/config.spec.js index 98d75126013..240b5e48b0e 100644 --- a/packages/dd-trace/test/profiling/config.spec.js +++ b/packages/dd-trace/test/profiling/config.spec.js @@ -1,14 +1,14 @@ 'use strict' const assert = require('node:assert/strict') - -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach } = require('tap').mocha const os = require('node:os') const path = require('node:path') -require('../setup/core') +const { describe, it, beforeEach, afterEach } = require('mocha') +const satisfies = require('semifies') +const { assertObjectContains } = require('../../../../integration-tests/helpers') +require('../setup/core') const { AgentExporter } = require('../../src/profiling/exporters/agent') const { FileExporter } = require('../../src/profiling/exporters/file') const WallProfiler = require('../../src/profiling/profilers/wall') @@ -18,6 +18,8 @@ const { ConsoleLogger } = require('../../src/profiling/loggers/console') const samplingContextsAvailable = process.platform !== 'win32' const oomMonitoringSupported = process.platform !== 'win32' +const isAtLeast24 = satisfies(process.versions.node, '>=24.0.0') +const zstdOrGzip = isAtLeast24 ? 'zstd' : 'gzip' describe('config', () => { let Config @@ -59,7 +61,7 @@ describe('config', () => { assert.strictEqual(config.profilers[1].codeHotspotsEnabled(), samplingContextsAvailable) assert.strictEqual(config.v8ProfilerBugWorkaroundEnabled, true) assert.strictEqual(config.cpuProfilingEnabled, samplingContextsAvailable) - assert.strictEqual(config.uploadCompression.method, 'gzip') + assert.strictEqual(config.uploadCompression.method, zstdOrGzip) assert.strictEqual(config.uploadCompression.level, undefined) }) @@ -508,6 +510,59 @@ describe('config', () => { }) } + describe('async context', () => { + const isSupported = samplingContextsAvailable && isAtLeast24 + describe('where supported', () => { + it('should be on by default', function () { + if (!isSupported) { + this.skip() + } else { + const config = new Config({}) + assert.strictEqual(config.asyncContextFrameEnabled, true) + } + }) + + it('can be turned off by env var', function () { + if (!isSupported) { + this.skip() + } else { + process.env.DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED = '0' + try { + const config = new Config({}) + assert.strictEqual(config.asyncContextFrameEnabled, false) + } finally { + delete process.env.DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED + } + } + }) + }) + + describe('where not supported', function () { + it('should be off by default', function () { + if (isSupported) { + this.skip() + } else { + const config = new Config({}) + assert.strictEqual(config.asyncContextFrameEnabled, false) + } + }) + + it('can not be turned on by env var', function () { + if (isSupported) { + this.skip() + } else { + process.env.DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED = '1' + try { + const config = new Config({}) + assert.strictEqual(config.asyncContextFrameEnabled, false) + } finally { + delete process.env.DD_PROFILING_ASYNC_CONTEXT_FRAME_ENABLED + } + } + }) + }) + }) + describe('upload compression settings', () => { const expectConfig = (env, method, level, warning) => { process.env = { @@ -536,15 +591,15 @@ describe('config', () => { } it('should accept known methods', () => { - expectConfig(undefined, 'gzip', undefined) + expectConfig(undefined, zstdOrGzip, undefined) expectConfig('off', 'off', undefined) - expectConfig('on', 'gzip', undefined) + expectConfig('on', zstdOrGzip, undefined) expectConfig('gzip', 'gzip', undefined) expectConfig('zstd', 'zstd', undefined) }) it('should reject unknown methods', () => { - expectConfig('foo', 'gzip', undefined, 'Invalid profile upload compression method "foo". Will use "on".') + expectConfig('foo', zstdOrGzip, undefined, 'Invalid profile upload compression method "foo". Will use "on".') }) it('should accept supported compression levels in methods that support levels', () => { @@ -564,7 +619,7 @@ describe('config', () => { it('should reject compression levels in methods that do not support levels', () => { ['on', 'off'].forEach((method) => { - const effectiveMethod = method === 'on' ? 'gzip' : method + const effectiveMethod = method === 'on' ? zstdOrGzip : method expectConfig(`${method}-3`, effectiveMethod, undefined, `Compression levels are not supported for "${method}".`) expectConfig(`${method}-foo`, effectiveMethod, undefined, diff --git a/packages/dd-trace/test/profiling/exporters/agent.spec.js b/packages/dd-trace/test/profiling/exporters/agent.spec.js index 25adf1794c5..7fc5b0a1646 100644 --- a/packages/dd-trace/test/profiling/exporters/agent.spec.js +++ b/packages/dd-trace/test/profiling/exporters/agent.spec.js @@ -1,20 +1,18 @@ 'use strict' const assert = require('node:assert/strict') +const os = require('node:os') +const path = require('node:path') +const { request } = require('node:http') -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const express = require('express') const upload = require('multer')() -const { Profile } = require('../../../../../vendor/dist/pprof-format') -const os = require('node:os') -const path = require('node:path') -const { request } = require('node:http') +const { Profile } = require('../../../../../vendor/dist/pprof-format') require('../../setup/core') - const tracer = require('../../../../../init') const WallProfiler = require('../../../src/profiling/profilers/wall') const SpaceProfiler = require('../../../src/profiling/profilers/space') @@ -132,8 +130,8 @@ describe('exporters/agent', function () { const wallProfile = Profile.decode(req.files[1].buffer) const spaceProfile = Profile.decode(req.files[2].buffer) - expect(wallProfile).to.be.a.profile - expect(spaceProfile).to.be.a.profile + assertIsProfile(wallProfile) + assertIsProfile(spaceProfile) assert.deepStrictEqual(wallProfile, Profile.decode(profiles.wall)) assert.deepStrictEqual(spaceProfile, Profile.decode(profiles.space)) @@ -440,7 +438,7 @@ describe('exporters/agent', function () { }) }) - describe('using UDS', () => { + ;(os.platform() === 'win32' ? describe.skip : describe)('using UDS', () => { let listener beforeEach(done => { @@ -488,5 +486,58 @@ describe('exporters/agent', function () { exporter.export({ profiles, start, end, tags }).catch(reject) })) }) - }, { skip: os.platform() === 'win32' }) + }) }) + +function assertIsProfile (obj, msg) { + assert.ok(typeof obj === 'object' && obj !== null, msg) + assert.strictEqual(typeof obj.timeNanos, 'bigint', msg) + assert.ok(typeof obj.period === 'number' || typeof obj.period === 'bigint', msg) + + assertIsValueType(obj.periodType, msg) + + assert.ok(Array.isArray(obj.sampleType), msg) + assert.strictEqual(obj.sampleType.length, 2, msg) + assert.ok(Array.isArray(obj.sample), msg) + assert.ok(Array.isArray(obj.location), msg) + assert.ok(Array.isArray(obj.function), msg) + + assert.ok(typeof obj.stringTable === 'object' && obj.stringTable !== null, msg) + assert.ok(Array.isArray(obj.stringTable.strings), msg) + assert.ok(obj.stringTable.strings.length >= 1, msg) + assert.strictEqual(obj.stringTable.strings[0], '', msg) + + for (const sampleType of obj.sampleType) { + assertIsValueType(sampleType, msg) + } + + for (const fn of obj.function) { + assert.strictEqual(typeof fn.filename, 'number', msg) + assert.strictEqual(typeof fn.systemName, 'number', msg) + assert.strictEqual(typeof fn.name, 'number', msg) + assert.ok(Number.isSafeInteger(fn.id), msg) + } + + for (const location of obj.location) { + assert.ok(Number.isSafeInteger(location.id), msg) + assert.ok(Array.isArray(location.line), msg) + + for (const line of location.line) { + assert.ok(Number.isSafeInteger(line.functionId), msg) + assert.strictEqual(typeof line.line, 'number', msg) + } + } + + for (const sample of obj.sample) { + assert.ok(Array.isArray(sample.locationId), msg) + assert.ok(sample.locationId.length >= 1, msg) + assert.ok(Array.isArray(sample.value), msg) + assert.strictEqual(sample.value.length, obj.sampleType.length, msg) + } + + function assertIsValueType (valueType, msg) { + assert.ok(typeof valueType === 'object' && valueType !== null, msg) + assert.strictEqual(typeof valueType.type, 'number', msg) + assert.strictEqual(typeof valueType.unit, 'number', msg) + } +} diff --git a/packages/dd-trace/test/profiling/exporters/file.spec.js b/packages/dd-trace/test/profiling/exporters/file.spec.js index dfa830d6c85..bfc974bc8fc 100644 --- a/packages/dd-trace/test/profiling/exporters/file.spec.js +++ b/packages/dd-trace/test/profiling/exporters/file.spec.js @@ -1,6 +1,6 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/profiling/libuv-size.spec.js b/packages/dd-trace/test/profiling/libuv-size.spec.js index e7cdd5701af..de9be67f850 100644 --- a/packages/dd-trace/test/profiling/libuv-size.spec.js +++ b/packages/dd-trace/test/profiling/libuv-size.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('../setup/core') +const { describe, it } = require('mocha') +require('../setup/core') const libuvSize = require('../../src/profiling/libuv-size') describe('libuv-size', function () { diff --git a/packages/dd-trace/test/profiling/loggers/console.spec.js b/packages/dd-trace/test/profiling/loggers/console.spec.js index ca7f00567dd..ec20277dbae 100644 --- a/packages/dd-trace/test/profiling/loggers/console.spec.js +++ b/packages/dd-trace/test/profiling/loggers/console.spec.js @@ -1,6 +1,6 @@ 'use strict' -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('../../setup/core') diff --git a/packages/dd-trace/test/profiling/profiler.spec.js b/packages/dd-trace/test/profiling/profiler.spec.js index cdd3d0a069d..aa7a3c687c8 100644 --- a/packages/dd-trace/test/profiling/profiler.spec.js +++ b/packages/dd-trace/test/profiling/profiler.spec.js @@ -2,8 +2,7 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -239,8 +238,8 @@ describe('profiler', function () { assert.ok(Object.hasOwn(profiles, 'space')) assert.ok(profiles.space instanceof Buffer) assert.strictEqual(profiles.space.indexOf(magicBytes), 0) - expect(start).to.be.a('date') - expect(end).to.be.a('date') + assert.ok((start) instanceof Date) + assert.ok((end) instanceof Date) assert.strictEqual(end - start, 65000) assert.strictEqual(tags.foo, 'foo') } @@ -311,8 +310,8 @@ describe('profiler', function () { await waitForExport() const { start, end } = exporter.export.args[0][0] - expect(start).to.be.a('date') - expect(end).to.be.a('date') + assert.ok((start) instanceof Date) + assert.ok((end) instanceof Date) assert.strictEqual(end - start, 65000) sinon.assert.calledOnce(exporter.export) @@ -324,8 +323,8 @@ describe('profiler', function () { const { start: start2, end: end2 } = exporter.export.args[0][0] assert.ok(start2 >= end) - expect(start2).to.be.a('date') - expect(end2).to.be.a('date') + assert.ok((start2) instanceof Date) + assert.ok((end2) instanceof Date) assert.strictEqual(end2 - start2, 65000) sinon.assert.calledOnce(exporter.export) diff --git a/packages/dd-trace/test/profiling/profilers/events.spec.js b/packages/dd-trace/test/profiling/profilers/events.spec.js index 7278c327f76..6b8e3ca9b56 100644 --- a/packages/dd-trace/test/profiling/profilers/events.spec.js +++ b/packages/dd-trace/test/profiling/profilers/events.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert') -const { describe, it } = require('tap').mocha + +const { describe, it } = require('mocha') const dc = require('dc-polyfill') require('../../setup/core') - const { storage } = require('../../../../datadog-core') const { availableParallelism, effectiveLibuvThreadCount } = require('../../../src/profiling/libuv-size') const EventsProfiler = require('../../../src/profiling/profilers/events') diff --git a/packages/dd-trace/test/profiling/profilers/poisson.spec.js b/packages/dd-trace/test/profiling/profilers/poisson.spec.js index 66b05d0a1a7..9884c999261 100644 --- a/packages/dd-trace/test/profiling/profilers/poisson.spec.js +++ b/packages/dd-trace/test/profiling/profilers/poisson.spec.js @@ -1,10 +1,10 @@ 'use strict' -const { describe, it, beforeEach } = require('tap').mocha const assert = require('node:assert') -require('../../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../../setup/core') const PoissonProcessSamplingFilter = require('../../../src/profiling/profilers/poisson') describe('PoissonProcessSamplingFilter', () => { diff --git a/packages/dd-trace/test/profiling/profilers/space.spec.js b/packages/dd-trace/test/profiling/profilers/space.spec.js index 863e2ba077b..4a7591586ae 100644 --- a/packages/dd-trace/test/profiling/profilers/space.spec.js +++ b/packages/dd-trace/test/profiling/profilers/space.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') diff --git a/packages/dd-trace/test/profiling/profilers/wall.spec.js b/packages/dd-trace/test/profiling/profilers/wall.spec.js index a37fae3e09e..a20384ea0d6 100644 --- a/packages/dd-trace/test/profiling/profilers/wall.spec.js +++ b/packages/dd-trace/test/profiling/profilers/wall.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') diff --git a/packages/dd-trace/test/profiling/tagger.spec.js b/packages/dd-trace/test/profiling/tagger.spec.js index e6deb40df25..bebf41bec1b 100644 --- a/packages/dd-trace/test/profiling/tagger.spec.js +++ b/packages/dd-trace/test/profiling/tagger.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') require('../setup/core') diff --git a/packages/dd-trace/test/proxy.spec.js b/packages/dd-trace/test/proxy.spec.js index 7f6e05fa823..3199febc564 100644 --- a/packages/dd-trace/test/proxy.spec.js +++ b/packages/dd-trace/test/proxy.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -35,7 +36,7 @@ describe('TracerProxy', () => { let PluginManager let pluginManager let flare - let remoteConfig + let RemoteConfig let handlers let rc let dogStatsD @@ -45,7 +46,7 @@ describe('TracerProxy', () => { let openfeatureProvider beforeEach(() => { - process.env.DD_TRACE_MOCHA_ENABLED = false + process.env.DD_TRACE_MOCHA_ENABLED = 'false' aiguardSdk = { evaluate: sinon.stub(), @@ -212,18 +213,19 @@ describe('TracerProxy', () => { cleanup: sinon.spy() } - remoteConfig = { - enable: sinon.stub() - } + RemoteConfig = sinon.stub().returns(rc) handlers = new Map() rc = { setProductHandler (product, handler) { handlers.set(product, handler) }, - removeProductHandler (product) { handlers.delete(product) } + removeProductHandler (product) { handlers.delete(product) }, + updateCapabilities: sinon.spy(), + setBatchHandler: sinon.spy(), + removeBatchHandler: sinon.spy(), + subscribeProducts: sinon.spy(), + unsubscribeProducts: sinon.spy() } - remoteConfig.enable.returns(rc) - NoopProxy = proxyquire('../src/noop/proxy', { './tracer': NoopTracer, '../aiguard/noop': NoopAIGuardSdk, @@ -242,7 +244,7 @@ describe('TracerProxy', () => { './appsec': appsec, './appsec/iast': iast, './telemetry': telemetry, - './remote_config': remoteConfig, + './remote_config': RemoteConfig, './aiguard/sdk': AIGuardSdk, './appsec/sdk': AppsecSdk, './dogstatsd': dogStatsD, @@ -268,7 +270,7 @@ describe('TracerProxy', () => { sinon.assert.calledWith(Config, options) sinon.assert.calledWith(DatadogTracer, config) - sinon.assert.calledOnceWithMatch(remoteConfig.enable, config) + sinon.assert.calledOnceWithExactly(RemoteConfig, config) }) it('should not initialize twice', () => { @@ -276,7 +278,7 @@ describe('TracerProxy', () => { proxy.init() sinon.assert.calledOnce(DatadogTracer) - sinon.assert.calledOnce(remoteConfig.enable) + sinon.assert.calledOnce(RemoteConfig) }) it('should not enable remote config when disabled', () => { @@ -285,7 +287,7 @@ describe('TracerProxy', () => { proxy.init() sinon.assert.calledOnce(DatadogTracer) - sinon.assert.notCalled(remoteConfig.enable) + sinon.assert.notCalled(RemoteConfig) }) it('should not initialize when disabled', () => { @@ -394,7 +396,7 @@ describe('TracerProxy', () => { './tracer': DatadogTracer, './appsec': appsec, './appsec/iast': iast, - './remote_config': remoteConfig, + './remote_config': RemoteConfig, './appsec/sdk': AppsecSdk }) @@ -425,7 +427,7 @@ describe('TracerProxy', () => { './config': Config, './appsec': appsec, './appsec/iast': iast, - './remote_config': remoteConfig, + './remote_config': RemoteConfig, './appsec/sdk': AppsecSdk }) @@ -566,7 +568,7 @@ describe('TracerProxy', () => { './profiler': null, // this will cause the import failure error './appsec': appsec, './telemetry': telemetry, - './remote_config': remoteConfig + './remote_config': RemoteConfig }) const profilerImportFailureProxy = new ProfilerImportFailureProxy() @@ -594,7 +596,7 @@ describe('TracerProxy', () => { './config': Config, './appsec': appsec, './appsec/iast': iast, - './remote_config': remoteConfig, + './remote_config': RemoteConfig, './appsec/sdk': AppsecSdk, './standalone': standalone, './telemetry': telemetry diff --git a/packages/dd-trace/test/ramdom_sampler.spec.js b/packages/dd-trace/test/ramdom_sampler.spec.js index 7fcaa6b20e6..bc710ec583d 100644 --- a/packages/dd-trace/test/ramdom_sampler.spec.js +++ b/packages/dd-trace/test/ramdom_sampler.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('./setup/core') diff --git a/packages/dd-trace/test/rate_limiter.spec.js b/packages/dd-trace/test/rate_limiter.spec.js index b3e29df5bf7..e20af01827c 100644 --- a/packages/dd-trace/test/rate_limiter.spec.js +++ b/packages/dd-trace/test/rate_limiter.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('./setup/core') - const RateLimiter = require('../src/rate_limiter') describe('RateLimiter', () => { diff --git a/packages/dd-trace/test/remote_config/index.spec.js b/packages/dd-trace/test/remote_config/index.spec.js index 09227bf42d8..5469d1ec18a 100644 --- a/packages/dd-trace/test/remote_config/index.spec.js +++ b/packages/dd-trace/test/remote_config/index.spec.js @@ -2,360 +2,805 @@ const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../setup/core') +const Capabilities = require('../../src/remote_config/capabilities') +const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('../../src/remote_config/apply_states') + +const noop = () => {} + +describe('RemoteConfig', () => { + let uuid + let scheduler + let Scheduler + let request + let log + let extraServices + let RemoteConfig + let config + let rc + let tagger -const getConfig = require('../../src/config') -const RuleManager = require('../../src/appsec/rule_manager') -const RemoteConfigCapabilities = require('../../src/remote_config/capabilities') -const { kPreUpdate } = require('../../src/remote_config/manager') - -let config -let rc -let RemoteConfigManager -let UserTracking -let log -let telemetry -let appsec -let remoteConfig - -describe('Remote Config index', () => { beforeEach(() => { - config = getConfig({ - appsec: { - enabled: undefined, - eventTracking: { - mode: 'identification' - } - } - }) + uuid = sinon.stub().returns('1234-5678') - rc = { - updateCapabilities: sinon.spy(), - on: sinon.spy(), - off: sinon.spy(), - setProductHandler: sinon.spy(), - removeProductHandler: sinon.spy() + scheduler = { + start: sinon.spy(), + stop: sinon.spy() } - RemoteConfigManager = sinon.stub().returns(rc) + Scheduler = sinon.stub().returns(scheduler) - UserTracking = { - setCollectionMode: sinon.stub() - } + request = sinon.stub() log = { - error: sinon.stub() + error: sinon.spy() } - telemetry = { - updateConfig: sinon.stub() + tagger = { + add: sinon.stub() } - appsec = { - enable: sinon.spy(), - disable: sinon.spy() - } + extraServices = [] - remoteConfig = proxyquire('../../src/remote_config', { - './manager': RemoteConfigManager, - '../appsec/user_tracking': UserTracking, + RemoteConfig = proxyquire('../../src/remote_config', { + '../../../../vendor/dist/crypto-randomuuid': uuid, + './scheduler': Scheduler, + '../../../../package.json': { version: '3.0.0' }, + '../exporters/common/request': request, '../log': log, - '../telemetry': telemetry + '../tagger': tagger, + '../service-naming/extra-services': { + getExtraServices: () => extraServices + } }) + + config = { + url: 'http://127.0.0.1:1337', + hostname: '127.0.0.1', + port: '1337', + tags: { + 'runtime-id': 'runtimeId' + }, + service: 'serviceName', + env: 'serviceEnv', + version: 'appVersion', + remoteConfig: { + pollInterval: 5 + } + } + + rc = new RemoteConfig(config) }) - describe('enable', () => { - it('should listen to remote config when appsec is not explicitly configured', () => { - config.appsec.enabled = undefined + it('should instantiate RemoteConfig', () => { + sinon.stub(rc, 'poll') + + sinon.assert.calledOnce(Scheduler) + const [firstArg, secondArg] = Scheduler.firstCall.args + assert.strictEqual(typeof firstArg, 'function') + assert.strictEqual(secondArg, 5e3) + + firstArg(noop) + sinon.assert.calledOnceWithExactly(rc.poll, noop) + + assert.strictEqual(rc.scheduler, scheduler) - remoteConfig.enable(config) + assert.deepStrictEqual(rc.url, config.url) - sinon.assert.calledOnceWithExactly(RemoteConfigManager, config) - sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION, true) - sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) - sinon.assert.calledWith(rc.setProductHandler, 'ASM_FEATURES') - assert.strictEqual(typeof rc.setProductHandler.firstCall.args[1], 'function') + sinon.assert.calledOnceWithExactly(tagger.add, config.tags, { + '_dd.rc.client_id': '1234-5678' }) - it('should listen to remote config when appsec is explicitly configured as enabled=true', () => { - config.appsec.enabled = true + assert.deepStrictEqual(rc.state, { + client: { + state: { + root_version: 1, + targets_version: 0, + config_states: [], + has_error: false, + error: '', + backend_client_state: '' + }, + id: '1234-5678', + products: [], + is_tracer: true, + client_tracer: { + runtime_id: config.tags['runtime-id'], + language: 'node', + tracer_version: '3.0.0', + service: config.service, + env: config.env, + app_version: config.version, + extra_services: [], + tags: ['runtime-id:runtimeId'], + process_tags: rc.state.client.client_tracer.process_tags + }, + capabilities: 'AA==' + }, + cached_target_files: [] + }) + + assert.ok(rc.appliedConfigs instanceof Map) + }) + + it('should include process_tags in client_tracer', () => { + const clientTracer = rc.state.client.client_tracer + + assert.ok(clientTracer.process_tags, 'process_tags should exist') + assert.ok(Array.isArray(clientTracer.process_tags), 'process_tags should be an array') + + // Verify expected process tag keys are present + assert.ok(clientTracer.process_tags.some(tag => tag.startsWith('entrypoint.basedir:'))) + assert.ok(clientTracer.process_tags.some(tag => tag.startsWith('entrypoint.name:'))) + assert.ok(clientTracer.process_tags.some(tag => tag.startsWith('entrypoint.type:'))) + assert.ok(clientTracer.process_tags.some(tag => tag.startsWith('entrypoint.workdir:'))) + + // Verify entrypoint.type has expected value + assert.ok(clientTracer.process_tags.some(tag => tag === 'entrypoint.type:script')) + }) + + it('should add git metadata to tags if present', () => { + const configWithGit = { + ...config, + repositoryUrl: 'https://github.com/DataDog/dd-trace-js', + commitSHA: '1234567890' + } + const rc = new RemoteConfig(configWithGit) + assert.deepStrictEqual(rc.state.client.client_tracer.tags, [ + 'runtime-id:runtimeId', + 'git.repository_url:https://github.com/DataDog/dd-trace-js', + 'git.commit.sha:1234567890' + ]) + }) + + describe('updateCapabilities', () => { + it('should set multiple capabilities to true', () => { + rc.updateCapabilities(Capabilities.ASM_ACTIVATION, true) + assert.strictEqual(rc.state.client.capabilities, 'Ag==') + + rc.updateCapabilities(Capabilities.ASM_IP_BLOCKING, true) + assert.strictEqual(rc.state.client.capabilities, 'Bg==') - remoteConfig.enable(config) + rc.updateCapabilities(Capabilities.ASM_DD_RULES, true) + assert.strictEqual(rc.state.client.capabilities, 'Dg==') - sinon.assert.calledOnceWithExactly(RemoteConfigManager, config) - sinon.assert.neverCalledWith(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION) - sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) - sinon.assert.calledOnceWithMatch(rc.setProductHandler, 'ASM_FEATURES') - assert.strictEqual(typeof rc.setProductHandler.firstCall.args[1], 'function') + rc.updateCapabilities(Capabilities.ASM_USER_BLOCKING, true) + assert.strictEqual(rc.state.client.capabilities, 'jg==') }) - it('should not listen to remote config when appsec is explicitly configured as enabled=false', () => { - config.appsec.enabled = false + it('should set multiple capabilities to false', () => { + rc.state.client.capabilities = 'jg==' - remoteConfig.enable(config) + rc.updateCapabilities(Capabilities.ASM_USER_BLOCKING, false) + assert.strictEqual(rc.state.client.capabilities, 'Dg==') - sinon.assert.calledOnceWithExactly(RemoteConfigManager, config) - sinon.assert.neverCalledWith(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION, true) - sinon.assert.neverCalledWith(rc.updateCapabilities, RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) - sinon.assert.notCalled(rc.setProductHandler) + rc.updateCapabilities(Capabilities.ASM_ACTIVATION, false) + assert.strictEqual(rc.state.client.capabilities, 'DA==') + + rc.updateCapabilities(Capabilities.ASM_IP_BLOCKING, false) + assert.strictEqual(rc.state.client.capabilities, 'CA==') + + rc.updateCapabilities(Capabilities.ASM_DD_RULES, false) + assert.strictEqual(rc.state.client.capabilities, 'AA==') }) - it('should always enable FFE_FLAG_CONFIGURATION_RULES capability', () => { - remoteConfig.enable(config) + it('should set an arbitrary amount of capabilities', () => { + rc.updateCapabilities(1n << 1n, true) + rc.updateCapabilities(1n << 200n, true) + assert.strictEqual(rc.state.client.capabilities, 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=') - sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true) + rc.updateCapabilities(1n << 200n, false) + assert.strictEqual(rc.state.client.capabilities, 'Ag==') }) + }) - describe('ASM_FEATURES remote config listener', () => { - let listener + describe('setProductHandler/removeProductHandler', () => { + it('should update the product list and autostart or autostop', () => { + sinon.assert.notCalled(rc.scheduler.start) - beforeEach(() => { - config.appsec.enabled = undefined - remoteConfig.enable(config, appsec) + rc.setProductHandler('ASM_FEATURES', noop) - listener = rc.setProductHandler.firstCall.args[1] - }) + assert.deepStrictEqual(rc.state.client.products, ['ASM_FEATURES']) + sinon.assert.called(rc.scheduler.start) - it('should enable appsec when listener is called with apply and enabled', () => { - listener('apply', { asm: { enabled: true } }) + rc.setProductHandler('ASM_DATA', noop) + rc.setProductHandler('ASM_DD', noop) - sinon.assert.called(appsec.enable) - }) + assert.deepStrictEqual(rc.state.client.products, ['ASM_FEATURES', 'ASM_DATA', 'ASM_DD']) - it('should enable appsec when listener is called with modify and enabled', () => { - listener('modify', { asm: { enabled: true } }) + rc.removeProductHandler('ASM_FEATURES') - sinon.assert.called(appsec.enable) - }) + assert.deepStrictEqual(rc.state.client.products, ['ASM_DATA', 'ASM_DD']) - it('should disable appsec when listener is called with unapply and enabled', () => { - listener('unapply', { asm: { enabled: true } }) + rc.removeProductHandler('ASM_DATA') - sinon.assert.calledOnce(appsec.disable) - }) + sinon.assert.notCalled(rc.scheduler.stop) + + rc.removeProductHandler('ASM_DD') + + sinon.assert.called(rc.scheduler.stop) + assert.strictEqual(rc.state.client.products.length, 0) + }) + }) + + describe('poll', () => { + let expectedPayload + + beforeEach(() => { + sinon.stub(rc, 'parseConfig') + expectedPayload = { + url: rc.url, + method: 'POST', + path: '/v0.7/config', + headers: { 'Content-Type': 'application/json; charset=utf-8' } + } + }) - it('should not do anything when listener is called with apply and malformed data', () => { - listener('apply', {}) + it('should request and do nothing when received status 404', (cb) => { + request.yieldsRight(new Error('Response received 404'), '{"a":"b"}', 404) - sinon.assert.notCalled(appsec.enable) - sinon.assert.notCalled(appsec.disable) + const payload = JSON.stringify(rc.state) + + rc.poll(() => { + sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(rc.parseConfig) + cb() }) + }) - describe('update config origin activation', () => { - const rcConfigAsmEnabling = { asm: { enabled: true } } - const rcConfigAsmDisabling = { asm: { enabled: true } } + it('should request when received error', (cb) => { + const err = new Error('Response received 500') + request.yieldsRight(err, '{"a":"b"}', 500) - it('should update appsec.enabled when applying asm enabling by RC', () => { - listener('apply', rcConfigAsmEnabling) + const payload = JSON.stringify(rc.state) - sinon.assert.calledOnce(telemetry.updateConfig) - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].name, 'appsec.enabled') - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].origin, 'remote_config') - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].value, rcConfigAsmEnabling.asm.enabled) - }) + rc.poll(() => { + sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) + sinon.assert.notCalled(rc.parseConfig) + cb() + }) + }) - it('should update appsec.enabled when modifying asm enabling by RC', () => { - listener('modify', rcConfigAsmDisabling) + it('should request and call parseConfig when payload is not empty', (cb) => { + request.yieldsRight(null, '{"a":"b"}', 200) - sinon.assert.calledOnce(telemetry.updateConfig) - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].name, 'appsec.enabled') - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].origin, 'remote_config') - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].value, rcConfigAsmDisabling.asm.enabled) - }) + const payload = JSON.stringify(rc.state) - it('should update when unapplying asm enabling by RC', () => { - listener('unapply', { asm: { enabled: true } }) + rc.poll(() => { + sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) + sinon.assert.notCalled(log.error) + sinon.assert.calledOnceWithExactly(rc.parseConfig, { a: 'b' }) + cb() + }) + }) - sinon.assert.calledOnce(telemetry.updateConfig) - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].name, 'appsec.enabled') - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].origin, 'default') - assert.strictEqual(telemetry.updateConfig.firstCall.args[0][0].value, config.appsec.enabled) + it('should catch exceptions, update the error state, and clear the error state at next request', (cb) => { + const error = new Error('Unable to parse config') + request + .onFirstCall().yieldsRight(null, '{"a":"b"}', 200) + .onSecondCall().yieldsRight(null, null, 200) + rc.parseConfig.onFirstCall().throws(error) + + const payload = JSON.stringify(rc.state) + + rc.poll(() => { + sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) + sinon.assert.calledOnceWithExactly(rc.parseConfig, { a: 'b' }) + sinon.assert.calledOnceWithExactly(log.error, '[RC] Could not parse remote config response', error) + assert.strictEqual(rc.state.client.state.has_error, true) + assert.strictEqual(rc.state.client.state.error, 'Error: Unable to parse config') + + const payload2 = JSON.stringify(rc.state) + + rc.poll(() => { + sinon.assert.calledTwice(request) + sinon.assert.calledWith(request.secondCall, payload2, expectedPayload) + sinon.assert.calledOnce(rc.parseConfig) + sinon.assert.calledOnce(log.error) + assert.strictEqual(rc.state.client.state.has_error, false) + assert.strictEqual(rc.state.client.state.error.length, 0) + cb() }) }) + }) - describe('auto_user_instrum', () => { - const rcConfig = { auto_user_instrum: { mode: 'anonymous' } } - const configId = 'collectionModeId' + it('should request and do nothing when payload is empty JSON object', (cb) => { + request.yieldsRight(null, '{}', 200) - afterEach(() => { - listener('unapply', rcConfig, configId) - }) + const payload = JSON.stringify(rc.state) - it('should not update collection mode when not a string', () => { - listener('apply', { auto_user_instrum: { mode: 123 } }, configId) + rc.poll(() => { + sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(rc.parseConfig) + cb() + }) + }) - sinon.assert.notCalled(UserTracking.setCollectionMode) - }) + it('should include extra_services in the payload', (cb) => { + request.yieldsRight(null, '{}', 200) - it('should throw when called two times with different config ids', () => { - listener('apply', rcConfig, configId) + extraServices = ['test-service'] - assert.throws(() => listener('apply', rcConfig, 'anotherId')) - sinon.assert.calledOnceWithExactly(log.error, - '[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config' - ) - }) + // getPayload includes the new extraServices that might be available + const payload = rc.getPayload() + assert.deepStrictEqual(JSON.parse(payload).client.client_tracer.extra_services, extraServices) - it('should update collection mode when called with apply', () => { - listener('apply', rcConfig, configId) + rc.poll(() => { + sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) + cb() + }) + }) + }) - sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, rcConfig.auto_user_instrum.mode) - }) + describe('parseConfig', () => { + let payload + const parsePayload = () => rc.parseConfig(payload) + let previousState - it('should update collection mode when called with modify', () => { - listener('modify', rcConfig, configId) + beforeEach(() => { + sinon.stub(rc, 'dispatch').callsFake((list, action) => { + const items = /** @type {Array<{path: string, apply_state: number}>} */ (list) + for (const item of items) { + item.apply_state = ACKNOWLEDGED - sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, rcConfig.auto_user_instrum.mode) - }) + if (action === 'unapply') rc.appliedConfigs.delete(item.path) + else rc.appliedConfigs.set(item.path, item) + } + }) - it('should revert collection mode when called with unapply', () => { - listener('apply', rcConfig, configId) - UserTracking.setCollectionMode.resetHistory() + previousState = JSON.parse(JSON.stringify(rc.state)) + }) - listener('unapply', rcConfig, configId) + it('should do nothing if passed an empty payload', () => { + payload = {} + parsePayload() + sinon.assert.notCalled(rc.dispatch) + assert.deepStrictEqual(rc.state, previousState) + }) - sinon.assert.calledOnceWithExactly(UserTracking.setCollectionMode, config.appsec.eventTracking.mode) + it('should throw when target is not found', () => { + payload = { + client_configs: ['datadog/42/PRODUCT/confId/config'], + targets: toBase64({ + signed: { + targets: { + 'datadog/42/OTHERPRODUCT/confId/config': {} + } + } }) + } - it('should not revert collection mode when called with unapply and unknown id', () => { - listener('apply', rcConfig, configId) - UserTracking.setCollectionMode.resetHistory() - - listener('unapply', rcConfig, 'unknownId') + assert.throws(parsePayload, { message: 'Unable to find target for path datadog/42/PRODUCT/confId/config' }) + sinon.assert.notCalled(rc.dispatch) + assert.deepStrictEqual(rc.state, previousState) + }) - sinon.assert.notCalled(UserTracking.setCollectionMode) + it('should throw when target file is not found', () => { + payload = { + client_configs: ['datadog/42/PRODUCT/confId/config'], + targets: toBase64({ + signed: { + targets: { + 'datadog/42/PRODUCT/confId/config': { + hashes: { + sha256: 'haaaxx' + } + } + } + } }) - }) + } + + assert.throws(parsePayload, { message: 'Unable to find file for path datadog/42/PRODUCT/confId/config' }) + sinon.assert.notCalled(rc.dispatch) + assert.deepStrictEqual(rc.state, previousState) }) - }) - describe('enableWafUpdate', () => { - const expectCapabilitiesCalledWith = (capabilityList, expectedValue) => { - capabilityList.forEach(capability => { - sinon.assert.calledWithExactly(rc.updateCapabilities, capability, expectedValue) - }) - } + it('should throw when config path cannot be parsed', () => { + payload = { + client_configs: ['datadog/42/confId/config'], + targets: toBase64({ + signed: { + targets: { + 'datadog/42/confId/config': { + hashes: { + sha256: 'haaaxx' + } + } + } + } + }), + target_files: [{ + path: 'datadog/42/confId/config', + raw: toBase64({}) + }] + } - const expectCapabilitiesNotCalled = (capabilityList) => { - capabilityList.forEach(capability => { - sinon.assert.neverCalledWith(rc.updateCapabilities, capability) - }) - } + assert.throws(parsePayload, { message: 'Unable to parse path datadog/42/confId/config' }) + sinon.assert.notCalled(rc.dispatch) + assert.deepStrictEqual(rc.state, previousState) + }) - const CORE_ASM_CAPABILITIES = [ - RemoteConfigCapabilities.ASM_IP_BLOCKING, - RemoteConfigCapabilities.ASM_USER_BLOCKING, - RemoteConfigCapabilities.ASM_DD_RULES, - RemoteConfigCapabilities.ASM_EXCLUSIONS, - RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, - RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, - RemoteConfigCapabilities.ASM_CUSTOM_RULES, - RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, - RemoteConfigCapabilities.ASM_TRUSTED_IPS, - RemoteConfigCapabilities.ASM_PROCESSOR_OVERRIDES, - RemoteConfigCapabilities.ASM_CUSTOM_DATA_SCANNERS, - RemoteConfigCapabilities.ASM_EXCLUSION_DATA, - RemoteConfigCapabilities.ASM_ENDPOINT_FINGERPRINT, - RemoteConfigCapabilities.ASM_SESSION_FINGERPRINT, - RemoteConfigCapabilities.ASM_NETWORK_FINGERPRINT, - RemoteConfigCapabilities.ASM_HEADER_FINGERPRINT, - RemoteConfigCapabilities.ASM_DD_MULTICONFIG, - RemoteConfigCapabilities.ASM_TRACE_TAGGING_RULES - ] - - const RASP_CAPABILITIES = [ - RemoteConfigCapabilities.ASM_RASP_SSRF, - RemoteConfigCapabilities.ASM_RASP_SQLI, - RemoteConfigCapabilities.ASM_RASP_LFI, - RemoteConfigCapabilities.ASM_RASP_SHI, - RemoteConfigCapabilities.ASM_RASP_CMDI - ] - - const ALL_ASM_CAPABILITIES = [...CORE_ASM_CAPABILITIES, ...RASP_CAPABILITIES] - - describe('enable', () => { - it('should not fail if remote config is not enabled before', () => { - config.appsec = {} - remoteConfig.enableWafUpdate(config.appsec) - - sinon.assert.notCalled(rc.updateCapabilities) - sinon.assert.notCalled(rc.setProductHandler) + it('should parse the config, call dispatch, and update the state', () => { + rc.appliedConfigs.set('datadog/42/UNAPPLY/confId/config', { + path: 'datadog/42/UNAPPLY/confId/config', + product: 'UNAPPLY', + id: 'confId', + version: 69, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 147, + hashes: { sha256: 'anotherHash' }, + file: { asm: { enabled: true } } + }) + rc.appliedConfigs.set('datadog/42/IGNORE/confId/config', { + path: 'datadog/42/IGNORE/confId/config', + product: 'IGNORE', + id: 'confId', + version: 43, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 420, + hashes: { sha256: 'sameHash' }, + file: {} + }) + rc.appliedConfigs.set('datadog/42/MODIFY/confId/config', { + path: 'datadog/42/MODIFY/confId/config', + product: 'MODIFY', + id: 'confId', + version: 11, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 147, + hashes: { sha256: 'oldHash' }, + file: { config: 'oldConf' } }) - it('should not enable when custom appsec rules are provided', () => { - config.appsec = { enabled: true, rules: {} } - remoteConfig.enable(config) - remoteConfig.enableWafUpdate(config.appsec) + payload = { + client_configs: [ + 'datadog/42/IGNORE/confId/config', + 'datadog/42/MODIFY/confId/config', + 'datadog/42/APPLY/confId/config' + ], + targets: toBase64({ + signed: { + custom: { + opaque_backend_state: 'opaquestateinbase64' + }, + targets: { + 'datadog/42/IGNORE/confId/config': { + custom: { + v: 43 + }, + hashes: { + sha256: 'sameHash' + }, + length: 420 + }, + 'datadog/42/MODIFY/confId/config': { + custom: { + v: 12 + }, + hashes: { + sha256: 'newHash' + }, + length: 147 + }, + 'datadog/42/APPLY/confId/config': { + custom: { + v: 1 + }, + hashes: { + sha256: 'haaaxx' + }, + length: 0 + } + }, + version: 12345 + } + }), + target_files: [ + { + path: 'datadog/42/MODIFY/confId/config', + raw: toBase64({ config: 'newConf' }) + }, + { + path: 'datadog/42/APPLY/confId/config', + raw: '' + } + ] + } - sinon.assert.neverCalledWith(rc.updateCapabilities, 'ASM_ACTIVATION') - sinon.assert.called(rc.setProductHandler) - }) + // Calling parsePayload should not throw. + parsePayload() + + assert.strictEqual(rc.state.client.state.targets_version, 12345) + assert.strictEqual(rc.state.client.state.backend_client_state, 'opaquestateinbase64') + + sinon.assert.calledThrice(rc.dispatch) + sinon.assert.calledWithMatch(rc.dispatch.firstCall, [{ + path: 'datadog/42/UNAPPLY/confId/config', + product: 'UNAPPLY', + id: 'confId', + version: 69, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 147, + hashes: { sha256: 'anotherHash' }, + file: { asm: { enabled: true } } + }], 'unapply', sinon.match.instanceOf(Set)) + sinon.assert.calledWithMatch(rc.dispatch.secondCall, [{ + path: 'datadog/42/APPLY/confId/config', + product: 'APPLY', + id: 'confId', + version: 1, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 0, + hashes: { sha256: 'haaaxx' }, + file: null + }], 'apply', sinon.match.instanceOf(Set)) + sinon.assert.calledWithMatch(rc.dispatch.thirdCall, [{ + path: 'datadog/42/MODIFY/confId/config', + product: 'MODIFY', + id: 'confId', + version: 12, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 147, + hashes: { sha256: 'newHash' }, + file: { config: 'newConf' } + }], 'modify', sinon.match.instanceOf(Set)) + + assert.deepStrictEqual(rc.state.client.state.config_states, [ + { + id: 'confId', + version: 43, + product: 'IGNORE', + apply_state: ACKNOWLEDGED, + apply_error: '' + }, + { + id: 'confId', + version: 12, + product: 'MODIFY', + apply_state: ACKNOWLEDGED, + apply_error: '' + }, + { + id: 'confId', + version: 1, + product: 'APPLY', + apply_state: ACKNOWLEDGED, + apply_error: '' + } + ]) + assert.deepStrictEqual(rc.state.cached_target_files, [ + { + path: 'datadog/42/IGNORE/confId/config', + length: 420, + hashes: [{ algorithm: 'sha256', hash: 'sameHash' }] + }, + { + path: 'datadog/42/MODIFY/confId/config', + length: 147, + hashes: [{ algorithm: 'sha256', hash: 'newHash' }] + }, + { + path: 'datadog/42/APPLY/confId/config', + length: 0, + hashes: [{ algorithm: 'sha256', hash: 'haaaxx' }] + } + ]) + }) - it('should enable when using default rules', () => { - config.appsec = { enabled: true, rules: null, rasp: { enabled: true } } - remoteConfig.enable(config) - remoteConfig.enableWafUpdate(config.appsec) + it('should allow batch handlers to ack + handle items and skip per-product handlers (including unapply)', () => { + // Arrange: two configs already applied, one will be unapplied. + const unapplyPath = 'datadog/42/ASM/confId/config' + rc.appliedConfigs.set(unapplyPath, { + path: unapplyPath, + product: 'ASM', + id: 'confId', + version: 1, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 1, + hashes: { sha256: 'oldHash' }, + file: { a: 1 } + }) - expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, true) + const handler = sinon.spy() + rc.setProductHandler('ASM', handler) - sinon.assert.calledWith(rc.setProductHandler, 'ASM_DATA') - sinon.assert.calledWith(rc.setProductHandler, 'ASM_DD') - sinon.assert.calledWith(rc.setProductHandler, 'ASM') - sinon.assert.calledWithExactly(rc.on, kPreUpdate, RuleManager.updateWafFromRC) + // Batch hook will handle the unapply and report success. + rc.setBatchHandler(['ASM'], (transaction) => { + for (const item of transaction.toUnapply) { + transaction.ack(item.path) + } }) - it('should activate if appsec is manually enabled', () => { - config.appsec = { enabled: true, rasp: { enabled: true } } - remoteConfig.enable(config) - remoteConfig.enableWafUpdate(config.appsec) + payload = { + client_configs: [], + targets: toBase64({ + signed: { + custom: { opaque_backend_state: 'state' }, + targets: {}, + version: 2 + } + }), + target_files: [] + } - expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, true) + // Act + parsePayload() - sinon.assert.calledWith(rc.setProductHandler, 'ASM_DATA') - sinon.assert.calledWith(rc.setProductHandler, 'ASM_DD') - sinon.assert.calledWith(rc.setProductHandler, 'ASM') - sinon.assert.calledWithExactly(rc.on, kPreUpdate, RuleManager.updateWafFromRC) - }) + // Assert: handler should not be invoked, but state should be updated (unapplied). + sinon.assert.notCalled(handler) + assert.strictEqual(rc.appliedConfigs.has(unapplyPath), false) + }) - it('should activate if appsec enabled is not defined', () => { - config.appsec = { rasp: { enabled: true } } - remoteConfig.enable(config) - remoteConfig.enableWafUpdate(config.appsec) + it('should call per-product handlers when batch handlers do not ack/error (including unapply)', () => { + const unapplyPath = 'datadog/42/ASM/confId/config' + const conf = { + path: unapplyPath, + product: 'ASM', + id: 'confId', + version: 1, + apply_state: ACKNOWLEDGED, + apply_error: '', + length: 1, + hashes: { sha256: 'oldHash' }, + file: { a: 1 } + } + rc.appliedConfigs.set(unapplyPath, conf) + + const handler = sinon.spy() + rc.setProductHandler('ASM', handler) + + // Batch hook does nothing (does not ack/error), so per-product handler should run. + rc.setBatchHandler(['ASM'], () => {}) + + // This test needs the real dispatch path in order to verify handler invocation. + rc.dispatch.restore() + + payload = { + client_configs: [], + targets: toBase64({ + signed: { + custom: { opaque_backend_state: 'state' }, + targets: {}, + version: 2 + } + }), + target_files: [] + } - sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION, true) + parsePayload() - expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, true) - }) + sinon.assert.calledOnceWithExactly(handler, 'unapply', conf.file, conf.id) + assert.strictEqual(rc.appliedConfigs.has(unapplyPath), false) + }) + }) - it('should not activate rasp capabilities if rasp is disabled', () => { - config.appsec = { rasp: { enabled: false } } - remoteConfig.enable(config) - remoteConfig.enableWafUpdate(config.appsec) + describe('dispatch', () => { + it('should call registered handler for each config, catch errors, and update the state', (done) => { + const syncGoodNonAckHandler = sinon.spy() + const syncBadNonAckHandler = sinon.spy((action, conf, id) => { throw new Error('sync fn') }) + const asyncGoodHandler = sinon.spy(async (action, conf, id) => {}) + const asyncBadHandler = sinon.spy(async (action, conf, id) => { throw new Error('async fn') }) + const syncGoodAckHandler = sinon.spy((action, conf, id, ack) => { ack() }) + const syncBadAckHandler = sinon.spy((action, conf, id, ack) => { ack(new Error('sync ack fn')) }) + const asyncGoodAckHandler = sinon.spy((action, conf, id, ack) => { setImmediate(ack) }) + const asyncBadAckHandler = sinon.spy((action, conf, id, ack) => { + setImmediate(ack.bind(null, new Error('async ack fn'))) + }) + const unackHandler = sinon.spy((action, conf, id, ack) => {}) + + rc.setProductHandler('PRODUCT_0', syncGoodNonAckHandler) + rc.setProductHandler('PRODUCT_1', syncBadNonAckHandler) + rc.setProductHandler('PRODUCT_2', asyncGoodHandler) + rc.setProductHandler('PRODUCT_3', asyncBadHandler) + rc.setProductHandler('PRODUCT_4', syncGoodAckHandler) + rc.setProductHandler('PRODUCT_5', syncBadAckHandler) + rc.setProductHandler('PRODUCT_6', asyncGoodAckHandler) + rc.setProductHandler('PRODUCT_7', asyncBadAckHandler) + rc.setProductHandler('PRODUCT_8', unackHandler) + + const list = [] + for (let i = 0; i < 9; i++) { + list[i] = { + id: `id_${i}`, + path: `datadog/42/PRODUCT_${i}/confId/config`, + product: `PRODUCT_${i}`, + apply_state: UNACKNOWLEDGED, + apply_error: '', + file: { index: i } + } + } - sinon.assert.calledWithExactly(rc.updateCapabilities, RemoteConfigCapabilities.ASM_ACTIVATION, true) + rc.dispatch(list, 'apply', new Set()) + + sinon.assert.calledOnceWithExactly(syncGoodNonAckHandler, 'apply', list[0].file, list[0].id) + sinon.assert.calledOnceWithExactly(syncBadNonAckHandler, 'apply', list[1].file, list[1].id) + sinon.assert.calledOnceWithExactly(asyncGoodHandler, 'apply', list[2].file, list[2].id) + sinon.assert.calledOnceWithExactly(asyncBadHandler, 'apply', list[3].file, list[3].id) + assertAsyncHandlerCallArguments(syncGoodAckHandler, 'apply', list[4].file, list[4].id) + assertAsyncHandlerCallArguments(syncBadAckHandler, 'apply', list[5].file, list[5].id) + assertAsyncHandlerCallArguments(asyncGoodAckHandler, 'apply', list[6].file, list[6].id) + assertAsyncHandlerCallArguments(asyncBadAckHandler, 'apply', list[7].file, list[7].id) + assertAsyncHandlerCallArguments(unackHandler, 'apply', list[8].file, list[8].id) + + assert.strictEqual(list[0].apply_state, ACKNOWLEDGED) + assert.strictEqual(list[0].apply_error, '') + assert.strictEqual(list[1].apply_state, ERROR) + assert.strictEqual(list[1].apply_error, 'Error: sync fn') + assert.strictEqual(list[2].apply_state, UNACKNOWLEDGED) + assert.strictEqual(list[2].apply_error, '') + assert.strictEqual(list[3].apply_state, UNACKNOWLEDGED) + assert.strictEqual(list[3].apply_error, '') + assert.strictEqual(list[4].apply_state, ACKNOWLEDGED) + assert.strictEqual(list[4].apply_error, '') + assert.strictEqual(list[5].apply_state, ERROR) + assert.strictEqual(list[5].apply_error, 'Error: sync ack fn') + assert.strictEqual(list[6].apply_state, UNACKNOWLEDGED) + assert.strictEqual(list[6].apply_error, '') + assert.strictEqual(list[7].apply_state, UNACKNOWLEDGED) + assert.strictEqual(list[7].apply_error, '') + assert.strictEqual(list[8].apply_state, UNACKNOWLEDGED) + assert.strictEqual(list[8].apply_error, '') + + for (let i = 0; i < list.length; i++) { + assert.strictEqual(rc.appliedConfigs.get(`datadog/42/PRODUCT_${i}/confId/config`), list[i]) + } - expectCapabilitiesCalledWith(CORE_ASM_CAPABILITIES, true) - expectCapabilitiesNotCalled(RASP_CAPABILITIES) + setImmediate(() => { + assert.strictEqual(list[2].apply_state, ACKNOWLEDGED) + assert.strictEqual(list[2].apply_error, '') + assert.strictEqual(list[3].apply_state, ERROR) + assert.strictEqual(list[3].apply_error, 'Error: async fn') + assert.strictEqual(list[6].apply_state, ACKNOWLEDGED) + assert.strictEqual(list[6].apply_error, '') + assert.strictEqual(list[7].apply_state, ERROR) + assert.strictEqual(list[7].apply_error, 'Error: async ack fn') + assert.strictEqual(list[8].apply_state, UNACKNOWLEDGED) + assert.strictEqual(list[8].apply_error, '') + done() }) + + function assertAsyncHandlerCallArguments (handler, ...expectedArgs) { + sinon.assert.calledOnceWithMatch(handler, ...expectedArgs) + assert.strictEqual(handler.args[0].length, expectedArgs.length + 1) + assert.strictEqual(typeof handler.args[0][handler.args[0].length - 1], 'function') + } }) - describe('disable', () => { - it('should update capabilities and unsubscribe listener', () => { - remoteConfig.enable(config) - rc.updateCapabilities.resetHistory() - remoteConfig.disableWafUpdate() + it('should delete config from state when action is unapply', () => { + const handler = sinon.spy() + rc.setProductHandler('ASM_FEATURES', handler) + + rc.appliedConfigs.set('datadog/42/ASM_FEATURES/confId/config', { + id: 'asm_data', + path: 'datadog/42/ASM_FEATURES/confId/config', + product: 'ASM_FEATURES', + apply_state: ACKNOWLEDGED, + apply_error: '', + file: { asm: { enabled: true } } + }) - expectCapabilitiesCalledWith(ALL_ASM_CAPABILITIES, false) + rc.dispatch([rc.appliedConfigs.get('datadog/42/ASM_FEATURES/confId/config')], 'unapply', new Set()) - sinon.assert.calledWith(rc.removeProductHandler, 'ASM_DATA') - sinon.assert.calledWith(rc.removeProductHandler, 'ASM_DD') - sinon.assert.calledWith(rc.removeProductHandler, 'ASM') - sinon.assert.calledWithExactly(rc.off, kPreUpdate, RuleManager.updateWafFromRC) - }) + sinon.assert.calledOnceWithExactly(handler, 'unapply', { asm: { enabled: true } }, 'asm_data') + assert.strictEqual(rc.appliedConfigs.size, 0) }) }) }) + +function toBase64 (data) { + return Buffer.from(JSON.stringify(data), 'utf8').toString('base64') +} diff --git a/packages/dd-trace/test/remote_config/manager.spec.js b/packages/dd-trace/test/remote_config/manager.spec.js deleted file mode 100644 index f6a87214c74..00000000000 --- a/packages/dd-trace/test/remote_config/manager.spec.js +++ /dev/null @@ -1,705 +0,0 @@ -'use strict' - -const assert = require('node:assert/strict') - -const { expect } = require('chai') -const { describe, it, beforeEach } = require('tap').mocha -const sinon = require('sinon') -const proxyquire = require('proxyquire') - -require('../setup/core') - -const Capabilities = require('../../src/remote_config/capabilities') -const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('../../src/remote_config/apply_states') - -const noop = () => {} - -describe('RemoteConfigManager', () => { - let uuid - let scheduler - let Scheduler - let request - let log - let extraServices - let RemoteConfigManager - let config - let rc - let tagger - - beforeEach(() => { - uuid = sinon.stub().returns('1234-5678') - - scheduler = { - start: sinon.spy(), - stop: sinon.spy() - } - - Scheduler = sinon.stub().returns(scheduler) - - request = sinon.stub() - - log = { - error: sinon.spy() - } - - tagger = { - add: sinon.stub() - } - - extraServices = [] - - RemoteConfigManager = proxyquire('../../src/remote_config/manager', { - '../../../../vendor/dist/crypto-randomuuid': uuid, - './scheduler': Scheduler, - '../../../../package.json': { version: '3.0.0' }, - '../exporters/common/request': request, - '../log': log, - '../tagger': tagger, - '../service-naming/extra-services': { - getExtraServices: () => extraServices - } - }) - - config = { - url: 'http://127.0.0.1:1337', - hostname: '127.0.0.1', - port: '1337', - tags: { - 'runtime-id': 'runtimeId' - }, - service: 'serviceName', - env: 'serviceEnv', - version: 'appVersion', - remoteConfig: { - pollInterval: 5 - } - } - - rc = new RemoteConfigManager(config) - }) - - it('should instantiate RemoteConfigManager', () => { - sinon.stub(rc, 'poll') - - sinon.assert.calledOnce(Scheduler) - const [firstArg, secondArg] = Scheduler.firstCall.args - assert.strictEqual(typeof firstArg, 'function') - assert.strictEqual(secondArg, 5e3) - - firstArg(noop) - expect(rc.poll).to.have.calledOnceWithExactly(noop) - - assert.strictEqual(rc.scheduler, scheduler) - - assert.deepStrictEqual(rc.url, config.url) - - sinon.assert.calledOnceWithExactly(tagger.add, config.tags, { - '_dd.rc.client_id': '1234-5678' - }) - - assert.deepStrictEqual(rc.state, { - client: { - state: { - root_version: 1, - targets_version: 0, - config_states: [], - has_error: false, - error: '', - backend_client_state: '' - }, - id: '1234-5678', - products: [], - is_tracer: true, - client_tracer: { - runtime_id: config.tags['runtime-id'], - language: 'node', - tracer_version: '3.0.0', - service: config.service, - env: config.env, - app_version: config.version, - extra_services: [], - tags: ['runtime-id:runtimeId'] - }, - capabilities: 'AA==' - }, - cached_target_files: [] - }) - - assert.ok(rc.appliedConfigs instanceof Map) - }) - - it('should add git metadata to tags if present', () => { - const configWithGit = { - ...config, - repositoryUrl: 'https://github.com/DataDog/dd-trace-js', - commitSHA: '1234567890' - } - const rc = new RemoteConfigManager(configWithGit) - assert.deepStrictEqual(rc.state.client.client_tracer.tags, [ - 'runtime-id:runtimeId', - 'git.repository_url:https://github.com/DataDog/dd-trace-js', - 'git.commit.sha:1234567890' - ]) - }) - - describe('updateCapabilities', () => { - it('should set multiple capabilities to true', () => { - rc.updateCapabilities(Capabilities.ASM_ACTIVATION, true) - assert.strictEqual(rc.state.client.capabilities, 'Ag==') - - rc.updateCapabilities(Capabilities.ASM_IP_BLOCKING, true) - assert.strictEqual(rc.state.client.capabilities, 'Bg==') - - rc.updateCapabilities(Capabilities.ASM_DD_RULES, true) - assert.strictEqual(rc.state.client.capabilities, 'Dg==') - - rc.updateCapabilities(Capabilities.ASM_USER_BLOCKING, true) - assert.strictEqual(rc.state.client.capabilities, 'jg==') - }) - - it('should set multiple capabilities to false', () => { - rc.state.client.capabilities = 'jg==' - - rc.updateCapabilities(Capabilities.ASM_USER_BLOCKING, false) - assert.strictEqual(rc.state.client.capabilities, 'Dg==') - - rc.updateCapabilities(Capabilities.ASM_ACTIVATION, false) - assert.strictEqual(rc.state.client.capabilities, 'DA==') - - rc.updateCapabilities(Capabilities.ASM_IP_BLOCKING, false) - assert.strictEqual(rc.state.client.capabilities, 'CA==') - - rc.updateCapabilities(Capabilities.ASM_DD_RULES, false) - assert.strictEqual(rc.state.client.capabilities, 'AA==') - }) - - it('should set an arbitrary amount of capabilities', () => { - rc.updateCapabilities(1n << 1n, true) - rc.updateCapabilities(1n << 200n, true) - assert.strictEqual(rc.state.client.capabilities, 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=') - - rc.updateCapabilities(1n << 200n, false) - assert.strictEqual(rc.state.client.capabilities, 'Ag==') - }) - }) - - describe('setProductHandler/removeProductHandler', () => { - it('should update the product list and autostart or autostop', () => { - sinon.assert.notCalled(rc.scheduler.start) - - rc.setProductHandler('ASM_FEATURES', noop) - - assert.deepStrictEqual(rc.state.client.products, ['ASM_FEATURES']) - sinon.assert.called(rc.scheduler.start) - - rc.setProductHandler('ASM_DATA', noop) - rc.setProductHandler('ASM_DD', noop) - - assert.deepStrictEqual(rc.state.client.products, ['ASM_FEATURES', 'ASM_DATA', 'ASM_DD']) - - rc.removeProductHandler('ASM_FEATURES') - - assert.deepStrictEqual(rc.state.client.products, ['ASM_DATA', 'ASM_DD']) - - rc.removeProductHandler('ASM_DATA') - - sinon.assert.notCalled(rc.scheduler.stop) - - rc.removeProductHandler('ASM_DD') - - sinon.assert.called(rc.scheduler.stop) - assert.strictEqual(rc.state.client.products.length, 0) - }) - }) - - describe('poll', () => { - let expectedPayload - - beforeEach(() => { - sinon.stub(rc, 'parseConfig') - expectedPayload = { - url: rc.url, - method: 'POST', - path: '/v0.7/config', - headers: { 'Content-Type': 'application/json; charset=utf-8' } - } - }) - - it('should request and do nothing when received status 404', (cb) => { - request.yieldsRight(new Error('Response received 404'), '{"a":"b"}', 404) - - const payload = JSON.stringify(rc.state) - - rc.poll(() => { - sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) - sinon.assert.notCalled(log.error) - sinon.assert.notCalled(rc.parseConfig) - cb() - }) - }) - - it('should request when received error', (cb) => { - const err = new Error('Response received 500') - request.yieldsRight(err, '{"a":"b"}', 500) - - const payload = JSON.stringify(rc.state) - - rc.poll(() => { - sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) - sinon.assert.notCalled(rc.parseConfig) - cb() - }) - }) - - it('should request and call parseConfig when payload is not empty', (cb) => { - request.yieldsRight(null, '{"a":"b"}', 200) - - const payload = JSON.stringify(rc.state) - - rc.poll(() => { - sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) - sinon.assert.notCalled(log.error) - sinon.assert.calledOnceWithExactly(rc.parseConfig, { a: 'b' }) - cb() - }) - }) - - it('should catch exceptions, update the error state, and clear the error state at next request', (cb) => { - const error = new Error('Unable to parse config') - request - .onFirstCall().yieldsRight(null, '{"a":"b"}', 200) - .onSecondCall().yieldsRight(null, null, 200) - rc.parseConfig.onFirstCall().throws(error) - - const payload = JSON.stringify(rc.state) - - rc.poll(() => { - sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) - sinon.assert.calledOnceWithExactly(rc.parseConfig, { a: 'b' }) - expect(log.error).to.have.been - .calledOnceWithExactly('[RC] Could not parse remote config response', error) - assert.strictEqual(rc.state.client.state.has_error, true) - assert.strictEqual(rc.state.client.state.error, 'Error: Unable to parse config') - - const payload2 = JSON.stringify(rc.state) - - rc.poll(() => { - sinon.assert.calledTwice(request) - sinon.assert.calledWith(request.secondCall, payload2, expectedPayload) - sinon.assert.calledOnce(rc.parseConfig) - sinon.assert.calledOnce(log.error) - assert.strictEqual(rc.state.client.state.has_error, false) - assert.strictEqual(rc.state.client.state.error.length, 0) - cb() - }) - }) - }) - - it('should request and do nothing when payload is empty JSON object', (cb) => { - request.yieldsRight(null, '{}', 200) - - const payload = JSON.stringify(rc.state) - - rc.poll(() => { - sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) - sinon.assert.notCalled(log.error) - sinon.assert.notCalled(rc.parseConfig) - cb() - }) - }) - - it('should include extra_services in the payload', (cb) => { - request.yieldsRight(null, '{}', 200) - - extraServices = ['test-service'] - - // getPayload includes the new extraServices that might be available - const payload = rc.getPayload() - assert.deepStrictEqual(JSON.parse(payload).client.client_tracer.extra_services, extraServices) - - rc.poll(() => { - sinon.assert.calledOnceWithMatch(request, payload, expectedPayload) - cb() - }) - }) - }) - - describe('parseConfig', () => { - let payload - const func = () => rc.parseConfig(payload) - let previousState - - beforeEach(() => { - sinon.stub(rc, 'dispatch').callsFake((list, action) => { - for (const item of list) { - item.apply_state = ACKNOWLEDGED - - if (action === 'unapply') rc.appliedConfigs.delete(item.path) - else rc.appliedConfigs.set(item.path, item) - } - }) - - previousState = JSON.parse(JSON.stringify(rc.state)) - }) - - it('should do nothing if passed an empty payload', () => { - payload = {} - - assert.doesNotThrow(func) - sinon.assert.notCalled(rc.dispatch) - assert.deepStrictEqual(rc.state, previousState) - }) - - it('should throw when target is not found', () => { - payload = { - client_configs: ['datadog/42/PRODUCT/confId/config'], - targets: toBase64({ - signed: { - targets: { - 'datadog/42/OTHERPRODUCT/confId/config': {} - } - } - }) - } - - assert.throws(func, { message: 'Unable to find target for path datadog/42/PRODUCT/confId/config' }) - sinon.assert.notCalled(rc.dispatch) - assert.deepStrictEqual(rc.state, previousState) - }) - - it('should throw when target file is not found', () => { - payload = { - client_configs: ['datadog/42/PRODUCT/confId/config'], - targets: toBase64({ - signed: { - targets: { - 'datadog/42/PRODUCT/confId/config': { - hashes: { - sha256: 'haaaxx' - } - } - } - } - }) - } - - assert.throws(func, { message: 'Unable to find file for path datadog/42/PRODUCT/confId/config' }) - sinon.assert.notCalled(rc.dispatch) - assert.deepStrictEqual(rc.state, previousState) - }) - - it('should throw when config path cannot be parsed', () => { - payload = { - client_configs: ['datadog/42/confId/config'], - targets: toBase64({ - signed: { - targets: { - 'datadog/42/confId/config': { - hashes: { - sha256: 'haaaxx' - } - } - } - } - }), - target_files: [{ - path: 'datadog/42/confId/config', - raw: toBase64({}) - }] - } - - assert.throws(func, { message: 'Unable to parse path datadog/42/confId/config' }) - sinon.assert.notCalled(rc.dispatch) - assert.deepStrictEqual(rc.state, previousState) - }) - - it('should parse the config, call dispatch, and update the state', () => { - rc.appliedConfigs.set('datadog/42/UNAPPLY/confId/config', { - path: 'datadog/42/UNAPPLY/confId/config', - product: 'UNAPPLY', - id: 'confId', - version: 69, - apply_state: ACKNOWLEDGED, - apply_error: '', - length: 147, - hashes: { sha256: 'anotherHash' }, - file: { asm: { enabled: true } } - }) - rc.appliedConfigs.set('datadog/42/IGNORE/confId/config', { - path: 'datadog/42/IGNORE/confId/config', - product: 'IGNORE', - id: 'confId', - version: 43, - apply_state: ACKNOWLEDGED, - apply_error: '', - length: 420, - hashes: { sha256: 'sameHash' }, - file: {} - }) - rc.appliedConfigs.set('datadog/42/MODIFY/confId/config', { - path: 'datadog/42/MODIFY/confId/config', - product: 'MODIFY', - id: 'confId', - version: 11, - apply_state: ACKNOWLEDGED, - apply_error: '', - length: 147, - hashes: { sha256: 'oldHash' }, - file: { config: 'oldConf' } - }) - - payload = { - client_configs: [ - 'datadog/42/IGNORE/confId/config', - 'datadog/42/MODIFY/confId/config', - 'datadog/42/APPLY/confId/config' - ], - targets: toBase64({ - signed: { - custom: { - opaque_backend_state: 'opaquestateinbase64' - }, - targets: { - 'datadog/42/IGNORE/confId/config': { - custom: { - v: 43 - }, - hashes: { - sha256: 'sameHash' - }, - length: 420 - }, - 'datadog/42/MODIFY/confId/config': { - custom: { - v: 12 - }, - hashes: { - sha256: 'newHash' - }, - length: 147 - }, - 'datadog/42/APPLY/confId/config': { - custom: { - v: 1 - }, - hashes: { - sha256: 'haaaxx' - }, - length: 0 - } - }, - version: 12345 - } - }), - target_files: [ - { - path: 'datadog/42/MODIFY/confId/config', - raw: toBase64({ config: 'newConf' }) - }, - { - path: 'datadog/42/APPLY/confId/config', - raw: '' - } - ] - } - - // Calling func should not throw. - func() - - assert.strictEqual(rc.state.client.state.targets_version, 12345) - assert.strictEqual(rc.state.client.state.backend_client_state, 'opaquestateinbase64') - - sinon.assert.calledThrice(rc.dispatch) - sinon.assert.calledWithExactly(rc.dispatch.firstCall, [{ - path: 'datadog/42/UNAPPLY/confId/config', - product: 'UNAPPLY', - id: 'confId', - version: 69, - apply_state: ACKNOWLEDGED, - apply_error: '', - length: 147, - hashes: { sha256: 'anotherHash' }, - file: { asm: { enabled: true } } - }], 'unapply') - sinon.assert.calledWithExactly(rc.dispatch.secondCall, [{ - path: 'datadog/42/APPLY/confId/config', - product: 'APPLY', - id: 'confId', - version: 1, - apply_state: ACKNOWLEDGED, - apply_error: '', - length: 0, - hashes: { sha256: 'haaaxx' }, - file: null - }], 'apply') - sinon.assert.calledWithExactly(rc.dispatch.thirdCall, [{ - path: 'datadog/42/MODIFY/confId/config', - product: 'MODIFY', - id: 'confId', - version: 12, - apply_state: ACKNOWLEDGED, - apply_error: '', - length: 147, - hashes: { sha256: 'newHash' }, - file: { config: 'newConf' } - }], 'modify') - - assert.deepStrictEqual(rc.state.client.state.config_states, [ - { - id: 'confId', - version: 43, - product: 'IGNORE', - apply_state: ACKNOWLEDGED, - apply_error: '' - }, - { - id: 'confId', - version: 12, - product: 'MODIFY', - apply_state: ACKNOWLEDGED, - apply_error: '' - }, - { - id: 'confId', - version: 1, - product: 'APPLY', - apply_state: ACKNOWLEDGED, - apply_error: '' - } - ]) - assert.deepStrictEqual(rc.state.cached_target_files, [ - { - path: 'datadog/42/IGNORE/confId/config', - length: 420, - hashes: [{ algorithm: 'sha256', hash: 'sameHash' }] - }, - { - path: 'datadog/42/MODIFY/confId/config', - length: 147, - hashes: [{ algorithm: 'sha256', hash: 'newHash' }] - }, - { - path: 'datadog/42/APPLY/confId/config', - length: 0, - hashes: [{ algorithm: 'sha256', hash: 'haaaxx' }] - } - ]) - }) - }) - - describe('dispatch', () => { - it('should call registered handler for each config, catch errors, and update the state', (done) => { - const syncGoodNonAckHandler = sinon.spy() - const syncBadNonAckHandler = sinon.spy(() => { throw new Error('sync fn') }) - const asyncGoodHandler = sinon.spy(async () => {}) - const asyncBadHandler = sinon.spy(async () => { throw new Error('async fn') }) - const syncGoodAckHandler = sinon.spy((action, conf, id, ack) => { ack() }) - const syncBadAckHandler = sinon.spy((action, conf, id, ack) => { ack(new Error('sync ack fn')) }) - const asyncGoodAckHandler = sinon.spy((action, conf, id, ack) => { setImmediate(ack) }) - const asyncBadAckHandler = sinon.spy((action, conf, id, ack) => { - setImmediate(ack.bind(null, new Error('async ack fn'))) - }) - const unackHandler = sinon.spy((action, conf, id, ack) => {}) - - rc.setProductHandler('PRODUCT_0', syncGoodNonAckHandler) - rc.setProductHandler('PRODUCT_1', syncBadNonAckHandler) - rc.setProductHandler('PRODUCT_2', asyncGoodHandler) - rc.setProductHandler('PRODUCT_3', asyncBadHandler) - rc.setProductHandler('PRODUCT_4', syncGoodAckHandler) - rc.setProductHandler('PRODUCT_5', syncBadAckHandler) - rc.setProductHandler('PRODUCT_6', asyncGoodAckHandler) - rc.setProductHandler('PRODUCT_7', asyncBadAckHandler) - rc.setProductHandler('PRODUCT_8', unackHandler) - - const list = [] - for (let i = 0; i < 9; i++) { - list[i] = { - id: `id_${i}`, - path: `datadog/42/PRODUCT_${i}/confId/config`, - product: `PRODUCT_${i}`, - apply_state: UNACKNOWLEDGED, - apply_error: '', - file: { index: i } - } - } - - rc.dispatch(list, 'apply') - - sinon.assert.calledOnceWithExactly(syncGoodNonAckHandler, 'apply', list[0].file, list[0].id) - sinon.assert.calledOnceWithExactly(syncBadNonAckHandler, 'apply', list[1].file, list[1].id) - sinon.assert.calledOnceWithExactly(asyncGoodHandler, 'apply', list[2].file, list[2].id) - sinon.assert.calledOnceWithExactly(asyncBadHandler, 'apply', list[3].file, list[3].id) - assertAsyncHandlerCallArguments(syncGoodAckHandler, 'apply', list[4].file, list[4].id) - assertAsyncHandlerCallArguments(syncBadAckHandler, 'apply', list[5].file, list[5].id) - assertAsyncHandlerCallArguments(asyncGoodAckHandler, 'apply', list[6].file, list[6].id) - assertAsyncHandlerCallArguments(asyncBadAckHandler, 'apply', list[7].file, list[7].id) - assertAsyncHandlerCallArguments(unackHandler, 'apply', list[8].file, list[8].id) - - assert.strictEqual(list[0].apply_state, ACKNOWLEDGED) - assert.strictEqual(list[0].apply_error, '') - assert.strictEqual(list[1].apply_state, ERROR) - assert.strictEqual(list[1].apply_error, 'Error: sync fn') - assert.strictEqual(list[2].apply_state, UNACKNOWLEDGED) - assert.strictEqual(list[2].apply_error, '') - assert.strictEqual(list[3].apply_state, UNACKNOWLEDGED) - assert.strictEqual(list[3].apply_error, '') - assert.strictEqual(list[4].apply_state, ACKNOWLEDGED) - assert.strictEqual(list[4].apply_error, '') - assert.strictEqual(list[5].apply_state, ERROR) - assert.strictEqual(list[5].apply_error, 'Error: sync ack fn') - assert.strictEqual(list[6].apply_state, UNACKNOWLEDGED) - assert.strictEqual(list[6].apply_error, '') - assert.strictEqual(list[7].apply_state, UNACKNOWLEDGED) - assert.strictEqual(list[7].apply_error, '') - assert.strictEqual(list[8].apply_state, UNACKNOWLEDGED) - assert.strictEqual(list[8].apply_error, '') - - for (let i = 0; i < list.length; i++) { - assert.strictEqual(rc.appliedConfigs.get(`datadog/42/PRODUCT_${i}/confId/config`), list[i]) - } - - setImmediate(() => { - assert.strictEqual(list[2].apply_state, ACKNOWLEDGED) - assert.strictEqual(list[2].apply_error, '') - assert.strictEqual(list[3].apply_state, ERROR) - assert.strictEqual(list[3].apply_error, 'Error: async fn') - assert.strictEqual(list[6].apply_state, ACKNOWLEDGED) - assert.strictEqual(list[6].apply_error, '') - assert.strictEqual(list[7].apply_state, ERROR) - assert.strictEqual(list[7].apply_error, 'Error: async ack fn') - assert.strictEqual(list[8].apply_state, UNACKNOWLEDGED) - assert.strictEqual(list[8].apply_error, '') - done() - }) - - function assertAsyncHandlerCallArguments (handler, ...expectedArgs) { - sinon.assert.calledOnceWithMatch(handler, ...expectedArgs) - assert.strictEqual(handler.args[0].length, expectedArgs.length + 1) - assert.strictEqual(typeof handler.args[0][handler.args[0].length - 1], 'function') - } - }) - - it('should delete config from state when action is unapply', () => { - const handler = sinon.spy() - rc.setProductHandler('ASM_FEATURES', handler) - - rc.appliedConfigs.set('datadog/42/ASM_FEATURES/confId/config', { - id: 'asm_data', - path: 'datadog/42/ASM_FEATURES/confId/config', - product: 'ASM_FEATURES', - apply_state: ACKNOWLEDGED, - apply_error: '', - file: { asm: { enabled: true } } - }) - - rc.dispatch([rc.appliedConfigs.get('datadog/42/ASM_FEATURES/confId/config')], 'unapply') - - sinon.assert.calledOnceWithExactly(handler, 'unapply', { asm: { enabled: true } }, 'asm_data') - assert.strictEqual(rc.appliedConfigs.size, 0) - }) - }) -}) - -function toBase64 (data) { - return Buffer.from(JSON.stringify(data), 'utf8').toString('base64') -} diff --git a/packages/dd-trace/test/remote_config/scheduler.spec.js b/packages/dd-trace/test/remote_config/scheduler.spec.js index ba683233417..5787763f42f 100644 --- a/packages/dd-trace/test/remote_config/scheduler.spec.js +++ b/packages/dd-trace/test/remote_config/scheduler.spec.js @@ -1,10 +1,9 @@ 'use strict' -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('../setup/core') - const Scheduler = require('../../src/remote_config/scheduler') const INTERVAL = 5e3 diff --git a/packages/dd-trace/test/require-package-json.spec.js b/packages/dd-trace/test/require-package-json.spec.js index 4505341e12f..569c0a83556 100644 --- a/packages/dd-trace/test/require-package-json.spec.js +++ b/packages/dd-trace/test/require-package-json.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('./setup/core') +const { describe, it } = require('mocha') +require('./setup/core') const requirePackageJson = require('../src/require-package-json') const packageJson = require('../../../package.json') diff --git a/packages/dd-trace/test/ritm-tests/relative/module-c.js b/packages/dd-trace/test/ritm-tests/relative/module-c.js new file mode 100644 index 00000000000..dece2626b19 --- /dev/null +++ b/packages/dd-trace/test/ritm-tests/relative/module-c.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports.foo = 'foo' diff --git a/packages/dd-trace/test/ritm.spec.js b/packages/dd-trace/test/ritm.spec.js index 1fb642dc46c..2efa388b095 100644 --- a/packages/dd-trace/test/ritm.spec.js +++ b/packages/dd-trace/test/ritm.spec.js @@ -1,19 +1,18 @@ 'use strict' -const sinon = require('sinon') -const dc = require('dc-polyfill') -const { describe, it, before, beforeEach, afterEach } = require('tap').mocha - const assert = require('node:assert') const Module = require('node:module') -require('./setup/core') +const sinon = require('sinon') +const dc = require('dc-polyfill') +const { describe, it, before, beforeEach, afterEach } = require('mocha') +require('./setup/core') const Hook = require('../src/ritm') describe('Ritm', () => { let moduleLoadStartChannel, moduleLoadEndChannel, startListener, endListener - let utilHook, aHook, bHook, httpHook + let utilHook, aHook, bHook, httpHook, relativeHook before(() => { moduleLoadStartChannel = dc.channel('dd-trace:moduleLoadStart') @@ -47,6 +46,10 @@ describe('Ritm', () => { exports.foo = 1 return exports }) + relativeHook = new Hook(['./ritm-tests/relative/module-c'], function onRequire (exports) { + exports.foo = 1 + return exports + }) }) afterEach(() => { @@ -54,6 +57,7 @@ describe('Ritm', () => { aHook.unhook() bHook.unhook() httpHook.unhook() + relativeHook.unhook() }) it('should shim util', () => { @@ -101,4 +105,10 @@ describe('Ritm', () => { 'a failing `require(...)` can still throw as expected' ) }) + + it('should hook into registered relative path requires', () => { + assert.equal(require('./ritm-tests/relative/module-c').foo, 1) + assert.equal(startListener.callCount, 1) + assert.equal(endListener.callCount, 1) + }) }) diff --git a/packages/dd-trace/test/runtime_metrics.spec.js b/packages/dd-trace/test/runtime_metrics.spec.js index 21a87de6496..d3cb6592857 100644 --- a/packages/dd-trace/test/runtime_metrics.spec.js +++ b/packages/dd-trace/test/runtime_metrics.spec.js @@ -1,21 +1,18 @@ 'use strict' -const { describe, it, beforeEach, afterEach } = require('tap').mocha const assert = require('node:assert') const os = require('node:os') +const { performance } = require('node:perf_hooks') +const { setImmediate, setTimeout } = require('node:timers/promises') +const util = require('node:util') +const { describe, it, beforeEach, afterEach } = require('mocha') const proxyquire = require('proxyquire') const sinon = require('sinon') -const performance = require('node:perf_hooks').performance -const { setImmediate, setTimeout } = require('node:timers/promises') -const util = require('node:util') require('./setup/core') - const { DogStatsDClient } = require('../src/dogstatsd') -const isWindows = os.platform() === 'win32' - function createGarbage (count = 50) { let last = {} const obj = last @@ -134,7 +131,7 @@ function createGarbage (count = 50) { sinon.assert.notCalled(runtimeMetrics.decrement) sinon.assert.calledOnce(runtimeMetrics.stop) }) - }, { skip: isWindows && !nativeMetrics }) + }) describe('runtimeMetrics', () => { let runtimeMetrics @@ -182,6 +179,13 @@ function createGarbage (count = 50) { throw new Error('Native metrics are not supported in this environment') }, } + } else { + // The log is called in case native metrics fail to load. + proxiedObject['../log'] = { + error () { + throw new Error('Native metrics should load properly') + }, + } } runtimeMetrics = proxyquire('../src/runtime_metrics/runtime_metrics', proxiedObject) @@ -273,6 +277,9 @@ function createGarbage (count = 50) { global.gc() + await setImmediate() + await setImmediate() + clock.tick(10000 - waitTime) const isFiniteNumber = sinon.match((value) => { @@ -784,6 +791,6 @@ function createGarbage (count = 50) { }) }) }) - }, { skip: isWindows && !nativeMetrics }) + }) }) }) diff --git a/packages/dd-trace/test/sampler.spec.js b/packages/dd-trace/test/sampler.spec.js index 4bf6425012a..c6b598dfa65 100644 --- a/packages/dd-trace/test/sampler.spec.js +++ b/packages/dd-trace/test/sampler.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('./setup/core') - const SpanContext = require('../src/opentracing/span_context') const id = require('../src/id') diff --git a/packages/dd-trace/test/sampling_rule.spec.js b/packages/dd-trace/test/sampling_rule.spec.js index f5a977646c4..cb04eab5a01 100644 --- a/packages/dd-trace/test/sampling_rule.spec.js +++ b/packages/dd-trace/test/sampling_rule.spec.js @@ -1,11 +1,11 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('./setup/core') - const id = require('../src/id') const SpanContext = require('../src/opentracing/span_context') diff --git a/packages/dd-trace/test/scope.spec.js b/packages/dd-trace/test/scope.spec.js index 3d33d51849a..b1c81eedb31 100644 --- a/packages/dd-trace/test/scope.spec.js +++ b/packages/dd-trace/test/scope.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') -const { Span } = require('../../../vendor/dist/opentracing') +const { Span } = require('../../../vendor/dist/opentracing') require('./setup/core') - const Scope = require('../src/scope') describe('Scope', () => { diff --git a/packages/dd-trace/test/service-naming/extra-services.spec.js b/packages/dd-trace/test/service-naming/extra-services.spec.js index 30a648fe63a..f746270ec85 100644 --- a/packages/dd-trace/test/service-naming/extra-services.spec.js +++ b/packages/dd-trace/test/service-naming/extra-services.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../setup/core') const { registerExtraService, getExtraServices, clear } = require('../../src/service-naming/extra-services') describe('Extra services', () => { diff --git a/packages/dd-trace/test/service-naming/schema.spec.js b/packages/dd-trace/test/service-naming/schema.spec.js index 23f06a7a15a..e47dec62480 100644 --- a/packages/dd-trace/test/service-naming/schema.spec.js +++ b/packages/dd-trace/test/service-naming/schema.spec.js @@ -2,12 +2,10 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('../setup/core') - const SchemaDefinition = require('../../src/service-naming/schemas/definition') describe('Service naming', () => { @@ -81,7 +79,7 @@ describe('Service naming', () => { describe('Operation name getter', () => { it('should passthrough operation name arguments', () => { resolver.getOpName('messaging', 'inbound', 'kafka', extra) - expect(dummySchema.messaging.inbound.kafka.opName).to.be.calledWith(extra) + sinon.assert.calledWith(dummySchema.messaging.inbound.kafka.opName, extra) }) }) @@ -89,7 +87,7 @@ describe('Service naming', () => { it('should add service name and passthrough service name arguments', () => { const opts = { tracerService: 'test-service', ...extra } resolver.getServiceName('messaging', 'inbound', 'kafka', opts) - expect(dummySchema.messaging.inbound.kafka.serviceName).to.be.calledWith(opts) + sinon.assert.calledWith(dummySchema.messaging.inbound.kafka.serviceName, opts) }) }) }) diff --git a/packages/dd-trace/test/setup/core.js b/packages/dd-trace/test/setup/core.js index 2d8381bab14..71033f8b1a9 100644 --- a/packages/dd-trace/test/setup/core.js +++ b/packages/dd-trace/test/setup/core.js @@ -1,11 +1,5 @@ 'use strict' -const chai = require('chai') -const sinonChai = require('sinon-chai') - -chai.use(sinonChai) -chai.use(require('../asserts/profile')) - process.env.DD_INSTRUMENTATION_TELEMETRY_ENABLED = 'false' // If this is a release PR, set the SSI variables. @@ -13,3 +7,7 @@ if (/^v\d+\.x$/.test(process.env.GITHUB_BASE_REF || '')) { process.env.DD_INJECTION_ENABLED = 'true' process.env.DD_INJECT_FORCE = 'true' } + +// Make this file a module for type-aware tooling. It is intentionally imported +// for side effects only. +module.exports = {} diff --git a/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js b/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js new file mode 100644 index 00000000000..e8f2c405c86 --- /dev/null +++ b/packages/dd-trace/test/setup/helpers/plugin-test-helpers/index.js @@ -0,0 +1,37 @@ +'use strict' + +const { describe, before, after } = require('mocha') +const agent = require('../../../plugins/agent') +const { withVersions } = require('../../mocha') + +function createIntegrationTestSuite (pluginName, packageName, options, testCallback) { + describe('Plugin', () => { + describe(pluginName, () => { + withVersions(pluginName, packageName, version => { + const meta = { agent, tracer: null, mod: null } + + describe('without configuration', () => { + before(async () => { + const plugins = [pluginName, ...(options.additionalPlugins || [])] + const pluginConfigs = [options.pluginConfig || {}, ...(options.additionalPlugins || []).map(() => ({}))] + await agent.load(plugins, pluginConfigs) + }) + + after(async () => { + await agent.close({ ritmReset: false }) + }) + + before(async () => { + meta.tracer = require('../../../../../dd-trace').init() + const mod = require(`../../../../../../versions/${packageName}@${version}`) + meta.mod = options.subModule ? mod.get(options.subModule) : mod.get() + }) + + testCallback(meta) + }) + }) + }) + }) +} + +module.exports = { createIntegrationTestSuite } diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index 5de55bc326a..1d81a97f121 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -15,6 +15,7 @@ const externals = require('../plugins/externals.json') const runtimeMetrics = require('../../src/runtime_metrics') const agent = require('../plugins/agent') const Nomenclature = require('../../src/service-naming') +const extraServices = require('../../src/service-naming/extra-services') const { storage } = require('../../../datadog-core') const { getInstrumentation } = require('./helpers/load-inst') @@ -147,7 +148,7 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service it('should compute peer service', async () => { const useCallback = spanGenerationFn.length === 1 const spanGenerationPromise = useCallback - ? new Promise((resolve, reject) => { + ? new Promise(/** @type {() => void} */ (resolve, reject) => { const result = spanGenerationFn((err) => err ? reject(err) : resolve()) // Some callback based methods are a mixture of callback and promise, // depending on the module version. Await the promises as well. @@ -237,9 +238,8 @@ function withVersions (plugin, modules, range, cb) { for (const version of versions) { if (process.env.RANGE && !semver.subset(version, process.env.RANGE)) continue if (version !== '*') { - const result = semver.coerce(version) - if (!result) throw new Error(`Invalid version: ${version}`) - const min = result.version + const min = semver.coerce(version)?.version + if (!min) throw new Error(`Invalid version: ${version}`) testVersions.set(min, { versionRange: version, versionKey: min, resolvedVersion: min }) } @@ -360,5 +360,7 @@ exports.mochaHooks = { agent.reset() runtimeMetrics.stop() storage('legacy').enterWith(undefined) + storage('baggage').enterWith(undefined) + extraServices.clear() } } diff --git a/packages/dd-trace/test/span_format.spec.js b/packages/dd-trace/test/span_format.spec.js index b2badbfee9a..3525eb452d2 100644 --- a/packages/dd-trace/test/span_format.spec.js +++ b/packages/dd-trace/test/span_format.spec.js @@ -2,14 +2,11 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') - -const { assertObjectContains } = require('../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') +const { assertObjectContains } = require('../../../integration-tests/helpers') require('./setup/core') - const constants = require('../src/constants') const tags = require('../../../ext/tags') const id = require('../src/id') @@ -311,13 +308,8 @@ describe('spanFormat', () => { } trace = spanFormat(span, false) - expect(trace.meta).to.not.include({ - chunk: 'test' - }) - - expect(trace.metrics).to.not.include({ - count: 1 - }) + assert.ok(!('chunk' in trace.meta)) + assert.ok(!('count' in trace.metrics)) }) it('should extract empty tags', () => { diff --git a/packages/dd-trace/test/span_processor.spec.js b/packages/dd-trace/test/span_processor.spec.js index 994daea461a..bc84909790c 100644 --- a/packages/dd-trace/test/span_processor.spec.js +++ b/packages/dd-trace/test/span_processor.spec.js @@ -1,7 +1,8 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') diff --git a/packages/dd-trace/test/span_sampler.spec.js b/packages/dd-trace/test/span_sampler.spec.js index 95051035d4e..975b56c17aa 100644 --- a/packages/dd-trace/test/span_sampler.spec.js +++ b/packages/dd-trace/test/span_sampler.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('./setup/core') - const id = require('../src/id') describe('span sampler', () => { diff --git a/packages/dd-trace/test/span_stats.spec.js b/packages/dd-trace/test/span_stats.spec.js index 378aca0fb33..dcef61b9001 100644 --- a/packages/dd-trace/test/span_stats.spec.js +++ b/packages/dd-trace/test/span_stats.spec.js @@ -1,20 +1,18 @@ 'use strict' const assert = require('node:assert/strict') +const { hostname } = require('os') -const { describe, it } = require('tap').mocha +const { describe, it } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('./setup/core') - -const { hostname } = require('os') - const { LogCollapsingLowestDenseDDSketch } = require('../../../vendor/dist/@datadog/sketches-js') - const { version } = require('../src/pkg') const pkg = require('../../../package.json') const { ORIGIN_KEY, TOP_LEVEL_KEY } = require('../src/constants') + const { MEASURED, HTTP_STATUS_CODE, @@ -26,6 +24,7 @@ const { DEFAULT_SPAN_NAME, DEFAULT_SERVICE_NAME } = require('../src/encode/tags-processors') +const processTags = require('../src/process-tags') // Mock spans const basicSpan = { @@ -390,7 +389,8 @@ describe('SpanStatsProcessor', () => { Lang: 'javascript', TracerVersion: pkg.version, RuntimeID: processor.tags['runtime-id'], - Sequence: processor.sequence + Sequence: processor.sequence, + ProcessTags: processTags.serialized })) }) @@ -408,7 +408,8 @@ describe('SpanStatsProcessor', () => { Lang: 'javascript', TracerVersion: pkg.version, RuntimeID: processor.tags['runtime-id'], - Sequence: processor.sequence + Sequence: processor.sequence, + ProcessTags: processTags.serialized })) }) }) diff --git a/packages/dd-trace/test/standalone/index.spec.js b/packages/dd-trace/test/standalone/index.spec.js index b4d610828a4..c255edf93b0 100644 --- a/packages/dd-trace/test/standalone/index.spec.js +++ b/packages/dd-trace/test/standalone/index.spec.js @@ -1,15 +1,16 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const { channel } = require('dc-polyfill') require('../setup/core') - const standalone = require('../../src/standalone') const DatadogSpan = require('../../src/opentracing/span') + const { APM_TRACING_ENABLED_KEY, SAMPLING_MECHANISM_APPSEC, diff --git a/packages/dd-trace/test/standalone/product.spec.js b/packages/dd-trace/test/standalone/product.spec.js index 1d8132d3292..37c8aa6f992 100644 --- a/packages/dd-trace/test/standalone/product.spec.js +++ b/packages/dd-trace/test/standalone/product.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach, afterEach } = require('tap').mocha + +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../setup/core') - const RateLimiter = require('../../src/rate_limiter') describe('Disabled APM Tracing or Standalone - Product', () => { diff --git a/packages/dd-trace/test/standalone/tracesource.spec.js b/packages/dd-trace/test/standalone/tracesource.spec.js index 35228a9af66..606e4ad7590 100644 --- a/packages/dd-trace/test/standalone/tracesource.spec.js +++ b/packages/dd-trace/test/standalone/tracesource.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha -require('../setup/core') +const { describe, it, beforeEach } = require('mocha') +require('../setup/core') const { ASM } = require('../../src/standalone/product') const { TRACE_SOURCE_PROPAGATION_KEY } = require('../../src/constants') const { addTraceSourceTag } = require('../../src/standalone/tracesource') diff --git a/packages/dd-trace/test/standalone/tracesource_priority_sampler.spec.js b/packages/dd-trace/test/standalone/tracesource_priority_sampler.spec.js index 4e56cdad06f..289e47317ae 100644 --- a/packages/dd-trace/test/standalone/tracesource_priority_sampler.spec.js +++ b/packages/dd-trace/test/standalone/tracesource_priority_sampler.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') require('../setup/core') - const { USER_KEEP, AUTO_KEEP } = require('../../../../ext/priority') const DatadogSpan = require('../../src/opentracing/span') const TraceSourcePrioritySampler = require('../../src/standalone/tracesource_priority_sampler') diff --git a/packages/dd-trace/test/startup-log.spec.js b/packages/dd-trace/test/startup-log.spec.js index e299208a683..8830614137f 100644 --- a/packages/dd-trace/test/startup-log.spec.js +++ b/packages/dd-trace/test/startup-log.spec.js @@ -1,15 +1,15 @@ 'use strict' -const { describe, it, before } = require('tap').mocha const assert = require('node:assert') const os = require('node:os') +const { describe, it, before } = require('mocha') const sinon = require('sinon') -require('./setup/core') -const { getConfigFresh } = require('./helpers/config') +require('./setup/core') const SamplingRule = require('../src/sampling_rule') const tracerVersion = require('../../../package.json').version +const { getConfigFresh } = require('./helpers/config') describe('startup logging', () => { let firstStderrCall diff --git a/packages/dd-trace/test/tagger.spec.js b/packages/dd-trace/test/tagger.spec.js index 50a2de4d042..0e0a65bf239 100644 --- a/packages/dd-trace/test/tagger.spec.js +++ b/packages/dd-trace/test/tagger.spec.js @@ -1,10 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, beforeEach } = require('tap').mocha + +const { describe, it, beforeEach } = require('mocha') const constants = require('../src/constants') require('./setup/core') + const ERROR_MESSAGE = constants.ERROR_MESSAGE const ERROR_STACK = constants.ERROR_STACK const ERROR_TYPE = constants.ERROR_TYPE diff --git a/packages/dd-trace/test/telemetry/dependencies.spec.js b/packages/dd-trace/test/telemetry/dependencies.spec.js index d3951d7ba26..fcd287e16de 100644 --- a/packages/dd-trace/test/telemetry/dependencies.spec.js +++ b/packages/dd-trace/test/telemetry/dependencies.spec.js @@ -1,12 +1,11 @@ 'use strict' const assert = require('node:assert/strict') +const path = require('node:path') -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') -const path = require('node:path') const dc = require('dc-polyfill') require('../setup/core') @@ -163,8 +162,14 @@ describe('dependencies', () => { { name: request, version: packageVersion } ] } - expect(sendData) - .to.have.been.calledOnceWith(config, application, host, 'app-dependencies-loaded', expectedDependencies) + sinon.assert.calledOnceWithMatch( + sendData, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies + ) }) it('should call sendData with computed request from file path when it does not come in message', () => { @@ -178,8 +183,14 @@ describe('dependencies', () => { { name: request, version: packageVersion } ] } - expect(sendData) - .to.have.been.calledOnceWith(config, application, host, 'app-dependencies-loaded', expectedDependencies) + sinon.assert.calledOnceWithMatch( + sendData, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies + ) }) it('should call sendData with computed request from filename with scope when it does not come in message', () => { @@ -194,8 +205,14 @@ describe('dependencies', () => { { name: request, version: packageVersion } ] } - expect(sendData) - .to.have.been.calledOnceWith(config, application, host, 'app-dependencies-loaded', expectedDependencies) + sinon.assert.calledOnceWithMatch( + sendData, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies + ) }) it('should only include one copy of each dependency, regardless of how many of its files are loaded', () => { @@ -211,8 +228,14 @@ describe('dependencies', () => { { name: moduleName, version: packageVersion } ] } - expect(sendData) - .to.have.been.calledOnceWith(config, application, host, 'app-dependencies-loaded', expectedDependencies) + sinon.assert.calledOnceWithMatch( + sendData, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies + ) }) it('should include two dependencies when they are in different paths', () => { @@ -246,11 +269,23 @@ describe('dependencies', () => { } sinon.assert.calledTwice(sendData) - expect(sendData.firstCall) - .to.have.been.calledWith(config, application, host, 'app-dependencies-loaded', expectedDependencies1) - - expect(sendData.secondCall) - .to.have.been.calledWith(config, application, host, 'app-dependencies-loaded', expectedDependencies2) + sinon.assert.calledWith( + sendData.firstCall, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies1 + ) + + sinon.assert.calledWith( + sendData.secondCall, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies2 + ) }) it('should include only one dependency when they are in different paths but the version number is the same', () => { @@ -276,8 +311,14 @@ describe('dependencies', () => { { name: moduleName, version: packageVersion } ] } - expect(sendData).to.have.been - .calledOnceWith(config, application, host, 'app-dependencies-loaded', expectedDependencies) + sinon.assert.calledOnceWithMatch( + sendData, + config, + application, + host, + 'app-dependencies-loaded', + expectedDependencies + ) }) it('should call sendData only once with duplicated dependency', () => { diff --git a/packages/dd-trace/test/telemetry/endpoints.spec.js b/packages/dd-trace/test/telemetry/endpoints.spec.js index 2fdaa8f1bf0..b08b2dacf6e 100644 --- a/packages/dd-trace/test/telemetry/endpoints.spec.js +++ b/packages/dd-trace/test/telemetry/endpoints.spec.js @@ -1,13 +1,13 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') const dc = require('dc-polyfill') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') const originalSetImmediate = global.setImmediate @@ -107,21 +107,21 @@ describe('endpoints telemetry', () => { type: 'REST', method: 'GET', path: '/api', - operation_name: 'http.request', + operation_name: 'fastify.request', resource_name: 'GET /api' }, { type: 'REST', method: 'POST', path: '/api', - operation_name: 'http.request', + operation_name: 'fastify.request', resource_name: 'POST /api' }, { type: 'REST', method: 'PUT', path: '/api', - operation_name: 'http.request', + operation_name: 'fastify.request', resource_name: 'PUT /api' } ]) @@ -170,6 +170,42 @@ describe('endpoints telemetry', () => { assert.deepStrictEqual(resources, ['GET /test', 'HEAD /test']) }) + it('should use express.request as operation_name for express routes', () => { + expressRouteCh.publish({ method: 'POST', path: '/express-test' }) + + scheduledCallbacks.forEach(cb => cb()) + + sinon.assert.calledOnce(sendData) + const payload = sendData.firstCall.args[4] + assertObjectContains(payload.endpoints, [ + { + type: 'REST', + method: 'POST', + path: '/express-test', + operation_name: 'express.request', + resource_name: 'POST /express-test' + } + ]) + }) + + it('should use express.request as operation_name for router routes', () => { + routerRouteCh.publish({ method: 'DELETE', path: '/router-test' }) + + scheduledCallbacks.forEach(cb => cb()) + + sinon.assert.calledOnce(sendData) + const payload = sendData.firstCall.args[4] + assertObjectContains(payload.endpoints, [ + { + type: 'REST', + method: 'DELETE', + path: '/router-test', + operation_name: 'express.request', + resource_name: 'DELETE /router-test' + } + ]) + }) + it('should record express wildcard and ignore subsequent specific methods for same path', () => { expressRouteCh.publish({ method: '*', path: '/all' }) expressRouteCh.publish({ method: 'GET', path: '/all' }) diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index c495e1aaf31..41adb9e8616 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -1,19 +1,19 @@ 'use strict' const assert = require('node:assert/strict') - -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach, before, after } = require('tap').mocha -const sinon = require('sinon') -const proxyquire = require('proxyquire').noPreserveCache() const http = require('node:http') const { once } = require('node:events') const os = require('node:os') -require('../setup/core') +const { describe, it, beforeEach, afterEach, before, after } = require('mocha') +const sinon = require('sinon') +const proxyquire = require('proxyquire').noPreserveCache() +const { assertObjectContains } = require('../../../../integration-tests/helpers') +require('../setup/core') const { storage } = require('../../../datadog-core') const tracerVersion = require('../../../../package.json').version +const processTags = require('../../src/process-tags') const DEFAULT_HEARTBEAT_INTERVAL = 60000 @@ -106,9 +106,9 @@ describe('telemetry', () => { bar2: { _enabled: false } } /** - * @type {Object} CircularObject + * @type {object} CircularObject * @property {string} field - * @property {Object} child + * @property {object} child * @property {string} child.field * @property {CircularObject | null} child.parent */ @@ -164,12 +164,48 @@ describe('telemetry', () => { }) }) + it('should include process_tags in telemetry application object', async () => { + // Wait for the app-started request + while (traceAgent.reqs.length < 1) { + await once(traceAgent, 'handled-req') + } + const req = traceAgent.reqs[0] + + // Verify process_tags exists in application object + assert.ok(req.body.application, 'application object should exist') + assert.ok(req.body.application.process_tags, 'process_tags should exist in application object') + assert.strictEqual(typeof req.body.application.process_tags, 'object', 'process_tags should be an object') + + // Verify specific process tag fields by name + const processTags = req.body.application.process_tags + assert.ok(Object.hasOwn(processTags, 'entrypoint.type'), 'should have entrypoint.type field') + assert.strictEqual(processTags['entrypoint.type'], 'script', 'entrypoint.type should be "script"') + + assert.ok(Object.hasOwn(processTags, 'entrypoint.workdir'), 'should have entrypoint.workdir field') + assert.strictEqual(typeof processTags['entrypoint.workdir'], 'string', 'entrypoint.workdir should be a string') + + assert.ok(Object.hasOwn(processTags, 'entrypoint.name'), 'should have entrypoint.name field') + assert.strictEqual(typeof processTags['entrypoint.name'], 'string', 'entrypoint.name should be a string') + + assert.ok(Object.hasOwn(processTags, 'entrypoint.basedir'), 'should have entrypoint.basedir field') + assert.strictEqual(typeof processTags['entrypoint.basedir'], 'string', 'entrypoint.basedir should be a string') + + // package.json.name should exist + assert.ok(Object.hasOwn(processTags, 'package.json.name'), 'should have package.json.name field') + assert.strictEqual(typeof processTags['package.json.name'], 'string', 'package.json.name should be a string') + + // Verify no undefined values in process_tags + Object.entries(processTags).forEach(([key, value]) => { + assert.notStrictEqual(value, undefined, `process_tags.${key} should not be undefined`) + }) + }) + it('should send app-integrations', () => { return testSeq(2, 'app-integrations-change', payload => { assert.deepStrictEqual(payload, { integrations: [ - { name: 'foo2', enabled: true, auto_enabled: true }, - { name: 'bar2', enabled: false, auto_enabled: true } + { name: 'foo2', enabled: true, auto_enabled: true, process_tags: processTags.tagsObject }, + { name: 'bar2', enabled: false, auto_enabled: true, process_tags: processTags.tagsObject } ] }) }) @@ -182,7 +218,7 @@ describe('telemetry', () => { return testSeq(3, 'app-integrations-change', payload => { assert.deepStrictEqual(payload, { integrations: [ - { name: 'baz2', enabled: true, auto_enabled: true } + { name: 'baz2', enabled: true, auto_enabled: true, process_tags: processTags.tagsObject } ] }) }) @@ -195,7 +231,7 @@ describe('telemetry', () => { return testSeq(4, 'app-integrations-change', payload => { assert.deepStrictEqual(payload, { integrations: [ - { name: 'boo2', enabled: true, auto_enabled: true } + { name: 'boo2', enabled: true, auto_enabled: true, process_tags: processTags.tagsObject } ] }) }) @@ -561,7 +597,8 @@ describe('Telemetry retry', () => { integrations: [{ name: 'boo3', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] }) @@ -574,7 +611,8 @@ describe('Telemetry retry', () => { integrations: [{ name: 'boo5', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] } @@ -584,7 +622,8 @@ describe('Telemetry retry', () => { integrations: [{ name: 'boo3', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] } @@ -651,12 +690,14 @@ describe('Telemetry retry', () => { integrations: [{ name: 'foo2', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }, { name: 'bar2', enabled: false, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] } @@ -719,7 +760,8 @@ describe('Telemetry retry', () => { integrations: [{ name: 'zoo1', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] }) }) @@ -783,7 +825,8 @@ describe('Telemetry retry', () => { integrations: [{ name: 'zoo1', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] } @@ -793,7 +836,8 @@ describe('Telemetry retry', () => { integrations: [{ name: 'foo1', enabled: true, - auto_enabled: true + auto_enabled: true, + process_tags: processTags.tagsObject }] } @@ -860,8 +904,18 @@ describe('Telemetry retry', () => { assertObjectContains(extendedHeartbeatPayload, { integrations: [{ integrations: [ - { name: 'foo2', enabled: true, auto_enabled: true }, - { name: 'bar2', enabled: false, auto_enabled: true } + { + name: 'foo2', + enabled: true, + auto_enabled: true, + process_tags: processTags.tagsObject + }, + { + name: 'bar2', + enabled: false, + auto_enabled: true, + process_tags: processTags.tagsObject + } ] }] }) @@ -1051,7 +1105,7 @@ async function testSeq (seqId, reqType, validatePayload) { }, host }) - assert.strictEqual([1, 0, -1].includes(Math.floor(Date.now() / 1000) - req.body.tracer_time), true) + assertObjectContains([1, 0, -1], [Math.floor(Date.now() / 1000) - req.body.tracer_time]) validatePayload(req.body.payload) } diff --git a/packages/dd-trace/test/telemetry/logs/index.spec.js b/packages/dd-trace/test/telemetry/logs/index.spec.js index 4f2f40a7aa5..1e0e7b6dc36 100644 --- a/packages/dd-trace/test/telemetry/logs/index.spec.js +++ b/packages/dd-trace/test/telemetry/logs/index.spec.js @@ -1,7 +1,6 @@ 'use strict' -const { expect } = require('chai') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -69,7 +68,7 @@ describe('telemetry logs', () => { defaultConfig.telemetry.logCollection = false logs.start(defaultConfig) - expect(telemetryLog.subscribe).to.not.been.called + sinon.assert.notCalled(telemetryLog.subscribe) }) }) @@ -114,13 +113,13 @@ describe('telemetry logs', () => { it('should be called with WARN level', () => { telemetryLog.publish({ message: 'message', level: 'WARN' }) - expect(logCollectorAdd).to.be.calledOnceWith(match({ message: 'message', level: 'WARN' })) + sinon.assert.calledOnceWithExactly(logCollectorAdd, match({ message: 'message', level: 'WARN' })) }) it('should be called with ERROR level', () => { telemetryLog.publish({ message: 'message', level: 'ERROR' }) - expect(logCollectorAdd).to.be.calledOnceWith(match({ message: 'message', level: 'ERROR' })) + sinon.assert.calledOnceWithExactly(logCollectorAdd, match({ message: 'message', level: 'ERROR' })) }) it('should be called with ERROR level and stack_trace', () => { @@ -128,7 +127,10 @@ describe('telemetry logs', () => { const stack = error.stack telemetryLog.publish({ message: error.message, stack_trace: stack, level: 'ERROR' }) - expect(logCollectorAdd).to.be.calledOnceWith(match({ message: 'message', level: 'ERROR', stack_trace: stack })) + sinon.assert.calledOnceWithExactly( + logCollectorAdd, + match({ message: 'message', level: 'ERROR', stack_trace: stack }) + ) }) it('should not be called with no defined level', () => { @@ -149,19 +151,18 @@ describe('telemetry logs', () => { const stack = error.stack errorLog.publish({ cause: error, sendViaTelemetry: true }) - expect(logCollectorAdd) - .to.be.calledOnceWith(match({ - message: 'Generic Error', - level: 'ERROR', - errorType: 'Error', - stack_trace: stack - })) + sinon.assert.calledOnceWithExactly(logCollectorAdd, match({ + message: 'Generic Error', + level: 'ERROR', + errorType: 'Error', + stack_trace: stack + })) }) it('should be called when an error string is published to datadog:log:error', () => { errorLog.publish({ message: 'custom error message', sendViaTelemetry: true }) - expect(logCollectorAdd).to.be.calledOnceWith(match({ + sinon.assert.calledOnceWithExactly(logCollectorAdd, match({ message: 'custom error message', level: 'ERROR', stack_trace: undefined @@ -217,7 +218,7 @@ describe('telemetry logs', () => { logs.send(defaultConfig, application, host) - expect(sendData).to.be.calledOnceWithExactly(defaultConfig, application, host, 'logs', { logs: collectedLogs }) + sinon.assert.calledOnceWithExactly(sendData, defaultConfig, application, host, 'logs', { logs: collectedLogs }) }) it('should not drain logCollector and call sendData if not enabled', () => { diff --git a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js index 42a5e33867d..f0e6a25b5b0 100644 --- a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js +++ b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it, afterEach } = require('tap').mocha -require('../../setup/core') +const { describe, it, afterEach } = require('mocha') +require('../../setup/core') const { ddBasePath } = require('../../../src/util') const EOL = '\n' diff --git a/packages/dd-trace/test/telemetry/metrics.spec.js b/packages/dd-trace/test/telemetry/metrics.spec.js index 6bf7e38bc73..6ca6dad0fb3 100644 --- a/packages/dd-trace/test/telemetry/metrics.spec.js +++ b/packages/dd-trace/test/telemetry/metrics.spec.js @@ -2,8 +2,7 @@ const assert = require('node:assert/strict') -const { expect } = require('chai') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') @@ -121,38 +120,36 @@ describe('metrics', () => { manager.send(config, application, host) - expect(sendData).to.have.been - .calledWith(config, application, host, 'generate-metrics', { - namespace: 'test1', - series: [ - { - metric: 'metric1', - points: [[now / 1e3, 1]], - interval: undefined, - type: 'count', - tags: [ - 'bar:baz' - ], - common: true - } - ] - }) - expect(sendData).to.have.been - .calledWith(config, application, host, 'generate-metrics', { - namespace: 'test2', - series: [ - { - metric: 'metric2', - points: [[now / 1e3, 1]], - interval: undefined, - type: 'count', - tags: [ - 'bux:bax' - ], - common: true - } - ] - }) + sinon.assert.calledWith(sendData, config, application, host, 'generate-metrics', { + namespace: 'test1', + series: [ + { + metric: 'metric1', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bar:baz' + ], + common: true + } + ] + }) + sinon.assert.calledWith(sendData, config, application, host, 'generate-metrics', { + namespace: 'test2', + series: [ + { + metric: 'metric2', + points: [[now / 1e3, 1]], + interval: undefined, + type: 'count', + tags: [ + 'bux:bax' + ], + common: true + } + ] + }) }) it('should not send empty metrics', () => { @@ -338,7 +335,7 @@ describe('metrics', () => { metric.dec() - expect(metric.track).to.be.calledWith(-1) + sinon.assert.calledWith(metric.track, -1) assert.deepStrictEqual(metric.points, [ [now / 1e3, 1] @@ -355,7 +352,7 @@ describe('metrics', () => { metric.dec(2) - expect(metric.track).to.be.calledWith(-2) + sinon.assert.calledWith(metric.track, -2) assert.deepStrictEqual(metric.points, [ [now / 1e3, 1] diff --git a/packages/dd-trace/test/telemetry/send-data.spec.js b/packages/dd-trace/test/telemetry/send-data.spec.js index 29caacf05a1..63b6e0ae47c 100644 --- a/packages/dd-trace/test/telemetry/send-data.spec.js +++ b/packages/dd-trace/test/telemetry/send-data.spec.js @@ -1,12 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../../integration-tests/helpers') -const { describe, it, beforeEach } = require('tap').mocha +const { describe, it, beforeEach } = require('mocha') const sinon = require('sinon') const proxyquire = require('proxyquire') +const { assertObjectContains } = require('../../../../integration-tests/helpers') require('../setup/core') describe('sendData', () => { diff --git a/packages/dd-trace/test/tracer.spec.js b/packages/dd-trace/test/tracer.spec.js index 77af3fe79f9..02b7ca9d054 100644 --- a/packages/dd-trace/test/tracer.spec.js +++ b/packages/dd-trace/test/tracer.spec.js @@ -1,13 +1,12 @@ 'use strict' const assert = require('node:assert/strict') -const { assertObjectContains } = require('../../../integration-tests/helpers') -const { describe, it, beforeEach, afterEach } = require('tap').mocha +const { describe, it, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') +const { assertObjectContains } = require('../../../integration-tests/helpers') require('./setup/core') - const Tracer = require('../src/tracer') const Span = require('../src/opentracing/span') const getConfig = require('../src/config') diff --git a/packages/dd-trace/test/util.spec.js b/packages/dd-trace/test/util.spec.js index cef6dcfd196..c32ed25271a 100644 --- a/packages/dd-trace/test/util.spec.js +++ b/packages/dd-trace/test/util.spec.js @@ -1,10 +1,10 @@ 'use strict' const assert = require('node:assert/strict') -const { describe, it } = require('tap').mocha -require('./setup/core') +const { describe, it } = require('mocha') +require('./setup/core') const { isTrue, isFalse, globMatch } = require('../src/util') const TRUES = [ diff --git a/scripts/install_plugin_modules.js b/scripts/install_plugin_modules.js index a2190ed3982..841633acc8c 100644 --- a/scripts/install_plugin_modules.js +++ b/scripts/install_plugin_modules.js @@ -5,11 +5,11 @@ const { arch } = require('os') const { join } = require('path') const { createHash } = require('crypto') const semver = require('semver') -const exec = require('./helpers/exec') const externals = require('../packages/dd-trace/test/plugins/externals.json') const { getInstrumentation } = require('../packages/dd-trace/test/setup/helpers/load-inst') const { getCappedRange } = require('../packages/dd-trace/test/plugins/versions') - +const { isRelativeRequire } = require('../packages/datadog-instrumentations/src/helpers/shared-utils') +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) @@ -112,6 +112,8 @@ async function assertFolder (name, version) { * @param {boolean} external */ async function assertPackage (name, version, dependencyVersionRange, external) { + // Early return to prevent filePaths from being installed, their non path counterpars should suffice + if (isRelativeRequire(name)) return const dependencies = { [name]: getCappedRange(name, dependencyVersionRange) } @@ -167,7 +169,8 @@ async function assertPeerDependencies (rootFolder, parent = '') { const versionPkgJsonPath = join(folder, 'package.json') const versionPkgJson = require(versionPkgJsonPath) - for (const { dep, name } of externalDeps.get(externalName)) { + for (const { dep, name, node } of externalDeps.get(externalName)) { + if (node && !semver.satisfies(process.versions.node, node)) continue const pkgJsonPath = require(folder).pkgJsonPath() const pkgJson = require(pkgJsonPath) diff --git a/scripts/mocha-parallel-files.js b/scripts/mocha-parallel-files.js new file mode 100644 index 00000000000..e3e99530a72 --- /dev/null +++ b/scripts/mocha-parallel-files.js @@ -0,0 +1,561 @@ +'use strict' + +const { spawn } = require('child_process') +const fs = require('fs') +const os = require('os') +const path = require('path') +const { stripVTControlCharacters: stripAnsi } = require('util') + +const { globSync } = require('glob') + +/** + * @typedef {{ + * reporterEnabled: string[], + * xunitReporterOptions?: { output: string } + * }} ReporterOptions + * + * @typedef {{ + * type: 'mocha-run-file-result', + * passes?: number, + * failures?: number, + * pending?: number, + * tests?: number, + * duration?: number + * }} MochaRunFileResultMessage + */ + +/** + * @param {string[]} argv + * @returns {{jobs?: number, timeout?: number, exposeGc: boolean, require: string[], patterns: string[]}} + */ +function parseArgs (argv) { + /** @type {{jobs?: number, timeout?: number, exposeGc: boolean, require: string[], patterns: string[]}} */ + const opts = { exposeGc: false, require: [], patterns: [] } + + /** @type {string[]} */ + const rest = [] + let seenDoubleDash = false + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + + if (seenDoubleDash) { + rest.push(arg) + continue + } + + if (arg === '--') { + seenDoubleDash = true + continue + } + + if (arg === '--expose-gc') { + opts.exposeGc = true + continue + } + + if (arg === '--jobs') { + const value = argv[++i] + opts.jobs = value ? Number.parseInt(value, 10) : undefined + continue + } + + if (arg.startsWith('--jobs=')) { + opts.jobs = Number.parseInt(arg.slice('--jobs='.length), 10) + continue + } + + if (arg === '--timeout') { + const value = argv[++i] + opts.timeout = value ? Number.parseInt(value, 10) : undefined + continue + } + + if (arg.startsWith('--timeout=')) { + opts.timeout = Number.parseInt(arg.slice('--timeout='.length), 10) + continue + } + + if (arg === '--require' || arg === '-r') { + const value = argv[++i] + if (value) opts.require.push(value) + continue + } + + // Unknown flag; treat it as part of patterns to keep the tool flexible. + rest.push(arg) + } + + opts.patterns = rest + return opts +} + +function stableUnique (items) { + return [...new Set(items)] +} + +function ensureDir (dir) { + fs.mkdirSync(dir, { recursive: true }) +} + +function mergeXunitFilesToSingleTestsuite (inputFiles, outputFile) { + let totalTests = 0 + let totalErrors = 0 + let totalFailures = 0 + let totalSkipped = 0 + let totalTime = 0 + const testcases = [] + + for (const file of inputFiles) { + const xml = fs.readFileSync(file, 'utf8') + const openTagMatch = xml.match(/]*>/) + if (!openTagMatch) continue + + const openTag = openTagMatch[0] + const openIdx = xml.indexOf(openTag) + const closeTag = '' + const closeIdx = xml.lastIndexOf(closeTag) + if (openIdx === -1 || closeIdx === -1) continue + + const inner = xml.slice(openIdx + openTag.length, closeIdx) + + const attr = (name) => { + const m = openTag.match(new RegExp(`${name}="([^"]*)"`)) + return m ? Number(m[1]) : 0 + } + + totalTests += attr('tests') + totalFailures += attr('failures') + totalErrors += attr('errors') + totalSkipped += attr('skipped') + totalTime += attr('time') + + testcases.push(inner.trim()) + } + + const timestamp = new Date().toUTCString() + const merged = [ + ``, + testcases.filter(Boolean).join('\n'), + '\n' + ].join('\n') + + fs.writeFileSync(outputFile, merged) +} + +/** + * @param {unknown} msg + * @returns {msg is MochaRunFileResultMessage} + */ +function isMochaRunFileResultMessage (msg) { + if (!msg || typeof msg !== 'object') return false + return /** @type {Record} */ (msg).type === 'mocha-run-file-result' +} + +async function main () { + // If output is piped (e.g. to `head`), writes can throw EPIPE. Exit cleanly. + process.stdout.on('error', (err) => { + if (err && err.code === 'EPIPE') process.exit(0) + }) + process.stderr.on('error', (err) => { + if (err && err.code === 'EPIPE') process.exit(0) + }) + + const opts = parseArgs(process.argv.slice(2)) + if (opts.patterns.length === 0) { + process.stderr.write( + 'Usage: node scripts/mocha-parallel-files.js [--jobs N] [--timeout MS] ' + + '[--require path]... [--expose-gc] -- ...\n' + ) + process.exitCode = 2 + return + } + + const jobs = typeof opts.jobs === 'number' && Number.isFinite(opts.jobs) + ? Math.max(1, opts.jobs || 1) + : Math.max(1, Math.min(os.cpus().length, 8)) + + const timeout = typeof opts.timeout === 'number' && Number.isFinite(opts.timeout) ? opts.timeout : 30_000 + + const expandedFiles = stableUnique(opts.patterns.flatMap(p => + globSync(p, { nodir: true, windowsPathsNoEscape: true }).sort((a, b) => a.localeCompare(b, 'en')) + )) + + if (expandedFiles.length === 0) { + process.stderr.write('No test files matched.\n') + process.exitCode = 1 + return + } + + const repoRoot = process.cwd() + const runFileScript = path.join(repoRoot, 'scripts', 'mocha-run-file.js') + + const junitTmpDir = path.join(repoRoot, '.junit-tmp') + const junitOutFile = path.join(repoRoot, `node-${process.versions.node}-junit.xml`) + const junitTmpFiles = [] + const emitJunit = Boolean(process.env.CI) + + if (emitJunit) { + fs.rmSync(junitOutFile, { force: true }) + + if (jobs > 1) { + ensureDir(junitTmpDir) + // Clean up any stale junit shards. + for (const entry of fs.readdirSync(junitTmpDir)) { + if (entry.endsWith('.xml')) fs.rmSync(path.join(junitTmpDir, entry), { force: true }) + } + } + } + + let idx = 0 + let running = 0 + let failures = 0 + /** @type {{file: string, code: number|null, signal: NodeJS.Signals|null}[]} */ + const failed = [] + + const isFailureStartLine = (line) => /\b\d+\s+failing\b/.test(stripAnsi(line)) + const isWarningLine = (line) => { + const plain = stripAnsi(line) + return ( + plain.includes('DeprecationWarning:') || + plain.includes('MaxListenersExceededWarning:') || + plain.includes('ExperimentalWarning:') || + plain.includes('Warning: ') || + plain.includes('Use `node --trace-warnings') || + plain.includes('Use `node --trace-deprecation') + ) + } + + const splitLines = (carry, chunk) => { + const next = carry + chunk + const parts = next.split('\n') + return { lines: parts.slice(0, -1).map(l => l + '\n'), carry: parts.at(-1) ?? '' } + } + + const entries = expandedFiles.map((file) => ({ + file, + started: false, + exited: false, + stdoutEnded: false, + stderrEnded: false, + code: /** @type {number|null} */ (null), + signal: /** @type {NodeJS.Signals|null} */ (null), + + // Output buffers (in-memory). For non-active entries, preserve stdout/stderr warning ordering + // by buffering a merged stream with per-line ordering. + outBuf: /** @type {{stream:'stdout'|'stderr', text:string}[]} */ ([]), + stderrErrBuf: /** @type {string[]} */ ([]), + failureBuf: /** @type {string[]} */ ([]), + + stdoutCarry: '', + stderrCarry: '', + stdoutInFailure: false, + + stats: /** @type {{passes:number, failures:number, pending:number, tests:number, duration:number}|null} */ (null) + })) + + let activeIndex = 0 + const isActive = (entryIndex) => entryIndex === activeIndex + + const flushActiveBuffers = () => { + const entry = entries[activeIndex] + if (!entry) return + + if (entry.outBuf.length) { + for (const { stream, text } of entry.outBuf) { + if (stream === 'stderr') process.stderr.write(text) + else process.stdout.write(text) + } + entry.outBuf.length = 0 + } + } + + const maybeAdvanceActive = () => { + while (activeIndex < entries.length) { + const entry = entries[activeIndex] + // Only advance when the active entry is fully done and we have flushed what we buffered. + if (entry.exited && entry.stdoutEnded && entry.stderrEnded) { + flushActiveBuffers() + activeIndex++ + flushActiveBuffers() + continue + } + break + } + } + + const handleStdoutLine = (entryIndex, line) => { + const entry = entries[entryIndex] + if (!entry.stdoutInFailure && isFailureStartLine(line)) { + entry.stdoutInFailure = true + } + if (entry.stdoutInFailure) { + entry.failureBuf.push(line) + } else { + if (isActive(entryIndex)) process.stdout.write(line) + else entry.outBuf.push({ stream: 'stdout', text: line }) + } + } + + const handleStdoutChunk = (entryIndex, chunk) => { + const entry = entries[entryIndex] + const { lines, carry } = splitLines(entry.stdoutCarry, chunk) + entry.stdoutCarry = carry + for (const line of lines) handleStdoutLine(entryIndex, line) + } + + const handleStderrLine = (entryIndex, line) => { + const entry = entries[entryIndex] + if (isWarningLine(line)) { + if (isActive(entryIndex)) process.stderr.write(line) + else entry.outBuf.push({ stream: 'stderr', text: line }) + } else { + entry.stderrErrBuf.push(line) + } + } + + const handleStderrChunk = (entryIndex, chunk) => { + const entry = entries[entryIndex] + const { lines, carry } = splitLines(entry.stderrCarry, chunk) + entry.stderrCarry = carry + for (const line of lines) handleStderrLine(entryIndex, line) + } + + const runPromise = new Promise((resolve) => { + const launchNext = () => { + while (running < jobs && idx < expandedFiles.length) { + const entry = entries[idx] + const file = entry.file + idx++ + running++ + + const junitShard = emitJunit + ? (jobs > 1 + ? path.join(junitTmpDir, `node-${process.versions.node}-${process.pid}-${idx}.xml`) + : junitOutFile) + : undefined + + if (junitShard) junitTmpFiles.push(junitShard) + + /** @type {ReporterOptions} */ + const reporterOptions = emitJunit + ? { + reporterEnabled: ['spec', 'xunit'], + xunitReporterOptions: { output: junitShard } + } + : { + reporterEnabled: ['spec'] + } + + const childEnv = { + ...process.env, + MOCHA_RUN_FILE_CONFIG: JSON.stringify({ + timeout, + color: true, + reporter: 'mocha-multi-reporters', + reporterOptions, + require: opts.require + }), + } + + const nodeArgs = [] + if (opts.exposeGc) nodeArgs.push('--expose-gc') + nodeArgs.push(runFileScript, file) + + entry.started = true + + const child = spawn(process.execPath, nodeArgs, { + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + env: childEnv + }) + + if (!child.stdout || !child.stderr) { + throw new Error('Expected child stdout/stderr to be piped') + } + + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + + const entryIndex = idx - 1 + + child.stdout.on('data', (data) => { + handleStdoutChunk(entryIndex, data) + }) + child.stderr.on('data', (data) => { + handleStderrChunk(entryIndex, data) + }) + + child.on('message', (msg) => { + const m = /** @type {unknown} */ (msg) + if (isMochaRunFileResultMessage(m)) { + entries[entryIndex].stats = { + passes: m.passes || 0, + failures: m.failures || 0, + pending: m.pending || 0, + tests: m.tests || 0, + duration: m.duration || 0 + } + } + }) + + child.stdout.on('end', () => { + const entry = entries[entryIndex] + if (entry.stdoutCarry) { + const tail = entry.stdoutCarry + entry.stdoutCarry = '' + // process leftover without newline + handleStdoutLine(entryIndex, tail) + } + entry.stdoutEnded = true + maybeAdvanceActive() + }) + + child.stderr.on('end', () => { + const entry = entries[entryIndex] + if (entry.stderrCarry) { + const tail = entry.stderrCarry + entry.stderrCarry = '' + handleStderrLine(entryIndex, tail) + } + entry.stderrEnded = true + maybeAdvanceActive() + }) + + child.on('exit', (code, signal) => { + running-- + if (code || signal) { + failures++ + failed.push({ file, code, signal }) + } + + entry.exited = true + entry.code = code + entry.signal = signal + + if (idx >= expandedFiles.length && running === 0) { + resolve(undefined) + } else { + launchNext() + } + + maybeAdvanceActive() + }) + } + } + + launchNext() + }) + + // Wait for all children to complete. Output is emitted live via stream handlers above. + await runPromise + // Ensure any remaining buffered output for the last active file is flushed. + flushActiveBuffers() + + // Print buffered error output (non-warning stderr + mocha failure blocks) after all output has been streamed. + let globalFailureIndex = 0 + let hasConsolidatedErrors = false + for (const entry of entries) { + const stderrErrors = entry.stderrErrBuf.join('').trim() + const hasFailures = entry.failureBuf.length > 0 + if (!stderrErrors && !hasFailures) continue + + if (!hasConsolidatedErrors) { + process.stdout.write('\n=== Errors ===\n') + hasConsolidatedErrors = true + } + + if (stderrErrors) { + process.stdout.write(stderrErrors) + const last = entry.stderrErrBuf[entry.stderrErrBuf.length - 1] + if (last && !last.endsWith('\n')) process.stdout.write('\n') + } + if (hasFailures) { + let appendedFilenameToFailingLine = false + + for (const line of entry.failureBuf) { + let out = line + + // Print `n failing in ` while ensuring the appended filename stays uncolored. + if (!appendedFilenameToFailingLine && isFailureStartLine(out)) { + appendedFilenameToFailingLine = true + + const hasNewline = out.endsWith('\n') + const base = hasNewline ? out.slice(0, -1) : out + // Ensure `in ` is not red by resetting ANSI styles before printing the filename. + // Avoid double-resetting if the line already ends with a reset. + const reset = '\u001b[0m' + out = (base.endsWith(reset) ? base : base + reset) + ' in ' + entry.file + (hasNewline ? '\n' : '') + } + + // Prefix local `n)` failure lines with a deterministic global counter `[n]`. + if (/^\s*\d+\)/.test(stripAnsi(out))) { + globalFailureIndex++ + out = `[${globalFailureIndex}] ` + out + } + + process.stdout.write(out) + } + + const last = entry.failureBuf[entry.failureBuf.length - 1] + if (last && !last.endsWith('\n')) process.stdout.write('\n') + } + } + + if (emitJunit) { + // Merge xunit shards (one per file) into the historical output file name expected by CI tooling. + if (jobs > 1) { + mergeXunitFilesToSingleTestsuite(junitTmpFiles.filter(f => fs.existsSync(f)), junitOutFile) + } + } + + // Summary (always at the very end) + let totalPasses = 0 + let totalFailures = 0 + let totalPending = 0 + let totalTests = 0 + let totalDuration = 0 + let crashedFiles = 0 + + for (const entry of entries) { + // If a child exited non-zero but never reported mocha stats (or reported 0 failures), + // treat it as a "crash/harness failure" so summary reflects failure even when Mocha + // couldn't produce a failing test count (e.g., hard crash, early process.exit()). + if ((entry.code || entry.signal) && (!entry.stats || (entry.stats.failures || 0) === 0)) { + crashedFiles++ + } + + const result = entry.stats + if (!result) continue + totalPasses += result.passes || 0 + totalFailures += result.failures || 0 + totalPending += result.pending || 0 + totalTests += result.tests || 0 + totalDuration += result.duration || 0 + } + + process.stdout.write('\n=== Summary ===\n') + process.stdout.write(`Passed: ${totalPasses}\n`) + process.stdout.write(`Failed: ${totalFailures + crashedFiles}\n`) + process.stdout.write(`Pending: ${totalPending}\n`) + process.stdout.write(`Total: ${totalTests}\n`) + process.stdout.write(`Duration(ms): ${totalDuration}\n`) + if (crashedFiles) process.stdout.write(`Crashed files: ${crashedFiles}\n`) + + if (failed.length) { + process.stdout.write('\n=== Failed files ===\n') + for (const { file, code, signal } of failed) { + process.stdout.write(`- ${file} (exit=${code ?? 'null'} signal=${signal ?? 'null'})\n`) + } + process.stdout.write( + 'Legend: [n] = global failure index across all files; n) = local failure index within a file.\n' + ) + } + + process.exit(failures ? 1 : 0) +} + +main().catch(err => { + process.stderr.write(String(err?.stack || err) + '\n') + process.exitCode = 1 +}) diff --git a/scripts/mocha-run-file.js b/scripts/mocha-run-file.js new file mode 100644 index 00000000000..8e0eb55cabd --- /dev/null +++ b/scripts/mocha-run-file.js @@ -0,0 +1,104 @@ +'use strict' + +const path = require('path') + +const Mocha = require('mocha') + +function parseJson (value, fallback) { + if (!value) return fallback + try { + return JSON.parse(value) + } catch { + return fallback + } +} + +async function main () { + const file = process.argv[2] + if (!file) { + process.stderr.write('Usage: node scripts/mocha-run-file.js \n') + process.exitCode = 2 + return + } + + const resolvedFile = path.resolve(file) + // Many tests (and some production code) expect tap-like semantics where the test file is the "entrypoint". + // Emulate that by pointing argv[1] + require.main.filename at the spec file before loading it. + process.argv[1] = resolvedFile + if (require.main) { + require.main.filename = resolvedFile + } + + if (process.env.MOCHA_RUN_FILE_DEBUG) { + process.stderr.write( + `mocha-run-file debug: execArgv=${JSON.stringify(process.execArgv)} gc=${typeof global.gc}\n` + ) + } + + /** + * @type {{ + * timeout?: number, + * color?: boolean, + * bail?: boolean, + * retries?: number, + * fullTrace?: boolean, + * reporter?: string, + * reporterOptions?: Record, + * require?: string[] + * }} + */ + const config = parseJson(process.env.MOCHA_RUN_FILE_CONFIG, {}) + + const mocha = new Mocha({ + ui: 'bdd', + timeout: config.timeout ?? 5000, + color: config.color ?? true, + bail: config.bail ?? false, + retries: config.retries, + fullTrace: config.fullTrace ?? false, + reporter: config.reporter ?? 'spec', + reporterOptions: config.reporterOptions + }) + + for (const req of config.require ?? []) { + // Resolve relative to repo root (cwd), matching Mocha CLI behavior. + const mod = require(path.resolve(req)) + if (mod?.mochaHooks) { + mocha.rootHooks(mod.mochaHooks) + } + } + + mocha.addFile(resolvedFile) + + await mocha.loadFilesAsync() + + /** @type {import('mocha').Runner|undefined} */ + let runner + const failures = await new Promise(resolve => { + runner = mocha.run(resolve) + }) + + /** @type {{passes?: number, failures?: number, pending?: number, tests?: number, duration?: number}} */ + const stats = runner?.stats || {} + + if (typeof process.send === 'function') { + process.send({ + type: 'mocha-run-file-result', + file: resolvedFile, + passes: stats.passes ?? 0, + failures: stats.failures ?? failures ?? 0, + pending: stats.pending ?? 0, + tests: stats.tests ?? 0, + duration: stats.duration ?? 0 + }) + } + + // Ensure per-file execution behaves like tap (process-per-file): exit with the test result. + // This also avoids hangs from leaked handles (similar to `--exit`). + process.exit(failures ? 1 : 0) +} + +main().catch(err => { + process.stderr.write(String(err?.stack || err) + '\n') + process.exit(1) +}) diff --git a/scripts/release/notes.js b/scripts/release/notes.js index b083839dd24..9a89e9ff0b9 100644 --- a/scripts/release/notes.js +++ b/scripts/release/notes.js @@ -3,8 +3,8 @@ const fs = require('fs') const os = require('os') const path = require('path') -const { capture, run } = require('./helpers/terminal') const pkg = require('../../package.json') +const { capture, run } = require('./helpers/terminal') const version = pkg.version const tag = `v${version}` diff --git a/scripts/verify-ci-config.js b/scripts/verify-ci-config.js index 44a2dca59e5..9d147144c2e 100644 --- a/scripts/verify-ci-config.js +++ b/scripts/verify-ci-config.js @@ -6,10 +6,10 @@ const fs = require('fs') const path = require('path') const util = require('util') -const yaml = require('yaml') -const semver = require('semver') const { execSync } = require('child_process') const Module = require('module') +const yaml = require('yaml') +const semver = require('semver') const { getAllInstrumentations } = require('../packages/dd-trace/test/setup/helpers/load-inst') function errorMsg (title, ...message) { diff --git a/vendor/package.json b/vendor/package.json index 9a47c5cb6d3..ccde2ab13f9 100644 --- a/vendor/package.json +++ b/vendor/package.json @@ -6,7 +6,7 @@ "dependencies": { "@datadog/sketches-js": "2.1.1", "@datadog/source-map": "npm:source-map@^0.6.0", - "@isaacs/ttlcache": "^2.0.1", + "@isaacs/ttlcache": "^2.1.3", "@opentelemetry/core": ">=1.14.0 <1.31.0", "@opentelemetry/resources": ">=1.0.0 <1.31.0", "astring": "^1.9.0", diff --git a/vendor/yarn.lock b/vendor/yarn.lock index cb7232ec6ad..7408b063014 100644 --- a/vendor/yarn.lock +++ b/vendor/yarn.lock @@ -34,10 +34,10 @@ dependencies: tslib "^2.4.0" -"@isaacs/ttlcache@^2.0.1": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-2.1.2.tgz#63f0b897382df54412ab9c9a178107b67ed73c2b" - integrity sha512-lmG2ZHXBo7tgF1I3vGvBMnUJYhaNE25fmx+tUfotvE3vsARzxiiDXmCBI6Ch8qEHeHmfunr+eVPgIaIc8xIkbg== +"@isaacs/ttlcache@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-2.1.3.tgz#b5da3615f0a22e7687779afd899dd8f44cc36602" + integrity sha512-NCnhdBecu8reiXZ067zrpwR+xU2MY9wFTpRGpc7oZ1MwWC3EZmZkuqKdYI1Um5phbH1ql8kfiFjLdFkeuEWhfw== "@jsep-plugin/assignment@^1.3.0": version "1.3.0" diff --git a/yarn.lock b/yarn.lock index 031ea964f01..0c089cbccd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,12 +11,12 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.27.2": +"@babel/compat-data@^7.27.2": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f" integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== -"@babel/core@^7.5.5", "@babel/core@^7.7.5": +"@babel/core@^7.23.9": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== @@ -48,14 +48,7 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-annotate-as-pure@^7.27.1": - version "7.27.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" - integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== - dependencies: - "@babel/types" "^7.27.3" - -"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.27.2": +"@babel/helper-compilation-targets@^7.27.2": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== @@ -88,11 +81,6 @@ "@babel/helper-validator-identifier" "^7.27.1" "@babel/traverse" "^7.28.3" -"@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" - integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== - "@babel/helper-string-parser@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" @@ -116,64 +104,13 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.4" -"@babel/parser@^7.27.2", "@babel/parser@^7.28.5": +"@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: "@babel/types" "^7.28.5" -"@babel/plugin-proposal-object-rest-spread@^7.5.5": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-syntax-jsx@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" - integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-transform-destructuring@^7.5.0": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" - integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/traverse" "^7.28.5" - -"@babel/plugin-transform-parameters@^7.20.7": - version "7.27.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" - integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== - dependencies: - "@babel/helper-plugin-utils" "^7.27.1" - -"@babel/plugin-transform-react-jsx@^7.3.0": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz#1023bc94b78b0a2d68c82b5e96aed573bcfb9db0" - integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.27.1" - "@babel/helper-module-imports" "^7.27.1" - "@babel/helper-plugin-utils" "^7.27.1" - "@babel/plugin-syntax-jsx" "^7.27.1" - "@babel/types" "^7.27.1" - "@babel/template@^7.27.2": version "7.27.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" @@ -196,7 +133,7 @@ "@babel/types" "^7.28.5" debug "^4.3.1" -"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.4", "@babel/types@^7.28.5": +"@babel/types@^7.27.1", "@babel/types@^7.28.4", "@babel/types@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== @@ -246,10 +183,10 @@ "@datadog/flagging-core" "0.2.0" "@openfeature/server-sdk" "~1.18.0" -"@datadog/pprof@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.12.0.tgz#1f0a592aec5ea3a48ad795d069e8a925d21667db" - integrity sha512-qX32upm9eqObGVGvqHpjQB2bXVPTX0ccXTW3mUqUWXgJrAKyHtTfo9PqfoXhflYs0WD9el9xl9c0bM1RS4vRmQ== +"@datadog/pprof@5.13.1": + version "5.13.1" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.13.1.tgz#51c540d75cf4471806db65d0686cbe0a96125ce2" + integrity sha512-7yXQco1xOUFFEHN3UsRw/55603lQTctHcGx9N7PgkcgLGL8t/i5qKalF0AhOKBsLBUnbQ9Iv+ecC2YJErJ07PQ== dependencies: delay "^5.0.0" node-gyp-build "<4.0" @@ -386,21 +323,6 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@isaacs/import-jsx@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/import-jsx/-/import-jsx-4.0.1.tgz#493cab5fc543a0703dba7c3f5947d6499028a169" - integrity sha512-l34FEsEqpdYdGcQjRCxWy+7rHY6euUbOBz9FI+Mq6oQeVhNegHcXFSJxVxrJvOpO31NbnDjS74quKXDlPDearA== - dependencies: - "@babel/core" "^7.5.5" - "@babel/plugin-proposal-object-rest-spread" "^7.5.5" - "@babel/plugin-transform-destructuring" "^7.5.0" - "@babel/plugin-transform-react-jsx" "^7.3.0" - caller-path "^3.0.1" - find-cache-dir "^3.2.0" - make-dir "^3.0.2" - resolve-from "^3.0.0" - rimraf "^3.0.0" - "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -412,7 +334,7 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" -"@istanbuljs/schema@^0.1.2": +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== @@ -776,14 +698,14 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^13.0.5": - version "13.0.5" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" - integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw== +"@sinonjs/fake-timers@^15.1.0": + version "15.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-15.1.0.tgz#f42e713425d4eb1a7bc88ef5d7f76c4546586c25" + integrity sha512-cqfapCxwTGsrR80FEgOoPsTonoefMBY7dnUEbQ+GRcved0jvkJLzvX6F4WtN+HBqbPX/SiFsIRUp+IrCW/2I2w== dependencies: "@sinonjs/commons" "^3.0.1" -"@sinonjs/samsam@^8.0.1": +"@sinonjs/samsam@^8.0.3": version "8.0.3" resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.3.tgz#eb6ffaef421e1e27783cc9b52567de20cb28072d" integrity sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ== @@ -808,11 +730,6 @@ resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.159.tgz#0ab559f4519e42732a393b28c21564da9763c5aa" integrity sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg== -"@types/chai@^4.3.16": - version "4.3.20" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.20.tgz#cb291577ed342ca92600430841a00329ba05cecc" - integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== - "@types/estree@^1.0.6", "@types/estree@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" @@ -833,13 +750,6 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.10.tgz#91f62905e8d23cbd66225312f239454a23bebfa0" integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q== -"@types/node@*": - version "25.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.0.tgz#c0e0022c3c7b41635c49322e6b3a0279fffa7d62" - integrity sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew== - dependencies: - undici-types "~7.16.0" - "@types/node@^18.19.106": version "18.19.130" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" @@ -847,25 +757,6 @@ dependencies: undici-types "~5.26.4" -"@types/prop-types@*": - version "15.7.15" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" - integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== - -"@types/react@^17.0.52": - version "17.0.90" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.90.tgz#38b72ea265c7ea369cf2998b406f850f6f665825" - integrity sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "^0.16" - csstype "^3.2.2" - -"@types/scheduler@^0.16": - version "0.16.8" - resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" - integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== - "@types/sinon@^21.0.0": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-21.0.0.tgz#3a598a29b3aec0512a21e57ae0fd4c09aa013ca9" @@ -878,18 +769,6 @@ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz#49f731d9453f52d64dd79f5a5626c1cf1b81bea4" integrity sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w== -"@types/tap@^15.0.12": - version "15.0.12" - resolved "https://registry.yarnpkg.com/@types/tap/-/tap-15.0.12.tgz#e91b6a2e949d18f71e14bda421b9044c75d3fb0f" - integrity sha512-QuVlxQEBOBASkirrwp0ciwO9stIzOdRMHyaYYsexeVSAYwR4sq+YIYaQbVaYXSXi8+yPf22ZZNieRCB8KAJrTA== - dependencies: - "@types/node" "*" - -"@types/yoga-layout@1.9.2": - version "1.9.2" - resolved "https://registry.yarnpkg.com/@types/yoga-layout/-/yoga-layout-1.9.2.tgz#efaf9e991a7390dc081a0b679185979a83a9639a" - integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== - "@typescript-eslint/types@^8.46.0", "@typescript-eslint/types@^8.47.0": version "8.49.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.49.0.tgz#c1bd3ebf956d9e5216396349ca23c58d74f06aee" @@ -941,13 +820,6 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -970,19 +842,6 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== -ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" - integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - append-field@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" @@ -1085,36 +944,16 @@ arraybuffer.prototype.slice@^1.0.4: get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async-function@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== -async-hook-domain@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/async-hook-domain/-/async-hook-domain-2.0.4.tgz#5a24910982c04394ea33dd442860f80cce2d972c" - integrity sha512-14LjCmlK1PK8eDtTezR6WX8TMaYNIzBIsd2D1sGoGjgx0BuNMMoSdk7i/drlbtamy0AWv9yv2tkB+ASdmeqFIw== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -auto-bind@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-4.0.0.tgz#e3589fc6c2da8f7ca43ba9f84fa52a744fc997fb" - integrity sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ== - available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -1154,16 +993,6 @@ benchmark@^2.1.4: lodash "^4.17.4" platform "^1.3.3" -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bind-obj-methods@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bind-obj-methods/-/bind-obj-methods-3.0.0.tgz#65b66544d9d668d80dfefe2089dd347ad1dbcaed" - integrity sha512-nLEaaz3/sEzNSyPWRsN9HNsqwk1AUyECtGj+XwGdIi3xABnEqecvXtIJ0wehQXuuER5uZ/5fTs2usONgYjG+iw== - body-parser@^2.2.0, body-parser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.1.tgz#6df606b0eb0a6e3f783dde91dde182c24c82438c" @@ -1199,13 +1028,6 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -1297,21 +1119,7 @@ call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" -caller-callsite@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-4.1.0.tgz#3e33cb1d910e7b09332d59a3503b9af7462f7295" - integrity sha512-99nnnGlJexTc41xwQTr+mWl15OI5PPczUJzM4YRE7QjkefMKCXGa5gfQjCOuVrD+1TjI/fevIDHg2nz3iYN5Ig== - dependencies: - callsites "^3.1.0" - -caller-path@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-3.0.1.tgz#bc932ecec3f943e10c2f8922146e23b132f932e4" - integrity sha512-fhmztL4wURO/BzwJUJ4aVRdnKEFskPBbrJ8fNgl7XdUiD1ygzzlt+nhPgUBSRq2ciEVubo6x+W8vJQzm55QLLQ== - dependencies: - caller-callsite "^4.1.0" - -callsites@^3.0.0, callsites@^3.1.0: +callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -1331,35 +1139,6 @@ caniuse-lite@^1.0.30001759: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz#bdd1960fafedf8d5f04ff16e81460506ff9b798f" integrity sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw== -cardinal@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" - integrity sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw== - dependencies: - ansicolors "~0.3.2" - redeyed "~2.1.0" - -chai@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" - integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.1.0" - -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1378,28 +1157,6 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" - -chokidar@^3.3.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chokidar@^4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" @@ -1407,11 +1164,6 @@ chokidar@^4.0.1: dependencies: readdirp "^4.0.1" -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - ci-info@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa" @@ -1434,26 +1186,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-boxes@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -1463,15 +1195,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.4: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -1481,13 +1204,6 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -code-excerpt@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-3.0.0.tgz#fcfb6748c03dba8431c19f5474747fad3f250f10" - integrity sha512-VHNTVhd7KsLGOqfX3SyeO8RyYPMp1GJOg194VITk04WMYCv4plV68YWe6TJZxd9MhobjtpMRnVky01gqZsalaw== - dependencies: - convert-to-spaces "^1.0.1" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -1500,11 +1216,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1562,11 +1273,6 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -convert-to-spaces@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz#7e3e48bbe6d997b1417ddca2868204b4d3d85715" - integrity sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ== - cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" @@ -1603,11 +1309,6 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -csstype@^3.2.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" - integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== - data-view-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" @@ -1664,13 +1365,6 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -deep-eql@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" - integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== - dependencies: - type-detect "^4.0.0" - deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -1716,16 +1410,16 @@ depd@^2.0.0, depd@~2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -diff@^4.0.1, diff@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - diff@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== +diff@^8.0.2: + version "8.0.3" + resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.3.tgz#c7da3d9e0e8c283bb548681f8d7174653720c2d5" + integrity sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -1903,11 +1597,6 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -2118,7 +1807,7 @@ espree@^10.0.1, espree@^10.4.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^4.2.1" -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -2152,11 +1841,6 @@ etag@^1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -events-to-array@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.1.2.tgz#2d41f563e1fe400ed4962fe1a4d5c6a7539df7f6" - integrity sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA== - express@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" @@ -2226,13 +1910,6 @@ fill-keys@^1.0.2: is-object "~1.0.1" merge-descriptors "~1.0.0" -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - finalhandler@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" @@ -2275,11 +1952,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -findit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/findit/-/findit-2.0.0.tgz#6509f0126af4c178551cfa99394e032e13a4d56e" - integrity sha512-ENZS237/Hr8bjczn5eKuBohLgaD0JyUd0arxretR1f9RO46vZHA1b2y0VorgGV3WaOT3c+78P8h7v4JGJ1i/rg== - flat-cache@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" @@ -2318,7 +1990,7 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -foreground-child@^3.1.0: +foreground-child@^3.1.0, foreground-child@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -2352,31 +2024,16 @@ fromentries@^1.2.0: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== -fs-exists-cached@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-exists-cached/-/fs-exists-cached-1.0.0.tgz#cf25554ca050dc49ae6656b41de42258989dcbce" - integrity sha512-kSxoARUDn4F2RPXX48UXnaFKwVU7Ivd/6qpzZL29MCDmr9sTvybv4gFCp+qaI4fM9m0z9fgz/yJvi56GAz+BZg== - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -function-loop@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/function-loop/-/function-loop-2.0.1.tgz#799c56ced01698cf12a1b80e4802e9dafc2ebada" - integrity sha512-ktIR+O6i/4h+j/ZhZJNdzeI4i9lEPeEK6UPR2EVyTVBqOwcU3Za9xYKLH64ZR9HmcROyRrOkizNyjjtWJzDDkQ== - function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" @@ -2409,11 +2066,6 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.1, get-func-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" - integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== - get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" @@ -2466,13 +2118,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - glob@^10.4.5: version "10.5.0" resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" @@ -2485,7 +2130,7 @@ glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.0.5, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.3: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2642,10 +2287,10 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@^1.14.2: - version "1.15.0" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz#9e20827a322bbadaeb5e3bac49ea8f6d4685fdd8" - integrity sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA== +import-in-the-middle@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-2.0.0.tgz#295948cee94d0565314824c6bd75379d13e5b1a5" + integrity sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A== dependencies: acorn "^8.14.0" acorn-import-attributes "^1.9.5" @@ -2680,35 +2325,6 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.3, inherits@~2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ink@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ink/-/ink-3.2.0.tgz#434793630dc57d611c8fe8fffa1db6b56f1a16bb" - integrity sha512-firNp1q3xxTzoItj/eOOSZQnYSlyrWks5llCTVX37nJ59K3eXbQ8PtzCguqo8YI19EELo5QxaKnJd4VxzhU8tg== - dependencies: - ansi-escapes "^4.2.1" - auto-bind "4.0.0" - chalk "^4.1.0" - cli-boxes "^2.2.0" - cli-cursor "^3.1.0" - cli-truncate "^2.1.0" - code-excerpt "^3.0.0" - indent-string "^4.0.0" - is-ci "^2.0.0" - lodash "^4.17.20" - patch-console "^1.0.0" - react-devtools-core "^4.19.1" - react-reconciler "^0.26.2" - scheduler "^0.20.2" - signal-exit "^3.0.2" - slice-ansi "^3.0.0" - stack-utils "^2.0.2" - string-width "^4.2.2" - type-fest "^0.12.0" - widest-line "^3.1.0" - wrap-ansi "^6.2.0" - ws "^7.5.5" - yoga-layout-prebuilt "^1.9.6" - internal-slot@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" @@ -2750,13 +2366,6 @@ is-bigint@^1.1.0: dependencies: has-bigints "^1.0.2" -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-boolean-object@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" @@ -2782,13 +2391,6 @@ is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-core-module@^2.13.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" @@ -2841,7 +2443,7 @@ is-generator-function@^1.0.10: has-tostringtag "^1.0.2" safe-regex-test "^1.1.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -2866,11 +2468,6 @@ is-number-object@^1.1.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - is-object@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" @@ -3004,17 +2601,18 @@ istanbul-lib-hook@^3.0.0: dependencies: append-transform "^2.0.0" -istanbul-lib-instrument@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== +istanbul-lib-instrument@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" -istanbul-lib-processinfo@^2.0.2, istanbul-lib-processinfo@^2.0.3: +istanbul-lib-processinfo@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz#366d454cd0dcb7eb6e0e419378e60072c8626169" integrity sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg== @@ -3052,13 +2650,6 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jackspeak@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-1.4.2.tgz#30ad5e4b7b36f9f3ae580e23272b1a386b4f6b93" - integrity sha512-GHeGTmnuaHnvS+ZctRB01bfxARuu9wW83ENbuiweu07SFcVlZrJpcshSre/keGT7YGBhLHg/+rXCNSrsEHKU4Q== - dependencies: - cliui "^7.0.4" - jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -3068,7 +2659,7 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -3155,25 +2746,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libtap@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/libtap/-/libtap-1.4.1.tgz#6e2ba70ddc39c676c9f887333c354fab6d359613" - integrity sha512-S9v19shLTigoMn3c02V7LZ4t09zxmVP3r3RbEAwuHFYeKgF+ESFJxoQ0PMFKW4XdgQhcjVBEwDoopG6WROq/gw== - dependencies: - async-hook-domain "^2.0.4" - bind-obj-methods "^3.0.0" - diff "^4.0.2" - function-loop "^2.0.1" - minipass "^3.1.5" - own-or "^1.0.0" - own-or-env "^1.0.2" - signal-exit "^3.0.4" - stack-utils "^2.0.4" - tap-parser "^11.0.0" - tap-yaml "^1.0.0" - tcompare "^5.0.6" - trivial-deferred "^1.0.1" - lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -3205,7 +2777,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.4: +lodash@^4.17.15, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3218,20 +2790,6 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -loupe@^2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" - integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== - dependencies: - get-func-name "^2.0.1" - lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -3321,11 +2879,6 @@ mime-types@^3.0.0, mime-types@^3.0.1: dependencies: mime-db "^1.54.0" -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -3345,13 +2898,6 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^3.1.5, minipass@^3.1.6, minipass@^3.3.4: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" @@ -3364,11 +2910,6 @@ mkdirp@^0.5.6: dependencies: minimist "^1.2.6" -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - mkdirp@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" @@ -3430,7 +2971,7 @@ module-not-found-error@^1.0.1: resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0" integrity sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g== -ms@^2.1.1, ms@^2.1.2, ms@^2.1.3: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -3494,15 +3035,10 @@ node-releases@^2.0.27: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -nyc@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/nyc/-/nyc-15.1.0.tgz#1335dae12ddc87b6e249d5a1994ca4bdaea75f02" - integrity sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A== +nyc@^17.1.0: + version "17.1.0" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-17.1.0.tgz#b6349a401a62ffeb912bd38ea9a018839fdb6eb1" + integrity sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ== dependencies: "@istanbuljs/load-nyc-config" "^1.0.0" "@istanbuljs/schema" "^0.1.2" @@ -3511,12 +3047,12 @@ nyc@^15.1.0: decamelize "^1.2.0" find-cache-dir "^3.2.0" find-up "^4.1.0" - foreground-child "^2.0.0" + foreground-child "^3.3.0" get-package-type "^0.1.0" glob "^7.1.6" istanbul-lib-coverage "^3.0.0" istanbul-lib-hook "^3.0.0" - istanbul-lib-instrument "^4.0.0" + istanbul-lib-instrument "^6.0.2" istanbul-lib-processinfo "^2.0.2" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" @@ -3624,18 +3160,6 @@ once@^1.3.0, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -opener@^1.5.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - opentracing@>=0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" @@ -3662,18 +3186,6 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" -own-or-env@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/own-or-env/-/own-or-env-1.0.2.tgz#84e78d2d5128f7ee8a59f741ad5aafb4256a7c89" - integrity sha512-NQ7v0fliWtK7Lkb+WdFqe6ky9XAzYmlkXthQrBbzlYbmFKoAYbDDcwmOm6q8kOuwSRXW8bdL5ORksploUJmWgw== - dependencies: - own-or "^1.0.0" - -own-or@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/own-or/-/own-or-1.0.0.tgz#4e877fbeda9a2ec8000fbc0bcae39645ee8bf8dc" - integrity sha512-NfZr5+Tdf6MB8UI9GLvKRs4cXY8/yB0w3xtt84xFdWy8hkGjn+JFc60VhzS/hFRfbyxFcGYMTjnF4Me+RbbqrA== - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -3758,11 +3270,6 @@ parseurl@^1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -patch-console@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-1.0.0.tgz#19b9f028713feb8a3c023702a8cc8cb9f7466f9d" - integrity sha512-nxl9nrnLQmh64iTzMfyylSlRozL7kAXIaxw1fVcLYdyhNkJCRUzirRZTikXGJsg+hc4fqpneTK6iU2H1Q8THSA== - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -3796,21 +3303,11 @@ path-to-regexp@^8.0.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f" integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== - picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" @@ -3887,15 +3384,15 @@ proxyquire@^2.1.3: module-not-found-error "^1.0.1" resolve "^1.11.1" -punycode@^2.0.0, punycode@^2.1.0: +punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== qs@^6.14.0: - version "6.14.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" - integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + version "6.14.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159" + integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ== dependencies: side-channel "^1.1.0" @@ -3921,31 +3418,6 @@ raw-body@^3.0.1: iconv-lite "~0.7.0" unpipe "~1.0.0" -react-devtools-core@^4.19.1: - version "4.28.5" - resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.28.5.tgz#c8442b91f068cdf0c899c543907f7f27d79c2508" - integrity sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA== - dependencies: - shell-quote "^1.6.1" - ws "^7" - -react-reconciler@^0.26.2: - version "0.26.2" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.26.2.tgz#bbad0e2d1309423f76cf3c3309ac6c96e05e9d91" - integrity sha512-nK6kgY28HwrMNwDnMui3dvm3rCFjZrcGiuwLc5COUipBK5hWHLOxMJhSnSomirqWwjPBJKV1QcbkI0VJr7Gl1Q== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" - -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - readable-stream@^3.0.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -3973,20 +3445,6 @@ readdirp@^4.0.1: resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redeyed@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" - integrity sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ== - dependencies: - esprima "~4.0.0" - reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" @@ -4047,11 +3505,6 @@ reserved-identifiers@^1.0.0: resolved "https://registry.yarnpkg.com/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz#d2982cd698e317dd3dced1ee1c52412dbd64fc64" integrity sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw== -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -4076,14 +3529,6 @@ resolve@^1.11.1, resolve@^1.22.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" @@ -4150,20 +3595,12 @@ safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - semifies@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/semifies/-/semifies-1.0.0.tgz#b69569f32c2ba2ac04f705ea82831364289b2ae2" integrity sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw== -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -4265,11 +3702,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1: - version "1.8.3" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" - integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== - side-channel-list@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" @@ -4310,7 +3742,7 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -signal-exit@^3.0.2, signal-exit@^3.0.4, signal-exit@^3.0.6: +signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -4320,40 +3752,18 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -sinon-chai@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" - integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== - -sinon@^21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-21.0.0.tgz#dbda73abc7e6cb803fef3368cfbecbb5936e8a9e" - integrity sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw== +sinon@^21.0.1: + version "21.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-21.0.1.tgz#36b9126065a44906f7ba4a47b723b99315a8c356" + integrity sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw== dependencies: "@sinonjs/commons" "^3.0.1" - "@sinonjs/fake-timers" "^13.0.5" - "@sinonjs/samsam" "^8.0.1" - diff "^7.0.0" + "@sinonjs/fake-timers" "^15.1.0" + "@sinonjs/samsam" "^8.0.3" + diff "^8.0.2" supports-color "^7.2.0" -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -source-map-support@^0.5.16: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1: +source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -4403,13 +3813,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stack-utils@^2.0.2, stack-utils@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== - dependencies: - escape-string-regexp "^2.0.0" - statuses@^2.0.1, statuses@~2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" @@ -4428,8 +3831,16 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: - name string-width-cjs +"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" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4493,8 +3904,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: - name strip-ansi-cjs +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -4547,80 +3964,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tap-mocha-reporter@^5.0.3: - version "5.0.4" - resolved "https://registry.yarnpkg.com/tap-mocha-reporter/-/tap-mocha-reporter-5.0.4.tgz#666edb15c9b6b3ca7fe3af9908961d903f81c25b" - integrity sha512-J+YMO8B7lq1O6Zxd/jeuG27vJ+Y4tLiRMKPSb7KR6FVh86k3Rq1TwYc2GKPyIjCbzzdMdReh3Vfz9L5cg1Z2Bw== - dependencies: - color-support "^1.1.0" - debug "^4.1.1" - diff "^4.0.1" - escape-string-regexp "^2.0.0" - glob "^7.0.5" - tap-parser "^11.0.0" - tap-yaml "^1.0.0" - unicode-length "^2.0.2" - -tap-parser@^11.0.0, tap-parser@^11.0.2: - version "11.0.2" - resolved "https://registry.yarnpkg.com/tap-parser/-/tap-parser-11.0.2.tgz#5d3e76e2cc521e23a8c50201487b273ca0fba800" - integrity sha512-6qGlC956rcORw+fg7Fv1iCRAY8/bU9UabUAhs3mXRH6eRmVZcNPLheSXCYaVaYeSwx5xa/1HXZb1537YSvwDZg== - dependencies: - events-to-array "^1.0.1" - minipass "^3.1.6" - tap-yaml "^1.0.0" - -tap-yaml@^1.0.0, tap-yaml@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tap-yaml/-/tap-yaml-1.0.2.tgz#62032a459e5524e10661c19ee9df5d33d78812fa" - integrity sha512-GegASpuqBnRNdT1U+yuUPZ8rEU64pL35WPBpCISWwff4dErS2/438barz7WFJl4Nzh3Y05tfPidZnH+GaV1wMg== - dependencies: - yaml "^1.10.2" - -tap@^16.3.10: - version "16.3.10" - resolved "https://registry.yarnpkg.com/tap/-/tap-16.3.10.tgz#cf246b56af02d34bdfffa576636a9e34b55aaa5f" - integrity sha512-q5Am+PpGHS6JSjk/Zn4bCRBihmZVM15v/MYXUy60wenw5HDe7pVrevLCEoMEz7tuw6jaPOJJqni1y8apN23IGw== - dependencies: - "@isaacs/import-jsx" "^4.0.1" - "@types/react" "^17.0.52" - chokidar "^3.3.0" - findit "^2.0.0" - foreground-child "^2.0.0" - fs-exists-cached "^1.0.0" - glob "^7.2.3" - ink "^3.2.0" - isexe "^2.0.0" - istanbul-lib-processinfo "^2.0.3" - jackspeak "^1.4.2" - libtap "^1.4.0" - minipass "^3.3.4" - mkdirp "^1.0.4" - nyc "^15.1.0" - opener "^1.5.1" - react "^17.0.2" - rimraf "^3.0.0" - signal-exit "^3.0.6" - source-map-support "^0.5.16" - tap-mocha-reporter "^5.0.3" - tap-parser "^11.0.2" - tap-yaml "^1.0.2" - tcompare "^5.0.7" - treport "^3.0.4" - which "^2.0.2" - tapable@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== -tcompare@^5.0.6, tcompare@^5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/tcompare/-/tcompare-5.0.7.tgz#8c2d647208031ed5cac5e573428149e16f795bbf" - integrity sha512-d9iddt6YYGgyxJw5bjsN7UJUO1kGOtjSlNy/4PoGYAjQS5pAT/hzIoLf1bZCw+uUxRmZJh7Yy1aA7xKVRT9B4w== - dependencies: - diff "^4.0.2" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -4635,13 +3983,6 @@ tiktoken@^1.0.21: resolved "https://registry.yarnpkg.com/tiktoken/-/tiktoken-1.0.22.tgz#a6c674839228bb88f32dfe646dff47193762f7d3" integrity sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA== -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - to-valid-identifier@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz#27337955333c3c517feb60bea533cf27ce54b79f" @@ -4660,25 +4001,6 @@ toidentifier@~1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -treport@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/treport/-/treport-3.0.4.tgz#05247fa7820ad3afe92355e4cf08fe41a933084b" - integrity sha512-zUw1sfJypuoZi0I54woo6CNsfvMrv+OwLBD0/wc4LhMW8MA0MbSE+4fNObn22JSR8x9lOYccuAzfBfZ2IemzoQ== - dependencies: - "@isaacs/import-jsx" "^4.0.1" - cardinal "^2.1.1" - chalk "^3.0.0" - ink "^3.2.0" - ms "^2.1.2" - tap-parser "^11.0.0" - tap-yaml "^1.0.0" - unicode-length "^2.0.2" - -trivial-deferred@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.1.2.tgz#6b07aa1eb045f6128b8b30673b040f99bfe64a2e" - integrity sha512-vDPiDBC3hyP6O4JrJYMImW3nl3c03Tsj9fEXc7Qc/XKa1O7gf5ZtFfIR/E0dun9SnDHdwjna1Z2rSzYgqpxh/g== - 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" @@ -4713,21 +4035,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-detect@^4.0.0, type-detect@^4.1.0: +type-detect@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== -type-fest@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.12.0.tgz#f57a27ab81c68d136a51fd71467eff94157fa1ee" - integrity sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - type-fest@^0.8.0: version "0.8.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" @@ -4827,18 +4139,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" - integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== - -unicode-length@^2.0.2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-length/-/unicode-length-2.1.0.tgz#425202b99f21854f5ca3530cc2a08dc262ce619f" - integrity sha512-4bV582zTV9Q02RXBxSUMiuN/KHo5w4aTojuKTNT96DIKps/SIawFp7cS5Mu25VuY1AioGXrmYyzKZUzh8OqoUw== - dependencies: - punycode "^2.0.0" - universal-github-app-jwt@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz#38537e5a7d154085a35f97601a5e30e9e17717df" @@ -4942,20 +4242,13 @@ which-typed-array@^1.1.16, which-typed-array@^1.1.19: gopd "^1.2.0" has-tostringtag "^1.0.2" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -4971,8 +4264,7 @@ workerpool@^9.2.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -4990,6 +4282,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -5014,11 +4315,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7, ws@^7.5.5: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" @@ -5044,16 +4340,6 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - yaml@^2.8.0: version "2.8.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5" @@ -5126,10 +4412,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yoga-layout-prebuilt@^1.9.6: - version "1.10.0" - resolved "https://registry.yarnpkg.com/yoga-layout-prebuilt/-/yoga-layout-prebuilt-1.10.0.tgz#2936fbaf4b3628ee0b3e3b1df44936d6c146faa6" - integrity sha512-YnOmtSbv4MTf7RGJMK0FvZ+KD8OEe/J5BNnR0GHhD8J/XcG/Qvxgszm0Un6FTHWW4uHlTgP0IztiXQnGyIR45g== - dependencies: - "@types/yoga-layout" "1.9.2"