diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index fd409251590..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,12 +0,0 @@ -coverage -dist -docs -out -node_modules -versions -acmeair-nodejs -vendor -integration-tests/esbuild/out.js -integration-tests/esbuild/aws-sdk-out.js -packages/dd-trace/src/appsec/blocked_templates.js -packages/dd-trace/src/payload-tagging/jsonpath-plus.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 13031ec7db1..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 2021 - }, - "extends": [ - "eslint:recommended", - "standard", - "plugin:mocha/recommended" - ], - "plugins": [ - "mocha", - "n" - ], - "env": { - "node": true, - "es2021": true - }, - "settings": { - "node": { - "version": ">=16.0.0" - } - }, - "rules": { - "max-len": [2, 120, 2], - "no-var": 2, - "no-console": 2, - "prefer-const": 2, - "object-curly-spacing": [2, "always"], - "import/no-extraneous-dependencies": 2, - "standard/no-callback-literal": 0, - "no-prototype-builtins": 0, - "mocha/no-mocha-arrows": 0, - "mocha/no-setup-in-describe": 0, - "mocha/no-sibling-hooks": 0, - "mocha/no-top-level-hooks": 0, - "mocha/max-top-level-suites": 0, - "mocha/no-identical-title": 0, - "mocha/no-global-tests": 0, - "mocha/no-exports": 0, - "mocha/no-skipped-tests": 0, - "n/no-restricted-require": [2, ["diagnostics_channel"]], - "n/no-callback-literal": 0, - "object-curly-newline": ["error", {"multiline": true, "consistent": true }], - "import/no-absolute-path": 0 - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..8fb53ba14fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,64 @@ +name: "Bug Report (Low Priority)" +description: "Create a public Bug Report. Note that these may not be addressed as quickly as the helpdesk and that looking up account information will be difficult." +title: "[BUG]: " +labels: bug +body: + - type: input + attributes: + label: Tracer Version(s) + description: "Version(s) of the tracer affected by this bug" + placeholder: 1.2.3, 4.5.6 + validations: + required: true + + - type: input + attributes: + label: Node.js Version(s) + description: "Version(s) of Node.js (`node --version`) that you've encountered this bug with" + placeholder: 20.1.1 + validations: + required: true + + - type: textarea + attributes: + label: Bug Report + description: Please add a clear and concise description of the bug here + validations: + required: true + + - type: textarea + attributes: + label: Reproduction Code + description: Please add code here to help us reproduce the problem + validations: + required: false + + - type: textarea + attributes: + label: Error Logs + description: "Please provide any error logs from the tracer (`DD_TRACE_DEBUG=true` can help)" + validations: + required: false + + - type: input + attributes: + label: Operating System + description: "Provide your operating system and version (e.g. `uname -a`)" + placeholder: Darwin Kernel Version 23.6.0 + validations: + required: false + + - type: dropdown + attributes: + label: Bundling + description: "How is your application being bundled" + options: + - Unsure + - No Bundling + - ESBuild + - Webpack + - Next.js + - Vite + - Rollup + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b5a5eb1d199..5f822733ea5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,9 +1,8 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - - name: Bug Report + - name: Bug Report (High Priority) url: https://help.datadoghq.com/hc/en-us/requests/new?tf_1260824651490=pt_product_type:apm&tf_1900004146284=pt_apm_language:node - about: This option creates an expedited Bug Report via the helpdesk (no login required). This will allow us to look up your account and allows you to provide additional information in private. Please do not create a GitHub issue to report a bug. - - name: Feature Request + about: Create an expedited Bug Report via the helpdesk (no login required). This will allow us to look up your account and allows you to provide additional information in private. Please do not create a GitHub issue to report a bug. + - name: Feature Request (High Priority) url: https://help.datadoghq.com/hc/en-us/requests/new?tf_1260824651490=pt_product_type:apm&tf_1900004146284=pt_apm_language:node&tf_1260825272270=pt_apm_category_feature_request - about: This option creates an expedited Feature Request via the helpdesk (no login required). This helps with prioritization and allows you to provide additional information in private. Please do not create a GitHub issue to request a feature. - + about: Create an expedited Feature Request via the helpdesk (no login required). This helps with prioritization and allows you to provide additional information in private. Please do not create a GitHub issue to request a feature. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000000..9d26ea1dd33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,50 @@ +name: Feature Request (Low Priority) +description: Create a public Feature Request. Note that these may not be addressed as quickly as the helpdesk and that looking up account information will be difficult. +title: "[FEATURE]: " +labels: feature-request +body: + - type: input + attributes: + label: Package Name + description: "If your feature request is to add instrumentation support for an npm package please provide the name here" + placeholder: left-pad + validations: + required: false + + - type: input + attributes: + label: Package Version(s) + description: "If your feature request is to add instrumentation support for an npm package please provide the version you use" + placeholder: 1.2.3 + validations: + required: false + + - type: textarea + attributes: + label: Describe the feature you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Is your feature request related to a problem? + description: | + Please add a clear and concise description of your problem. + E.g. I'm unable to instrument my database queries... + validations: + required: false + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here + validations: + required: false diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index 66990a1147f..17a4e66f15c 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -205,11 +205,12 @@ jobs: next: strategy: + fail-fast: false matrix: version: - 18 - latest - range: ['9.5.0', '11.1.4', '13.2.0', '14.2.6'] + range: ['>=10.2.0 <11', '>=11.0.0 <13', '11.1.4', '>=13.0.0 <14', '13.2.0', '>=14.0.0 <=14.2.6', '>=14.2.7 <15', '>=15.0.0'] runs-on: ubuntu-latest env: PLUGINS: next @@ -264,3 +265,17 @@ jobs: - uses: ./.github/actions/node/latest - run: yarn test:appsec:plugins:ci - uses: codecov/codecov-action@v3 + + template: + runs-on: ubuntu-latest + env: + PLUGINS: handlebars|pug + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/oldest + - run: yarn test:appsec:plugins:ci + - uses: ./.github/actions/node/latest + - run: yarn test:appsec:plugins:ci + - uses: codecov/codecov-action@v3 diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 0e067a98fb5..4822539ecab 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -15,54 +15,30 @@ concurrency: jobs: - aerospike-node-16: - runs-on: ubuntu-latest - services: - aerospike: - image: aerospike:ce-5.7.0.15 - ports: - - "127.0.0.1:3000-3002:3000-3002" - env: - PLUGINS: aerospike - SERVICES: aerospike - PACKAGE_VERSION_RANGE: '>=4.0.0 <5.2.0' - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - id: pkg - run: | - content=`cat ./package.json | tr '\n' ' '` - echo "json=$content" >> $GITHUB_OUTPUT - - id: extract - run: | - version="${{fromJson(steps.pkg.outputs.json).version}}" - majorVersion=$(echo "$version" | cut -d '.' -f 1) - echo "Major Version: $majorVersion" - echo "MAJOR_VERSION=$majorVersion" >> $GITHUB_ENV - - uses: ./.github/actions/node/oldest - - name: Install dependencies - if: env.MAJOR_VERSION == '4' - uses: ./.github/actions/install - - name: Run tests - if: env.MAJOR_VERSION == '4' - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 - - aerospike-node-18-20: + aerospike: strategy: matrix: - node-version: [18] - range: ['5.2.0 - 5.7.0'] + node-version: [16] + range: ['>=4.0.0 <5.2.0'] + aerospike-image: [ce-5.7.0.15] + test-image: [ubuntu-22.04] include: + - node-version: 18 + range: '>=5.2.0' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest - node-version: 20 - range: '>=5.8.0' - runs-on: ubuntu-latest + range: '>=5.5.0' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest + - node-version: 22 + range: '>=5.12.1' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest + runs-on: ${{ matrix.test-image }} services: aerospike: - image: aerospike:ce-6.4.0.3 + image: aerospike:${{ matrix.aerospike-image }} ports: - "127.0.0.1:3000-3002:3000-3002" env: @@ -73,24 +49,13 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/setup - - id: pkg - run: | - content=`cat ./package.json | tr '\n' ' '` - echo "json=$content" >> $GITHUB_OUTPUT - - id: extract - run: | - version="${{fromJson(steps.pkg.outputs.json).version}}" - majorVersion=$(echo "$version" | cut -d '.' -f 1) - echo "Major Version: $majorVersion" - echo "MAJOR_VERSION=$majorVersion" >> $GITHUB_ENV - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + - run: yarn config set ignore-engines true - name: Install dependencies - if: env.MAJOR_VERSION == '5' uses: ./.github/actions/install - name: Run tests - if: env.MAJOR_VERSION == '5' run: yarn test:plugins:ci - if: always() uses: ./.github/actions/testagent/logs @@ -294,6 +259,7 @@ jobs: PLUGINS: couchbase SERVICES: couchbase PACKAGE_VERSION_RANGE: ${{ matrix.range }} + DD_INJECT_FORCE: 'true' steps: - uses: actions/checkout@v4 - uses: ./.github/actions/testagent/start @@ -561,6 +527,25 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/plugins/test-and-upstream + langchain: + runs-on: ubuntu-latest + env: + PLUGINS: langchain + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/18 # langchain doesn't support Node 16 + - run: yarn test:plugins:ci + shell: bash + - uses: ./.github/actions/node/latest + - run: yarn test:plugins:ci + shell: bash + - uses: codecov/codecov-action@v3 + - if: always() + uses: ./.github/actions/testagent/logs + limitd-client: runs-on: ubuntu-latest services: @@ -734,11 +719,12 @@ jobs: # TODO: fix performance issues and test more Node versions next: strategy: + fail-fast: false matrix: version: - 18 - latest - range: ['9.5.0', '11.1.4', '13.2.0', '14.2.6'] + range: ['>=10.2.0 <11', '>=11.0.0 <13', '11.1.4', '>=13.0.0 <14', '13.2.0', '>=14.0.0 <=14.2.6', '>=14.2.7 <15', '>=15.0.0'] runs-on: ubuntu-latest env: PLUGINS: next @@ -782,7 +768,11 @@ jobs: # TODO: Figure out why nyc stopped working with EACCESS errors. oracledb: runs-on: ubuntu-latest - container: bengl/node-12-with-oracle-client + container: + image: bengl/node-12-with-oracle-client + volumes: + - /node20217:/node20217:rw,rshared + - /node20217:/__e/node20:ro,rshared services: oracledb: image: gvenzl/oracle-xe:18-slim @@ -804,9 +794,15 @@ jobs: PLUGINS: oracledb SERVICES: oracledb DD_TEST_AGENT_URL: http://testagent:9126 + DD_INJECT_FORCE: 'true' # Needed to fix issue with `actions/checkout@v3: https://github.com/actions/checkout/issues/1590 ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: + # https://github.com/actions/runner/issues/2906#issuecomment-2109514798 + - name: Install Node for runner (with glibc 2.17 compatibility) + run: | + curl -LO https://unofficial-builds.nodejs.org/download/release/v20.9.0/node-v20.9.0-linux-x64-glibc-217.tar.xz + tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 38c43297947..3dd8475811e 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -18,7 +18,7 @@ jobs: # setting fail-fast to false in an attempt to prevent this from happening fail-fast: false matrix: - version: [18, 20, latest] + version: [18, 20, 22, latest] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -34,7 +34,7 @@ jobs: integration-guardrails: strategy: matrix: - version: [12, 14, 16] + version: [12, 14.0.0, 14, 16.0.0, 16, 18.0.0, 18.1.0, 20.0.0, 22.0.0] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -47,16 +47,17 @@ jobs: integration-guardrails-unsupported: strategy: matrix: - version: ['0.8', '0.10', '0.12', '4', '6', '8', '10'] + version: ['0.8', '0.10', '0.12', '4', '6', '8', '10', '12.0.0'] runs-on: ubuntu-latest - env: - DD_INJECTION_ENABLED: 'true' steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.version }} - run: node ./init + - run: node ./init + env: + DD_INJECTION_ENABLED: 'true' integration-ci: strategy: @@ -161,3 +162,10 @@ jobs: - run: yarn type:test - run: yarn type:doc + verify-yaml: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - run: node scripts/verify-ci-config.js diff --git a/.github/workflows/release-4.yml b/.github/workflows/release-4.yml index 169450d6cf2..9c60613455a 100644 --- a/.github/workflows/release-4.yml +++ b/.github/workflows/release-4.yml @@ -16,7 +16,9 @@ jobs: permissions: id-token: write contents: write + pull-requests: read env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v4 @@ -31,3 +33,4 @@ jobs: - run: | git tag v${{ fromJson(steps.pkg.outputs.json).version }} git push origin v${{ fromJson(steps.pkg.outputs.json).version }} + - run: node scripts/release/notes diff --git a/.github/workflows/release-latest.yml b/.github/workflows/release-latest.yml index 6fa92f3ee23..8d89efc1680 100644 --- a/.github/workflows/release-latest.yml +++ b/.github/workflows/release-latest.yml @@ -16,7 +16,9 @@ jobs: permissions: id-token: write contents: write + pull-requests: read env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} outputs: pkgjson: ${{ steps.pkg.outputs.json }} @@ -33,6 +35,7 @@ jobs: - run: | git tag v${{ fromJson(steps.pkg.outputs.json).version }} git push origin v${{ fromJson(steps.pkg.outputs.json).version }} + - run: node scripts/release/notes --latest docs: runs-on: ubuntu-latest diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index 0a7d4094b8b..f566ac729dd 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -26,21 +26,21 @@ jobs: name: system_tests_binaries path: ./binaries/**/* - get-essential-scenarios: + get-scenarios: name: Get parameters uses: DataDog/system-tests/.github/workflows/compute-workflow-parameters.yml@main with: library: nodejs - scenarios_groups: essentials + scenarios_groups: essentials,appsec_rasp system-tests: runs-on: ${{ contains(fromJSON('["CROSSED_TRACING_LIBRARIES", "INTEGRATIONS"]'), matrix.scenario) && 'ubuntu-latest-16-cores' || 'ubuntu-latest' }} needs: - - get-essential-scenarios + - get-scenarios strategy: matrix: - weblog-variant: ${{fromJson(needs.get-essential-scenarios.outputs.endtoend_weblogs)}} - scenario: ${{fromJson(needs.get-essential-scenarios.outputs.endtoend_scenarios)}} + weblog-variant: ${{fromJson(needs.get-scenarios.outputs.endtoend_weblogs)}} + scenario: ${{fromJson(needs.get-scenarios.outputs.endtoend_scenarios)}} env: TEST_LIBRARY: nodejs diff --git a/.gitignore b/.gitignore index 773f16d5a90..a8dcafe063b 100644 --- a/.gitignore +++ b/.gitignore @@ -106,7 +106,6 @@ typings/ # End of https://www.gitignore.io/api/node,macos,visualstudiocode -.github/notes .next package-lock.json out diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6da75a763ac..dcf8a6c7772 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,8 +22,9 @@ onboarding_tests_installer: SCENARIO: [ SIMPLE_INSTALLER_AUTO_INJECTION, SIMPLE_AUTO_INJECTION_PROFILING ] onboarding_tests_k8s_injection: - variables: - WEBLOG_VARIANT: sample-app + parallel: + matrix: + - WEBLOG_VARIANT: sample-app requirements_json_test: rules: diff --git a/.gitlab/benchmarks.yml b/.gitlab/benchmarks.yml index 57eba976441..7461f88b98c 100644 --- a/.gitlab/benchmarks.yml +++ b/.gitlab/benchmarks.yml @@ -65,6 +65,18 @@ benchmark: GROUP: 2 - MAJOR_VERSION: 18 GROUP: 3 + - MAJOR_VERSION: 20 + GROUP: 1 + - MAJOR_VERSION: 20 + GROUP: 2 + - MAJOR_VERSION: 20 + GROUP: 3 + - MAJOR_VERSION: 22 + GROUP: 1 + - MAJOR_VERSION: 22 + GROUP: 2 + - MAJOR_VERSION: 22 + GROUP: 3 variables: SPLITS: 3 diff --git a/.gitlab/requirements_block.json b/.gitlab/requirements_block.json index e728f802915..ba32e598e3f 100644 --- a/.gitlab/requirements_block.json +++ b/.gitlab/requirements_block.json @@ -6,6 +6,9 @@ {"name": "unsupported 2.x.x glibc x64","filepath": "/some/path", "args": [], "envars": [], "host": {"os": "linux", "arch": "arm64", "libc": "glibc:2.16.9"}}, {"name": "unsupported 2.x.x glibc x86","filepath": "/some/path", "args": [], "envars": [], "host": {"os": "linux", "arch": "x86", "libc": "glibc:2.17"}}, {"name": "npm","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/npm-cli.js"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}}, + {"name": "npm-symlink","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/npm"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}}, {"name": "yarn","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/yarn.js"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}}, - {"name": "pnpm","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/pnpm.cjs"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}} + {"name": "yarn-symlink","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/yarn"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}}, + {"name": "pnpm","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/pnpm.cjs"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}}, + {"name": "pnpm-symlink","filepath": "/pathto/node", "args": ["/pathto/node", "/pathto/pnpm"], "envars": [], "host": {"os": "linux", "arch": "x64", "libc": "glibc:2.40"}} ] diff --git a/CODEOWNERS b/CODEOWNERS index 3b45215923f..52963649952 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -56,7 +56,9 @@ /packages/dd-trace/src/llmobs/ @DataDog/ml-observability /packages/dd-trace/test/llmobs/ @DataDog/ml-observability /packages/datadog-plugin-openai/ @DataDog/ml-observability +/packages/datadog-plugin-langchain/ @DataDog/ml-observability /packages/datadog-instrumentations/src/openai.js @DataDog/ml-observability +/packages/datadog-instrumentations/src/langchain.js @DataDog/ml-observability # CI /.github/workflows/appsec.yml @DataDog/asm-js diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 772cd9b2553..4ba4775b73c 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -8,11 +8,11 @@ require,@datadog/pprof,Apache license 2.0,Copyright 2019 Google Inc. require,@datadog/sketches-js,Apache license 2.0,Copyright 2020 Datadog Inc. require,@opentelemetry/api,Apache license 2.0,Copyright OpenTelemetry Authors require,@opentelemetry/core,Apache license 2.0,Copyright OpenTelemetry Authors +require,@isaacs/ttlcache,ISC,Copyright (c) 2022-2023 - Isaac Z. Schlueter and Contributors require,crypto-randomuuid,MIT,Copyright 2021 Node.js Foundation and contributors require,dc-polyfill,MIT,Copyright 2023 Datadog Inc. require,ignore,MIT,Copyright 2013 Kael Zhang and contributors require,import-in-the-middle,Apache license 2.0,Copyright 2021 Datadog Inc. -require,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki require,istanbul-lib-coverage,BSD-3-Clause,Copyright 2012-2015 Yahoo! Inc. require,jest-docblock,MIT,Copyright Meta Platforms, Inc. and affiliates. require,koalas,MIT,Copyright 2013-2017 Brian Woodward @@ -20,7 +20,6 @@ require,limiter,MIT,Copyright 2011 John Hurliman require,lodash.sortby,MIT,Copyright JS Foundation and other contributors require,lru-cache,ISC,Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors require,module-details-from-path,MIT,Copyright 2016 Thomas Watson Steen -require,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki require,opentracing,MIT,Copyright 2016 Resonance Labs Inc require,path-to-regexp,MIT,Copyright 2014 Blake Embrey require,pprof-format,MIT,Copyright 2022 Stephen Belanger @@ -30,8 +29,12 @@ require,retry,MIT,Copyright 2011 Tim Koschützki Felix Geisendörfer require,rfdc,MIT,Copyright 2019 David Mark Clements require,semver,ISC,Copyright Isaac Z. Schlueter and Contributors require,shell-quote,mit,Copyright (c) 2013 James Halliday +require,source-map,BSD-3-Clause,Copyright (c) 2009-2011, Mozilla Foundation and contributors dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) dev,@types/node,MIT,Copyright Authors +dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, +dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, +dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, dev,autocannon,MIT,Copyright 2016 Matteo Collina dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. dev,axios,MIT,Copyright 2014-present Matt Zabriskie @@ -52,11 +55,14 @@ dev,eslint-plugin-promise,ISC,jden and other contributors dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson dev,get-port,MIT,Copyright Sindre Sorhus dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors +dev,globals,MIT,Copyright (c) Sindre Sorhus (https://sindresorhus.com) dev,graphql,MIT,Copyright 2015 Facebook Inc. +dev,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors dev,knex,MIT,Copyright (c) 2013-present Tim Griesser dev,mkdirp,MIT,Copyright 2010 James Halliday dev,mocha,MIT,Copyright 2011-2018 JS Foundation and contributors https://js.foundation +dev,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki dev,multer,MIT,Copyright 2014 Hage Yaapa dev,nock,MIT,Copyright 2017 Pedro Teixeira and other contributors dev,nyc,ISC,Copyright 2015 Contributors @@ -66,6 +72,7 @@ dev,sinon,BSD-3-Clause,Copyright 2010-2017 Christian Johansen dev,sinon-chai,WTFPL and BSD-2-Clause,Copyright 2004 Sam Hocevar 2012–2017 Domenic Denicola dev,tap,ISC,Copyright 2011-2022 Isaac Z. Schlueter and Contributors dev,tiktoken,MIT,Copyright (c) 2022 OpenAI, Shantanu Jain +dev,yaml,ISC,Copyright Eemeli Aro file,aws-lambda-nodejs-runtime-interface-client,Apache 2.0,Copyright 2019 Amazon.com Inc. or its affiliates. All Rights Reserved. file,profile.proto,Apache license 2.0,Copyright 2016 Google Inc. file,is-git-url,MIT,Copyright (c) 2017 Jon Schlinkert. diff --git a/benchmark/profiler/index.js b/benchmark/profiler/index.js deleted file mode 100644 index 20f1455d05d..00000000000 --- a/benchmark/profiler/index.js +++ /dev/null @@ -1,230 +0,0 @@ -'use strict' - -/* eslint-disable no-console */ - -const autocannon = require('autocannon') -const axios = require('axios') -const chalk = require('chalk') -const getPort = require('get-port') -const Table = require('cli-table3') -const URL = require('url').URL -const { spawn } = require('child_process') - -main() - -async function main () { - try { - const disabled = await run(false) - const enabled = await run(true) - - compare(disabled, enabled) - } catch (e) { - console.error(e) - process.exit(1) - } -} - -async function run (profilerEnabled) { - const port = await getPort() - const url = new URL(`http://localhost:${port}/hello`) - const server = await createServer(profilerEnabled, url) - - title(`Benchmark (enabled=${profilerEnabled})`) - - await getUsage(url) - - const net = await benchmark(url.href, 15000) - const cpu = await getUsage(url) - - server.kill('SIGINT') - - return { cpu, net } -} - -function benchmark (url, maxConnectionRequests) { - return new Promise((resolve, reject) => { - const duration = maxConnectionRequests * 2 / 1000 - const instance = autocannon({ duration, maxConnectionRequests, url }, (err, result) => { - err ? reject(err) : resolve(result) - }) - - process.once('SIGINT', () => { - instance.stop() - }) - - autocannon.track(instance, { - renderResultsTable: true, - renderProgressBar: false - }) - }) -} - -function compare (result1, result2) { - title('Comparison (disabled VS enabled)') - - compareNet(result1.net, result2.net) - compareCpu(result1.cpu, result2.cpu) -} - -function compareNet (result1, result2) { - const shortLatency = new Table({ - head: asColor(chalk.cyan, ['Stat', '2.5%', '50%', '97.5%', '99%', 'Avg', 'Max']) - }) - - shortLatency.push(asLowRow(chalk.bold('Latency'), asDiff(result1.latency, result2.latency))) - - console.log(shortLatency.toString()) - - const requests = new Table({ - head: asColor(chalk.cyan, ['Stat', '1%', '2.5%', '50%', '97.5%', 'Avg', 'Min']) - }) - - requests.push(asHighRow(chalk.bold('Req/Sec'), asDiff(result1.requests, result2.requests, true))) - requests.push(asHighRow(chalk.bold('Bytes/Sec'), asDiff(result1.throughput, result2.throughput, true))) - - console.log(requests.toString()) -} - -function compareCpu (result1, result2) { - const cpuTime = new Table({ - head: asColor(chalk.cyan, ['Stat', 'User', 'System', 'Process']) - }) - - cpuTime.push(asTimeRow(chalk.bold('CPU Time'), asDiff(result1, result2))) - - console.log(cpuTime.toString()) -} - -function waitOn ({ interval = 250, timeout, resources }) { - return Promise.all(resources.map(resource => { - return new Promise((resolve, reject) => { - let intervalTimer - const timeoutTimer = timeout && setTimeout(() => { - reject(new Error('Timeout.')) - clearTimeout(timeoutTimer) - clearTimeout(intervalTimer) - }, timeout) - - function waitOnResource () { - if (timeout && !timeoutTimer) return - - axios.get(resource) - .then(() => { - resolve() - clearTimeout(timeoutTimer) - clearTimeout(intervalTimer) - }) - .catch(() => { - intervalTimer = setTimeout(waitOnResource, interval) - }) - } - - waitOnResource() - }) - })) -} - -async function createServer (profilerEnabled, url) { - const server = spawn(process.execPath, ['server'], { - cwd: __dirname, - env: { - DD_PROFILING_ENABLED: String(profilerEnabled), - PORT: url.port - } - }) - - process.once('SIGINT', () => { - server.kill('SIGINT') - }) - - await waitOn({ - timeout: 5000, - resources: [url.href] - }) - - return server -} - -async function getUsage (url) { - const response = await axios.get(`${url.origin}/usage`) - const usage = response.data - - usage.process = usage.user + usage.system - - return usage -} - -function asColor (colorise, row) { - return row.map((entry) => colorise(entry)) -} - -function asDiff (stat1, stat2, reverse = false) { - const result = Object.create(null) - - Object.keys(stat1).forEach((k) => { - if (stat2[k] === stat1[k]) return (result[k] = '0%') - if (stat1[k] === 0) return (result[k] = '+∞%') - if (stat2[k] === 0) return (result[k] = '-∞%') - - const fraction = stat2[k] / stat1[k] - const percent = Math.round(fraction * 100) - 100 - const value = `${withSign(percent)}%` - - if (percent > 0) { - result[k] = reverse ? chalk.green(value) : chalk.red(value) - } else if (percent < 0) { - result[k] = reverse ? chalk.red(value) : chalk.green(value) - } else { - result[k] = value - } - }) - - return result -} - -function asLowRow (name, stat) { - return [ - name, - stat.p2_5, - stat.p50, - stat.p97_5, - stat.p99, - stat.average, - typeof stat.max === 'string' ? stat.max : Math.floor(stat.max * 100) / 100 - ] -} - -function asHighRow (name, stat) { - return [ - name, - stat.p1, - stat.p2_5, - stat.p50, - stat.p97_5, - stat.average, - typeof stat.min === 'string' ? stat.min : Math.floor(stat.min * 100) / 100 - ] -} - -function asTimeRow (name, stat) { - return [ - name, - stat.user, - stat.system, - stat.process - ] -} - -function withSign (value) { - return value < 0 ? `${value}` : `+${value}` -} - -function title (str) { - const line = ''.padStart(str.length, '=') - - console.log('') - console.log(line) - console.log(str) - console.log(line) - console.log('') -} diff --git a/benchmark/profiler/server.js b/benchmark/profiler/server.js deleted file mode 100644 index cf190e40eed..00000000000 --- a/benchmark/profiler/server.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -require('dotenv').config() -require('../..').init({ enabled: false }) - -const express = require('express') - -const app = express() - -let usage - -app.get('/hello', (req, res) => { - res.status(200).send('Hello World!') -}) - -app.get('/usage', (req, res) => { - const diff = process.cpuUsage(usage) - - usage = process.cpuUsage() - - res.status(200).send(diff) -}) - -app.listen(process.env.PORT || 8080, '127.0.0.1', () => { - usage = process.cpuUsage() -}) diff --git a/benchmark/sirun/.gitignore b/benchmark/sirun/.gitignore index bc111ce710b..6b557f5a398 100644 --- a/benchmark/sirun/.gitignore +++ b/benchmark/sirun/.gitignore @@ -1,2 +1,3 @@ *.ndjson meta-temp.json +summary.json diff --git a/benchmark/sirun/Dockerfile b/benchmark/sirun/Dockerfile index 6ce6d8557fe..ad27d5d71b1 100644 --- a/benchmark/sirun/Dockerfile +++ b/benchmark/sirun/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ git hwinfo jq procps \ software-properties-common build-essential libnss3-dev zlib1g-dev libgdbm-dev libncurses5-dev libssl-dev libffi-dev libreadline-dev libsqlite3-dev libbz2-dev -RUN git clone --depth 1 https://github.com/pyenv/pyenv.git --branch "v2.0.4" --single-branch /pyenv +RUN git clone --depth 1 https://github.com/pyenv/pyenv.git --branch "v2.4.1" --single-branch /pyenv ENV PYENV_ROOT "/pyenv" ENV PATH "/pyenv/shims:/pyenv/bin:$PATH" RUN eval "$(pyenv init -)" @@ -34,6 +34,7 @@ RUN mkdir -p /usr/local/nvm \ && nvm install --no-progress 16.20.1 \ && nvm install --no-progress 18.16.1 \ && nvm install --no-progress 20.4.0 \ + && nvm install --no-progress 22.10.0 \ && nvm alias default 18 \ && nvm use 18 diff --git a/benchmark/sirun/appsec-iast/README.md b/benchmark/sirun/appsec-iast/README.md index 79c5e0d21ab..728ed535fb3 100644 --- a/benchmark/sirun/appsec-iast/README.md +++ b/benchmark/sirun/appsec-iast/README.md @@ -1,4 +1,4 @@ -This creates 150 HTTP requests from client to server. +This benchmarks HTTP requests from client to server. The variants are: - control tracer with non vulnerable endpoint without iast diff --git a/benchmark/sirun/appsec/README.md b/benchmark/sirun/appsec/README.md index fd45c303b23..bbcb424e972 100644 --- a/benchmark/sirun/appsec/README.md +++ b/benchmark/sirun/appsec/README.md @@ -1,4 +1,4 @@ -This creates 1,000 HTTP requests from client to server. +This benchmarks HTTP requests from client to server. The variants are: - control tracer without appsec diff --git a/benchmark/sirun/encoding/README.md b/benchmark/sirun/encoding/README.md index 889bb9dec4b..957102dabc7 100644 --- a/benchmark/sirun/encoding/README.md +++ b/benchmark/sirun/encoding/README.md @@ -1,4 +1,4 @@ -This test sends a single trace 10000 times to the encoder. Each trace is +This test sends a single trace many times to the encoder. Each trace is pre-formatted (as the encoder requires) and consists of 30 spans with the same content in each of them. The IDs are all randomized. A null writer is provided to the encoder, so writing operations are not included here. diff --git a/benchmark/sirun/exporting-pipeline/README.md b/benchmark/sirun/exporting-pipeline/README.md index f7447afc608..28a0f23e5d2 100644 --- a/benchmark/sirun/exporting-pipeline/README.md +++ b/benchmark/sirun/exporting-pipeline/README.md @@ -2,6 +2,6 @@ This test creates a 30 span trace (of similar format to the encoding test). These spans are then passed through the formatting, encoding, and writing steps in our pipeline, and sent to a dummy agent. Once a span (i.e. a trace) is added to the exporter, we then proceed to the next iteration via `setImmediate`, and -run for 25000 iterations. +run for many iterations. There's a variant for each of our encodings/endpoints. diff --git a/benchmark/sirun/log/README.md b/benchmark/sirun/log/README.md index 9f25c806479..422abe0a610 100644 --- a/benchmark/sirun/log/README.md +++ b/benchmark/sirun/log/README.md @@ -1,4 +1,4 @@ -This test calls the internal logger on various log levels for 1000 iterations. +This test calls the internal logger on various log levels for many iterations. * `without-log` is the baseline that has logging disabled completely. * `skip-log` has logs enabled but uses a log level that isn't so that the handler doesn't run. diff --git a/benchmark/sirun/plugin-bluebird/README.md b/benchmark/sirun/plugin-bluebird/README.md index 5d1746b4b24..79fd4f57d0d 100644 --- a/benchmark/sirun/plugin-bluebird/README.md +++ b/benchmark/sirun/plugin-bluebird/README.md @@ -1,3 +1,3 @@ -This creates 50000 promises in a chain using the latest version of `bluebird`. +This creates a lot of promises in a chain using the latest version of `bluebird`. -The variants are with the tracer and without it. \ No newline at end of file +The variants are with the tracer and without it. diff --git a/benchmark/sirun/plugin-dns/README.md b/benchmark/sirun/plugin-dns/README.md index af30cb91095..566ac08842b 100644 --- a/benchmark/sirun/plugin-dns/README.md +++ b/benchmark/sirun/plugin-dns/README.md @@ -1,2 +1,2 @@ -Runs `dns.lookup('localhost', cb)` 10000 times. In the `with-tracer` variant, +Runs `dns.lookup('localhost', cb)` many times. In the `with-tracer` variant, tracing is enabled. Iteration count is set to 10. diff --git a/benchmark/sirun/plugin-http/README.md b/benchmark/sirun/plugin-http/README.md index 0ed9208d040..f42693cb6b2 100644 --- a/benchmark/sirun/plugin-http/README.md +++ b/benchmark/sirun/plugin-http/README.md @@ -1,4 +1,4 @@ -This creates 1,000 HTTP requests from client to server. +This benchmarks HTTP requests from client to server. The variants are with the tracer and without it, and instrumenting on the server and the client separately. diff --git a/benchmark/sirun/plugin-net/README.md b/benchmark/sirun/plugin-net/README.md index 0731413e121..dc2635fdbe9 100644 --- a/benchmark/sirun/plugin-net/README.md +++ b/benchmark/sirun/plugin-net/README.md @@ -1,3 +1,3 @@ -Creates 1000 connections between a net server and net client, doing a simple +Benchmarks connections between a net server and net client, doing a simple echo request. Since we only instrument client-side net connections, our variants are having the client with and without the tracer. diff --git a/benchmark/sirun/plugin-q/README.md b/benchmark/sirun/plugin-q/README.md index 48e57db4360..8dcce34ec93 100644 --- a/benchmark/sirun/plugin-q/README.md +++ b/benchmark/sirun/plugin-q/README.md @@ -1,3 +1,3 @@ -This creates 50000 promises in a chain using the latest version of `q`. +This benchmarks promises in a chain using the latest version of `q`. The variants are with the tracer and without it. diff --git a/benchmark/sirun/spans/README.md b/benchmark/sirun/spans/README.md index 734c9df65ac..7b695939b00 100644 --- a/benchmark/sirun/spans/README.md +++ b/benchmark/sirun/spans/README.md @@ -1,5 +1,5 @@ This test initializes a tracer with the no-op scope manager. It then creates -100000 spans, and depending on the variant, either finishes all of them as they +many spans, and depending on the variant, either finishes all of them as they are created, or later on once they're all created. Prior to creating any spans, it modifies the processor instance so that no span processing (or exporting) is done, and it simply stops storing the spans. diff --git a/benchmark/sirun/startup/README.md b/benchmark/sirun/startup/README.md index c09d0aed461..69c311d778c 100644 --- a/benchmark/sirun/startup/README.md +++ b/benchmark/sirun/startup/README.md @@ -1,3 +1,7 @@ This is a simple startup test. It tests with an without the tracer, and with and without requiring every dependency and devDependency in the package.json, for a total of four variants. + +While it's unrealistic to load all the tracer's devDependencies, the intention +is to simulate loading a lot of dependencies for an application, and have them +either be intercepted by our loader hooks, or not. diff --git a/ci/init.js b/ci/init.js index b54e29abd4d..7b15ed15151 100644 --- a/ci/init.js +++ b/ci/init.js @@ -1,11 +1,22 @@ /* eslint-disable no-console */ const tracer = require('../packages/dd-trace') const { isTrue } = require('../packages/dd-trace/src/util') +const log = require('../packages/dd-trace/src/log') const isJestWorker = !!process.env.JEST_WORKER_ID const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID const isMochaWorker = !!process.env.MOCHA_WORKER_ID +const packageManagers = [ + 'npm', + 'yarn', + 'pnpm' +] + +const isPackageManager = () => { + return packageManagers.some(packageManager => process.argv[1]?.includes(`bin/${packageManager}`)) +} + const options = { startupLogs: false, isCiVisibility: true, @@ -14,6 +25,11 @@ const options = { let shouldInit = true +if (isPackageManager()) { + log.debug('dd-trace is not initialized in a package manager.') + shouldInit = false +} + const isAgentlessEnabled = isTrue(process.env.DD_CIVISIBILITY_AGENTLESS_ENABLED) if (isAgentlessEnabled) { diff --git a/docs/test.ts b/docs/test.ts index 37342718c2a..2c2cbea332e 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -111,11 +111,10 @@ tracer.init({ blockedTemplateJson: './blocked.json', blockedTemplateGraphql: './blockedgraphql.json', eventTracking: { - mode: 'safe' + mode: 'anon' }, apiSecurity: { enabled: true, - requestSampling: 1.0 }, rasp: { enabled: true @@ -132,6 +131,7 @@ tracer.init({ requestSampling: 50, maxConcurrentRequests: 4, maxContextOperations: 30, + dbRowsToTaint: 12, deduplicationEnabled: true, redactionEnabled: true, redactionNamePattern: 'password', @@ -148,6 +148,7 @@ tracer.init({ requestSampling: 50, maxConcurrentRequests: 4, maxContextOperations: 30, + dbRowsToTaint: 6, deduplicationEnabled: true, redactionEnabled: true, redactionNamePattern: 'password', @@ -343,6 +344,7 @@ tracer.use('kafkajs'); tracer.use('knex'); tracer.use('koa'); tracer.use('koa', httpServerOptions); +tracer.use('langchain'); tracer.use('mariadb', { service: () => `my-custom-mariadb` }) tracer.use('memcached'); tracer.use('microgateway-core'); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..8b83488c08e --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,119 @@ +import mocha from 'eslint-plugin-mocha' +import n from 'eslint-plugin-n' +import stylistic from '@stylistic/eslint-plugin-js' +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +export default [ + { + ignores: [ + '**/coverage', // Just coverage reports. + '**/dist', // Generated + '**/docs', // Any JS here is for presentation only. + '**/out', // Generated + '**/node_modules', // We don't own these. + '**/versions', // This is effectively a node_modules tree. + '**/acmeair-nodejs', // We don't own this. + '**/vendor', // Generally, we didn't author this code. + 'integration-tests/esbuild/out.js', // Generated + 'integration-tests/esbuild/aws-sdk-out.js', // Generated + 'packages/dd-trace/src/appsec/blocked_templates.js', // TODO Why is this ignored? + 'packages/dd-trace/src/payload-tagging/jsonpath-plus.js' // Vendored + ] + }, ...compat.extends('eslint:recommended', 'standard', 'plugin:mocha/recommended'), { + plugins: { + mocha, + n, + '@stylistic/js': stylistic + }, + + languageOptions: { + globals: { + ...globals.node + }, + + ecmaVersion: 2022 + }, + + settings: { + node: { + version: '>=16.0.0' + } + }, + + rules: { + '@stylistic/js/max-len': ['error', { code: 120, tabWidth: 2 }], + '@stylistic/js/object-curly-newline': ['error', { + multiline: true, + consistent: true + }], + '@stylistic/js/object-curly-spacing': ['error', 'always'], + 'import/no-absolute-path': 'off', + 'import/no-extraneous-dependencies': 'error', + 'n/no-callback-literal': 'off', + 'n/no-restricted-require': ['error', ['diagnostics_channel']], + 'no-console': 'error', + 'no-prototype-builtins': 'off', + 'no-unused-expressions': 'off', + 'no-var': 'error', + 'prefer-const': 'error', + 'standard/no-callback-literal': 'off' + } + }, + { + files: [ + 'packages/*/test/**/*.js', + 'packages/*/test/**/*.mjs', + 'integration-tests/**/*.js', + 'integration-tests/**/*.mjs', + '**/*.spec.js' + ], + languageOptions: { + globals: { + ...globals.mocha, + sinon: false, + expect: false, + proxyquire: false, + withVersions: false, + withPeerService: false, + withNamingSchema: false, + withExports: false + } + }, + rules: { + 'mocha/max-top-level-suites': 'off', + 'mocha/no-exports': 'off', + 'mocha/no-global-tests': 'off', + 'mocha/no-identical-title': 'off', + 'mocha/no-mocha-arrows': 'off', + 'mocha/no-setup-in-describe': 'off', + 'mocha/no-sibling-hooks': 'off', + 'mocha/no-skipped-tests': 'off', + 'mocha/no-top-level-hooks': 'off', + 'n/handle-callback-err': 'off', + 'no-loss-of-precision': 'off' + } + }, + { + files: [ + 'integration-tests/**/*.js', + 'integration-tests/**/*.mjs', + 'packages/*/test/integration-test/**/*.js', + 'packages/*/test/integration-test/**/*.mjs' + ], + rules: { + 'import/no-extraneous-dependencies': 'off' + } + } +] diff --git a/index.d.ts b/index.d.ts index 940ca6a06db..8984d02f81a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -179,6 +179,7 @@ interface Plugins { "kafkajs": tracer.plugins.kafkajs "knex": tracer.plugins.knex; "koa": tracer.plugins.koa; + "langchain": tracer.plugins.langchain; "mariadb": tracer.plugins.mariadb; "memcached": tracer.plugins.memcached; "microgateway-core": tracer.plugins.microgateway_core; @@ -654,27 +655,33 @@ declare namespace tracer { */ eventTracking?: { /** - * Controls the automated user event tracking mode. Possible values are disabled, safe and extended. - * On safe mode, any detected Personally Identifiable Information (PII) about the user will be redacted from the event. - * On extended mode, no redaction will take place. - * @default 'safe' + * Controls the automated user tracking mode for user IDs and logins collections. Possible values: + * * 'anonymous': will hash user IDs and user logins before collecting them + * * 'anon': alias for 'anonymous' + * * 'safe': deprecated alias for 'anonymous' + * + * * 'identification': will collect user IDs and logins without redaction + * * 'ident': alias for 'identification' + * * 'extended': deprecated alias for 'identification' + * + * * 'disabled': will not collect user IDs and logins + * + * Unknown values will be considered as 'disabled' + * @default 'identification' */ - mode?: 'safe' | 'extended' | 'disabled' + mode?: + 'anonymous' | 'anon' | 'safe' | + 'identification' | 'ident' | 'extended' | + 'disabled' }, /** - * Configuration for Api Security sampling + * Configuration for Api Security */ apiSecurity?: { /** Whether to enable Api Security. - * @default false + * @default true */ enabled?: boolean, - - /** Controls the request sampling rate (between 0 and 1) in which Api Security is triggered. - * The value will be coerced back if it's outside of the 0-1 range. - * @default 0.1 - */ - requestSampling?: number }, /** * Configuration for RASP @@ -757,7 +764,7 @@ declare namespace tracer { */ maxDepth?: number } - + /** * Configuration enabling LLM Observability. Enablement is superceded by the DD_LLMOBS_ENABLED environment variable. */ @@ -1598,6 +1605,12 @@ declare namespace tracer { */ interface kafkajs extends Instrumentation {} + /** + * This plugin automatically instruments the + * [langchain](https://js.langchain.com/) module + */ + interface langchain extends Instrumentation {} + /** * This plugin automatically instruments the * [ldapjs](https://github.com/ldapjs/node-ldapjs/) module. @@ -2190,6 +2203,12 @@ declare namespace tracer { */ cookieFilterPattern?: string, + /** + * Defines the number of rows to taint in data coming from databases + * @default 1 + */ + dbRowsToTaint?: number, + /** * Whether to enable vulnerability deduplication */ @@ -2234,7 +2253,7 @@ declare namespace tracer { * Disable LLM Observability tracing. */ disable (): void, - + /** * Instruments a function by automatically creating a span activated on its * scope. @@ -2276,10 +2295,10 @@ declare namespace tracer { /** * Decorate a function in a javascript runtime that supports function decorators. * Note that this is **not** supported in the Node.js runtime, but is in TypeScript. - * + * * In TypeScript, this decorator is only supported in contexts where general TypeScript * function decorators are supported. - * + * * @param options Optional LLM Observability span options. */ decorate (options: llmobs.LLMObsNamelessSpanOptions): any @@ -2296,7 +2315,7 @@ declare namespace tracer { /** * Sets inputs, outputs, tags, metadata, and metrics as provided for a given LLM Observability span. * Note that with the exception of tags, this method will override any existing values for the provided fields. - * + * * For example: * ```javascript * llmobs.trace({ kind: 'llm', name: 'myLLM', modelName: 'gpt-4o', modelProvider: 'openai' }, () => { @@ -2309,7 +2328,7 @@ declare namespace tracer { * }) * }) * ``` - * + * * @param span The span to annotate (defaults to the current LLM Observability span if not provided) * @param options An object containing the inputs, outputs, tags, metadata, and metrics to set on the span. */ @@ -2485,14 +2504,14 @@ declare namespace tracer { * LLM Observability span kind. One of `agent`, `workflow`, `task`, `tool`, `retrieval`, `embedding`, or `llm`. */ kind: llmobs.spanKind, - + /** * The ID of the underlying user session. Required for tracking sessions. */ sessionId?: string, /** - * The name of the ML application that the agent is orchestrating. + * The name of the ML application that the agent is orchestrating. * If not provided, the default value will be set to mlApp provided during initalization, or `DD_LLMOBS_ML_APP`. */ mlApp?: string, diff --git a/init.js b/init.js index 8b183fc17ab..625d493b3b1 100644 --- a/init.js +++ b/init.js @@ -2,70 +2,8 @@ /* eslint-disable no-var */ -var NODE_MAJOR = require('./version').NODE_MAJOR +var guard = require('./packages/dd-trace/src/guardrails') -// We use several things that are not supported by older versions of Node: -// - AsyncLocalStorage -// - The `semver` module -// - dc-polyfill -// - Mocha (for testing) -// and probably others. -// TODO: Remove all these dependencies so that we can report telemetry. -if (NODE_MAJOR >= 12) { - var path = require('path') - var Module = require('module') - var semver = require('semver') - var log = require('./packages/dd-trace/src/log') - var isTrue = require('./packages/dd-trace/src/util').isTrue - var telemetry = require('./packages/dd-trace/src/telemetry/init-telemetry') - - var initBailout = false - var clobberBailout = false - var forced = isTrue(process.env.DD_INJECT_FORCE) - - if (process.env.DD_INJECTION_ENABLED) { - // If we're running via single-step install, and we're not in the app's - // node_modules, then we should not initialize the tracer. This prevents - // single-step-installed tracer from clobbering the manually-installed tracer. - var resolvedInApp - var entrypoint = process.argv[1] - try { - resolvedInApp = Module.createRequire(entrypoint).resolve('dd-trace') - } catch (e) { - // Ignore. If we can't resolve the module, we assume it's not in the app. - } - if (resolvedInApp) { - var ourselves = path.join(__dirname, 'index.js') - if (ourselves !== resolvedInApp) { - clobberBailout = true - } - } - - // If we're running via single-step install, and the runtime doesn't match - // the engines field in package.json, then we should not initialize the tracer. - if (!clobberBailout) { - var engines = require('./package.json').engines - var version = process.versions.node - if (!semver.satisfies(version, engines.node)) { - initBailout = true - telemetry([ - { name: 'abort', tags: ['reason:incompatible_runtime'] }, - { name: 'abort.runtime', tags: [] } - ]) - log.info('Aborting application instrumentation due to incompatible_runtime.') - log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') - if (forced) { - log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') - } - } - } - } - - if (!clobberBailout && (!initBailout || forced)) { - var tracer = require('.') - tracer.init() - module.exports = tracer - telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) - log.info('Application instrumentation bootstrapping complete') - } -} +module.exports = guard(function () { + return require('.').init() +}) diff --git a/initialize.mjs b/initialize.mjs index 777f45cc046..b7303848430 100644 --- a/initialize.mjs +++ b/initialize.mjs @@ -12,6 +12,7 @@ import { isMainThread } from 'worker_threads' +import * as Module from 'node:module' import { fileURLToPath } from 'node:url' import { load as origLoad, @@ -31,11 +32,16 @@ ${result.source}` return result } +const [NODE_MAJOR, NODE_MINOR] = process.versions.node.split('.').map(x => +x) + +const brokenLoaders = NODE_MAJOR === 18 && NODE_MINOR === 0 + export async function load (...args) { - return insertInit(await origLoad(...args)) + const loadHook = brokenLoaders ? args[args.length - 1] : origLoad + return insertInit(await loadHook(...args)) } -export const resolve = origResolve +export const resolve = brokenLoaders ? undefined : origResolve export const getFormat = origGetFormat @@ -44,12 +50,9 @@ export async function getSource (...args) { } if (isMainThread) { - // Need this IIFE for versions of Node.js without top-level await. - (async () => { - await import('./init.js') - const { register } = await import('node:module') - if (register) { - register('./loader-hook.mjs', import.meta.url) - } - })() + const require = Module.createRequire(import.meta.url) + require('./init.js') + if (Module.register) { + Module.register('./loader-hook.mjs', import.meta.url) + } } diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index c133a7a31fe..f08f1a24ecd 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -25,7 +25,7 @@ const DEFAULT_SUITES_TO_SKIP = [] const DEFAULT_GIT_UPLOAD_STATUS = 200 const DEFAULT_KNOWN_TESTS_UPLOAD_STATUS = 200 const DEFAULT_INFO_RESPONSE = { - endpoints: ['/evp_proxy/v2'] + endpoints: ['/evp_proxy/v2', '/debugger/v1/input'] } const DEFAULT_CORRELATION_ID = '1234' const DEFAULT_KNOWN_TESTS = ['test-suite1.js.test-name1', 'test-suite2.js.test-name2'] @@ -208,7 +208,10 @@ class FakeCiVisIntake extends FakeAgent { }) }) - app.post('/api/v2/logs', express.json(), (req, res) => { + app.post([ + '/api/v2/logs', + '/debugger/v1/input' + ], express.json(), (req, res) => { res.status(200).send('OK') this.emit('message', { headers: req.headers, diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/dependency.js b/integration-tests/ci-visibility/dynamic-instrumentation/dependency.js new file mode 100644 index 00000000000..b53ebf22f97 --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/dependency.js @@ -0,0 +1,7 @@ +module.exports = function (a, b) { + const localVariable = 2 + if (a > 10) { + throw new Error('a is too big') + } + return a + b + localVariable - localVariable +} diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js b/integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js new file mode 100644 index 00000000000..483b2a543d3 --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js @@ -0,0 +1,7 @@ +module.exports = function () { + try { + return typeof jest !== 'undefined' + } catch (e) { + return false + } +} diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js b/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js new file mode 100644 index 00000000000..ed2e3d14e51 --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js @@ -0,0 +1,22 @@ +/* eslint-disable */ +const sum = require('./dependency') +const isJest = require('./is-jest') +const { expect } = require('chai') + +// TODO: instead of retrying through jest, this should be retried with auto test retries +if (isJest()) { + jest.retryTimes(1) +} + +describe('dynamic-instrumentation', () => { + it('retries with DI', function () { + if (this.retries) { + this.retries(1) + } + expect(sum(11, 3)).to.equal(14) + }) + + it('is not retried', () => { + expect(1 + 2).to.equal(3) + }) +}) diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js b/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js new file mode 100644 index 00000000000..7960852a52c --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js @@ -0,0 +1,24 @@ +/* eslint-disable */ +const sum = require('./dependency') +const isJest = require('./is-jest') +const { expect } = require('chai') + +// TODO: instead of retrying through jest, this should be retried with auto test retries +if (isJest()) { + jest.retryTimes(1) +} + +let count = 0 +describe('dynamic-instrumentation', () => { + it('retries with DI', function () { + if (this.retries) { + this.retries(1) + } + const willFail = count++ === 0 + if (willFail) { + expect(sum(11, 3)).to.equal(14) // only throws the first time + } else { + expect(sum(1, 2)).to.equal(3) + } + }) +}) diff --git a/integration-tests/ci-visibility/features-di/support/steps.js b/integration-tests/ci-visibility/features-di/support/steps.js new file mode 100644 index 00000000000..00880f83467 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/support/steps.js @@ -0,0 +1,24 @@ +const assert = require('assert') +const { When, Then } = require('@cucumber/cucumber') +const sum = require('./sum') + +let count = 0 + +When('the greeter says hello', function () { + this.whatIHeard = 'hello' +}) + +Then('I should have heard {string}', function (expectedResponse) { + sum(11, 3) + assert.equal(this.whatIHeard, expectedResponse) +}) + +Then('I should have flakily heard {string}', function (expectedResponse) { + const shouldFail = count++ < 1 + if (shouldFail) { + sum(11, 3) + } else { + sum(1, 3) // does not hit the breakpoint the second time + } + assert.equal(this.whatIHeard, expectedResponse) +}) diff --git a/integration-tests/ci-visibility/features-di/support/sum.js b/integration-tests/ci-visibility/features-di/support/sum.js new file mode 100644 index 00000000000..cb1d7adb951 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/support/sum.js @@ -0,0 +1,10 @@ +function funSum (a, b) { + const localVariable = 2 + if (a > 10) { + throw new Error('the number is too big') + } + + return a + b + localVariable +} + +module.exports = funSum diff --git a/integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature b/integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature new file mode 100644 index 00000000000..06ef560af61 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature @@ -0,0 +1,6 @@ + +Feature: Greeting + + Scenario: Say hello + When the greeter says hello + Then I should have heard "hello" diff --git a/integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature b/integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature new file mode 100644 index 00000000000..ca5562b55c0 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature @@ -0,0 +1,6 @@ + +Feature: Greeting + + Scenario: Say hello + When the greeter says hello + Then I should have flakily heard "hello" diff --git a/integration-tests/ci-visibility/features-esm/support/steps.mjs b/integration-tests/ci-visibility/features-esm/support/steps.mjs index 64194a68684..66d05584383 100644 --- a/integration-tests/ci-visibility/features-esm/support/steps.mjs +++ b/integration-tests/ci-visibility/features-esm/support/steps.mjs @@ -5,12 +5,15 @@ class Greeter { sayFarewell () { return 'farewell' } + sayGreetings () { return 'greetings' } + sayYo () { return 'yo' } + sayYeah () { return 'yeah whatever' } diff --git a/integration-tests/ci-visibility/office-addin-mock/dependency.js b/integration-tests/ci-visibility/office-addin-mock/dependency.js new file mode 100644 index 00000000000..363131a422a --- /dev/null +++ b/integration-tests/ci-visibility/office-addin-mock/dependency.js @@ -0,0 +1,7 @@ +require('office-addin-mock') + +function sum (a, b) { + return a + b +} + +module.exports = sum diff --git a/integration-tests/ci-visibility/office-addin-mock/test.js b/integration-tests/ci-visibility/office-addin-mock/test.js new file mode 100644 index 00000000000..50a3b6c2e28 --- /dev/null +++ b/integration-tests/ci-visibility/office-addin-mock/test.js @@ -0,0 +1,6 @@ +const sum = require('./dependency') +const { expect } = require('chai') + +test('can sum', () => { + expect(sum(1, 2)).to.equal(3) +}) 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 4e05a904176..7ee22886c7b 100644 --- a/integration-tests/ci-visibility/playwright-tests/landing-page-test.js +++ b/integration-tests/ci-visibility/playwright-tests/landing-page-test.js @@ -4,29 +4,34 @@ test.beforeEach(async ({ page }) => { await page.goto(process.env.PW_BASE_URL) }) -test.describe('playwright', () => { - test('should work with passing tests', async ({ page }) => { - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello World' - ]) - }) - test.skip('should work with skipped tests', async ({ page }) => { - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello World' - ]) - }) - test.fixme('should work with fixme', async ({ page }) => { - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello Warld' - ]) - }) - test('should work with annotated tests', async ({ page }) => { - test.info().annotations.push({ type: 'DD_TAGS[test.memory.usage]', description: 'low' }) - test.info().annotations.push({ type: 'DD_TAGS[test.memory.allocations]', description: 16 }) - // this is malformed and should be ignored - test.info().annotations.push({ type: 'DD_TAGS[test.invalid', description: 'high' }) - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello World' - ]) +test.describe('highest-level-describe', () => { + test.describe(' leading and trailing spaces ', () => { + // even empty describe blocks should be allowed + test.describe(' ', () => { + test('should work with passing tests', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) + test.skip('should work with skipped tests', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) + test.fixme('should work with fixme', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello Warld' + ]) + }) + test('should work with annotated tests', async ({ page }) => { + test.info().annotations.push({ type: 'DD_TAGS[test.memory.usage]', description: 'low' }) + test.info().annotations.push({ type: 'DD_TAGS[test.memory.allocations]', description: 16 }) + // this is malformed and should be ignored + test.info().annotations.push({ type: 'DD_TAGS[test.invalid', description: 'high' }) + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) + }) }) }) diff --git a/integration-tests/ci-visibility/subproject/dependency.js b/integration-tests/ci-visibility/subproject/dependency.js new file mode 100644 index 00000000000..2012896b44c --- /dev/null +++ b/integration-tests/ci-visibility/subproject/dependency.js @@ -0,0 +1,3 @@ +module.exports = function (a, b) { + return a + b +} diff --git a/integration-tests/ci-visibility/subproject/subproject-test.js b/integration-tests/ci-visibility/subproject/subproject-test.js index 5300f1926d6..89b0ddab6b1 100644 --- a/integration-tests/ci-visibility/subproject/subproject-test.js +++ b/integration-tests/ci-visibility/subproject/subproject-test.js @@ -1,9 +1,10 @@ // eslint-disable-next-line const { expect } = require('chai') +const dependency = require('./dependency') describe('subproject-test', () => { it('can run', () => { // eslint-disable-next-line - expect(1).to.equal(1) + expect(dependency(1, 2)).to.equal(3) }) }) diff --git a/integration-tests/ci-visibility/vitest-tests/bad-sum.mjs b/integration-tests/ci-visibility/vitest-tests/bad-sum.mjs new file mode 100644 index 00000000000..809a131c8d3 --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/bad-sum.mjs @@ -0,0 +1,7 @@ +export function sum (a, b) { + const localVar = 10 + if (a > 10) { + throw new Error('a is too large') + } + return a + b + localVar - localVar +} diff --git a/integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs b/integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs new file mode 100644 index 00000000000..33c9bca09c5 --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs @@ -0,0 +1,18 @@ +import { describe, test, expect } from 'vitest' +import { sum } from './bad-sum' + +let numAttempt = 0 + +describe('dynamic instrumentation', () => { + test('can sum', () => { + const shouldFail = numAttempt++ === 0 + if (shouldFail) { + expect(sum(11, 2)).to.equal(13) + } else { + expect(sum(1, 2)).to.equal(3) + } + }) + test('is not retried', () => { + expect(sum(1, 2)).to.equal(3) + }) +}) diff --git a/integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs b/integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs new file mode 100644 index 00000000000..1e2bb73352d --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs @@ -0,0 +1,11 @@ +import { describe, test, expect } from 'vitest' +import { sum } from './bad-sum' + +describe('dynamic instrumentation', () => { + test('can sum', () => { + expect(sum(11, 2)).to.equal(13) + }) + test('is not retried', () => { + expect(sum(1, 2)).to.equal(3) + }) +}) diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index 35c4b3b2060..f7925210a87 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -37,7 +37,11 @@ const { TEST_SUITE, TEST_CODE_OWNERS, TEST_SESSION_NAME, - TEST_LEVEL_EVENT_TYPES + TEST_LEVEL_EVENT_TYPES, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_LINE } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -86,10 +90,11 @@ versions.forEach(version => { reportMethods.forEach((reportMethod) => { context(`reporting via ${reportMethod}`, () => { - let envVars, isAgentless + let envVars, isAgentless, logsEndpoint beforeEach(() => { isAgentless = reportMethod === 'agentless' envVars = isAgentless ? getCiVisAgentlessConfig(receiver.port) : getCiVisEvpProxyConfig(receiver.port) + logsEndpoint = isAgentless ? '/api/v2/logs' : '/debugger/v1/input' }) const runModes = ['serial'] @@ -275,6 +280,7 @@ versions.forEach(version => { } ) }) + it('can report code coverage', (done) => { const libraryConfigRequestPromise = receiver.payloadReceived( ({ url }) => url.endsWith('/api/v2/libraries/tests/services/setting') @@ -355,6 +361,7 @@ versions.forEach(version => { done() }) }) + it('does not report code coverage if disabled by the API', (done) => { receiver.setSettings({ itr_enabled: false, @@ -390,6 +397,7 @@ versions.forEach(version => { } ) }) + it('can skip suites received by the intelligent test runner API and still reports code coverage', (done) => { receiver.setSuitesToSkip([{ @@ -463,6 +471,7 @@ versions.forEach(version => { } ) }) + it('does not skip tests if git metadata upload fails', (done) => { receiver.setSuitesToSkip([{ type: 'suite', @@ -505,6 +514,7 @@ versions.forEach(version => { } ) }) + it('does not skip tests if test skipping is disabled by the API', (done) => { receiver.setSettings({ itr_enabled: true, @@ -543,6 +553,7 @@ versions.forEach(version => { } ) }) + it('does not skip suites if suite is marked as unskippable', (done) => { receiver.setSettings({ itr_enabled: true, @@ -611,6 +622,7 @@ versions.forEach(version => { }).catch(done) }) }) + it('only sets forced to run if suite was going to be skipped by ITR', (done) => { receiver.setSettings({ itr_enabled: true, @@ -673,6 +685,7 @@ versions.forEach(version => { }).catch(done) }) }) + it('sets _dd.ci.itr.tests_skipped to false if the received suite is not skipped', (done) => { receiver.setSuitesToSkip([{ type: 'suite', @@ -709,6 +722,7 @@ versions.forEach(version => { }).catch(done) }) }) + if (!isAgentless) { context('if the agent is not event platform proxy compatible', () => { it('does not do any intelligent test runner request', (done) => { @@ -757,6 +771,7 @@ versions.forEach(version => { }) }) } + it('reports itr_correlation_id in test suites', (done) => { const itrCorrelationId = '4321' receiver.setItrCorrelationId(itrCorrelationId) @@ -783,6 +798,45 @@ versions.forEach(version => { }).catch(done) }) }) + + it('reports code coverage relative to the repository root, not working directory', (done) => { + receiver.setSettings({ + itr_enabled: true, + code_coverage: true, + tests_skipping: false + }) + + const codeCoveragesPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcov'), (payloads) => { + const coveredFiles = payloads + .flatMap(({ payload }) => payload) + .flatMap(({ content: { coverages } }) => coverages) + .flatMap(({ files }) => files) + .map(({ filename }) => filename) + + assert.includeMembers(coveredFiles, [ + 'ci-visibility/subproject/features/support/steps.js', + 'ci-visibility/subproject/features/greetings.feature' + ]) + }) + + childProcess = exec( + '../../node_modules/nyc/bin/nyc.js node ../../node_modules/.bin/cucumber-js features/*.feature', + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...getCiVisAgentlessConfig(receiver.port) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + codeCoveragesPromise.then(() => { + done() + }).catch(done) + }) + }) }) context('early flake detection', () => { @@ -1487,6 +1541,234 @@ versions.forEach(version => { }) }) }) + // Dynamic instrumentation only supported from >=8.0.0 + context('dynamic instrumentation', () => { + it('does not activate if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === logsEndpoint, (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', + { + cwd, + env: envVars, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + + it('does not activate dynamic instrumentation if remote settings are disabled', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: false + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === logsEndpoint, (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', + { + cwd, + env: { + ...envVars, + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/features-di/support/sum.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === logsEndpoint, (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '3' + }, + localVariable: { + type: 'number', + value: '2' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', + { + cwd, + env: { + ...envVars, + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/features-di/support/sum.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-not-hit-breakpoint.feature --retry 1', + { + cwd, + env: { + ...envVars, + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', (exitCode) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(exitCode, 0) + done() + }).catch(done) + }) + }) + }) } }) }) diff --git a/integration-tests/cypress/cypress.spec.js b/integration-tests/cypress/cypress.spec.js index afc79b2ebe5..0a6f5f065f9 100644 --- a/integration-tests/cypress/cypress.spec.js +++ b/integration-tests/cypress/cypress.spec.js @@ -837,6 +837,63 @@ moduleTypes.forEach(({ }).catch(done) }) }) + + it('reports code coverage relative to the repository root, not working directory', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: true, + tests_skipping: false + }) + let command + + if (type === 'commonJS') { + const commandSuffix = version === '6.7.0' + ? '--config-file cypress-config.json --spec "cypress/e2e/*.cy.js"' + : '' + command = `../../node_modules/.bin/cypress run ${commandSuffix}` + } else { + command = `node --loader=${hookFile} ../../cypress-esm-config.mjs` + } + + const { + NODE_OPTIONS, // NODE_OPTIONS dd-trace config does not work with cypress + ...restEnvVars + } = getCiVisAgentlessConfig(receiver.port) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcov'), (payloads) => { + const coveredFiles = payloads + .flatMap(({ payload }) => payload) + .flatMap(({ content: { coverages } }) => coverages) + .flatMap(({ files }) => files) + .map(({ filename }) => filename) + + assert.includeMembers(coveredFiles, [ + 'ci-visibility/subproject/src/utils.tsx', + 'ci-visibility/subproject/src/App.tsx', + 'ci-visibility/subproject/src/index.tsx', + 'ci-visibility/subproject/cypress/e2e/spec.cy.js' + ]) + }, 10000) + + childProcess = exec( + command, + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}` + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) }) it('still reports correct format if there is a plugin incompatibility', (done) => { diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 3330a6c32d3..6db68d0607d 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -9,12 +9,22 @@ const { ACKNOWLEDGED, ERROR } = require('../../packages/dd-trace/src/appsec/remo const { version } = require('../../package.json') describe('Dynamic Instrumentation', function () { - const t = setup() + describe('DD_TRACING_ENABLED=true', function () { + testWithTracingEnabled() + }) + + describe('DD_TRACING_ENABLED=false', function () { + testWithTracingEnabled(false) + }) +}) + +function testWithTracingEnabled (tracingEnabled = true) { + const t = setup({ DD_TRACING_ENABLED: tracingEnabled }) it('base case: target app should work as expected if no test probe has been added', async function () { - const response = await t.axios.get('/foo') + const response = await t.axios.get(t.breakpoint.url) assert.strictEqual(response.status, 200) - assert.deepStrictEqual(response.data, { hello: 'foo' }) + assert.deepStrictEqual(response.data, { hello: 'bar' }) }) describe('diagnostics messages', function () { @@ -24,15 +34,15 @@ describe('Dynamic Instrumentation', function () { const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'EMITTING' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'EMITTING' } } }] t.agent.on('remote-config-ack-update', (id, version, state, error) => { @@ -51,10 +61,10 @@ describe('Dynamic Instrumentation', function () { assertUUID(payload.debugger.diagnostics.runtimeId) if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get('/foo') + t.axios.get(t.breakpoint.url) .then((response) => { assert.strictEqual(response.status, 200) - assert.deepStrictEqual(response.data, { hello: 'foo' }) + assert.deepStrictEqual(response.data, { hello: 'bar' }) }) .catch(done) } else { @@ -75,19 +85,19 @@ describe('Dynamic Instrumentation', function () { const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 1, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 1, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 1, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 1, status: 'INSTALLED' } } }] const triggers = [ () => { @@ -128,11 +138,11 @@ describe('Dynamic Instrumentation', function () { const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }] t.agent.on('remote-config-ack-update', (id, version, state, error) => { @@ -201,7 +211,7 @@ describe('Dynamic Instrumentation', function () { }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, version: 0, status: 'ERROR' } } + debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, probeVersion: 0, status: 'ERROR' } } }] t.agent.on('debugger-diagnostics', ({ payload }) => { @@ -235,8 +245,18 @@ describe('Dynamic Instrumentation', function () { describe('input messages', function () { it('should capture and send expected payload when a log line probe is triggered', function (done) { + let traceId, spanId, dd + t.triggerBreakpoint() + t.agent.on('message', ({ payload }) => { + const span = payload.find((arr) => arr[0].name === 'fastify.request')[0] + traceId = span.trace_id.toString() + spanId = span.span_id.toString() + + assertDD() + }) + t.agent.on('debugger-input', ({ payload }) => { const expected = { ddsource: 'dd_debugger', @@ -245,7 +265,7 @@ describe('Dynamic Instrumentation', function () { message: 'Hello World!', logger: { name: t.breakpoint.file, - method: 'handler', + method: 'fooHandler', version, thread_name: 'MainThread' }, @@ -260,7 +280,21 @@ describe('Dynamic Instrumentation', function () { } assertObjectContains(payload, expected) + assert.match(payload.logger.thread_id, /^pid:\d+$/) + + if (tracingEnabled) { + assert.isObject(payload.dd) + assert.hasAllKeys(payload.dd, ['trace_id', 'span_id']) + assert.typeOf(payload.dd.trace_id, 'string') + assert.typeOf(payload.dd.span_id, 'string') + assert.isAbove(payload.dd.trace_id.length, 0) + assert.isAbove(payload.dd.span_id.length, 0) + dd = payload.dd + } else { + assert.doesNotHaveAnyKeys(payload, ['dd']) + } + assertUUID(payload['debugger.snapshot'].id) assert.isNumber(payload['debugger.snapshot'].timestamp) assert.isTrue(payload['debugger.snapshot'].timestamp > Date.now() - 1000 * 60) @@ -279,27 +313,38 @@ describe('Dynamic Instrumentation', function () { const topFrame = payload['debugger.snapshot'].stack[0] // path seems to be prefeixed with `/private` on Mac assert.match(topFrame.fileName, new RegExp(`${t.appFile}$`)) - assert.strictEqual(topFrame.function, 'handler') + assert.strictEqual(topFrame.function, 'fooHandler') assert.strictEqual(topFrame.lineNumber, t.breakpoint.line) assert.strictEqual(topFrame.columnNumber, 3) - done() + if (tracingEnabled) { + assertDD() + } else { + done() + } }) t.agent.addRemoteConfig(t.rcConfig) + + function assertDD () { + if (!traceId || !spanId || !dd) return + assert.strictEqual(dd.trace_id, traceId) + assert.strictEqual(dd.span_id, spanId) + done() + } }) it('should respond with updated message if probe message is updated', function (done) { const expectedMessages = ['Hello World!', 'Hello Updated World!'] const triggers = [ async () => { - await t.axios.get('/foo') + await t.axios.get(t.breakpoint.url) t.rcConfig.config.version++ t.rcConfig.config.template = 'Hello Updated World!' t.agent.updateRemoteConfig(t.rcConfig.id, t.rcConfig.config) }, async () => { - await t.axios.get('/foo') + await t.axios.get(t.breakpoint.url) } ] @@ -316,29 +361,17 @@ describe('Dynamic Instrumentation', function () { }) it('should not trigger if probe is deleted', function (done) { - t.agent.on('debugger-diagnostics', async ({ payload }) => { - try { - if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.agent.once('remote-confg-responded', async () => { - try { - await t.axios.get('/foo') - // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail - // if it does, but not so long that the test times out. - // TODO: Is there some signal we can use instead of a timer? - setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval - } catch (err) { - // Nessecary hack: Any errors thrown inside of an async function is invisible to Mocha unless the outer - // `it` callback is also `async` (which we can't do in this case since we rely on the `done` callback). - done(err) - } - }) + t.agent.on('debugger-diagnostics', ({ payload }) => { + if (payload.debugger.diagnostics.status === 'INSTALLED') { + t.agent.once('remote-confg-responded', async () => { + await t.axios.get(t.breakpoint.url) + // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail + // if it does, but not so long that the test times out. + // TODO: Is there some signal we can use instead of a timer? + setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval + }) - t.agent.removeRemoteConfig(t.rcConfig.id) - } - } catch (err) { - // Nessecary hack: Any errors thrown inside of an async function is invisible to Mocha unless the outer `it` - // callback is also `async` (which we can't do in this case since we rely on the `done` callback). - done(err) + t.agent.removeRemoteConfig(t.rcConfig.id) } }) @@ -350,6 +383,100 @@ describe('Dynamic Instrumentation', function () { }) }) + describe('sampling', function () { + it('should respect sampling rate for single probe', function (done) { + let start, timer + let payloadsReceived = 0 + const rcConfig = t.generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + + function triggerBreakpointContinuously () { + t.axios.get(t.breakpoint.url).catch(done) + timer = setTimeout(triggerBreakpointContinuously, 10) + } + + t.agent.on('debugger-diagnostics', ({ payload }) => { + if (payload.debugger.diagnostics.status === 'INSTALLED') triggerBreakpointContinuously() + }) + + t.agent.on('debugger-input', () => { + payloadsReceived++ + if (payloadsReceived === 1) { + start = Date.now() + } else if (payloadsReceived === 2) { + const duration = Date.now() - start + clearTimeout(timer) + + // Allow for a variance of -5/+50ms (time will tell if this is enough) + assert.isAbove(duration, 995) + assert.isBelow(duration, 1050) + + // Wait at least a full sampling period, to see if we get any more payloads + timer = setTimeout(done, 1250) + } else { + clearTimeout(timer) + done(new Error('Too many payloads received!')) + } + }) + + t.agent.addRemoteConfig(rcConfig) + }) + + it('should adhere to individual probes sample rate', function (done) { + const rcConfig1 = t.breakpoints[0].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + const rcConfig2 = t.breakpoints[1].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + const state = { + [rcConfig1.config.id]: { + payloadsReceived: 0, + tiggerBreakpointContinuously () { + t.axios.get(t.breakpoints[0].url).catch(done) + this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) + } + }, + [rcConfig2.config.id]: { + payloadsReceived: 0, + tiggerBreakpointContinuously () { + t.axios.get(t.breakpoints[1].url).catch(done) + this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) + } + } + } + + t.agent.on('debugger-diagnostics', ({ payload }) => { + const { probeId, status } = payload.debugger.diagnostics + if (status === 'INSTALLED') state[probeId].tiggerBreakpointContinuously() + }) + + t.agent.on('debugger-input', ({ payload }) => { + const _state = state[payload['debugger.snapshot'].probe.id] + _state.payloadsReceived++ + if (_state.payloadsReceived === 1) { + _state.start = Date.now() + } else if (_state.payloadsReceived === 2) { + const duration = Date.now() - _state.start + clearTimeout(_state.timer) + + // Allow for a variance of -5/+50ms (time will tell if this is enough) + assert.isAbove(duration, 995) + assert.isBelow(duration, 1050) + + // Wait at least a full sampling period, to see if we get any more payloads + _state.timer = setTimeout(doneWhenCalledTwice, 1250) + } else { + clearTimeout(_state.timer) + done(new Error('Too many payloads received!')) + } + }) + + t.agent.addRemoteConfig(rcConfig1) + t.agent.addRemoteConfig(rcConfig2) + + function doneWhenCalledTwice () { + if (doneWhenCalledTwice.calledOnce) return done() + doneWhenCalledTwice.calledOnce = true + } + }) + }) + describe('race conditions', function () { it('should remove the last breakpoint completely before trying to add a new one', function (done) { const rcConfig2 = t.generateRemoteConfig() @@ -380,7 +507,7 @@ describe('Dynamic Instrumentation', function () { }) // Perform HTTP request to try and trigger the probe - t.axios.get('/foo').catch((err) => { + t.axios.get(t.breakpoint.url).catch((err) => { // If the request hasn't fully completed by the time the tests ends and the target app is destroyed, Axios // will complain with a "socket hang up" error. Hence this sanity check before calling `done(err)`. If we // later add more tests below this one, this shouuldn't be an issue. @@ -392,4 +519,4 @@ describe('Dynamic Instrumentation', function () { t.agent.addRemoteConfig(t.rcConfig) }) }) -}) +} diff --git a/integration-tests/debugger/snapshot-pruning.spec.js b/integration-tests/debugger/snapshot-pruning.spec.js index 91190a1c25d..c1ba218dd1c 100644 --- a/integration-tests/debugger/snapshot-pruning.spec.js +++ b/integration-tests/debugger/snapshot-pruning.spec.js @@ -1,9 +1,7 @@ 'use strict' const { assert } = require('chai') -const { setup, getBreakpointInfo } = require('./utils') - -const { line } = getBreakpointInfo() +const { setup } = require('./utils') describe('Dynamic Instrumentation', function () { const t = setup() @@ -17,7 +15,7 @@ describe('Dynamic Instrumentation', function () { assert.isBelow(Buffer.byteLength(JSON.stringify(payload)), 1024 * 1024) // 1MB assert.deepEqual(payload['debugger.snapshot'].captures, { lines: { - [line]: { + [t.breakpoint.line]: { locals: { notCapturedReason: 'Snapshot was too large', size: 6 diff --git a/integration-tests/debugger/snapshot.spec.js b/integration-tests/debugger/snapshot.spec.js index 94ef323f6a7..e3d17b225c4 100644 --- a/integration-tests/debugger/snapshot.spec.js +++ b/integration-tests/debugger/snapshot.spec.js @@ -31,7 +31,7 @@ describe('Dynamic Instrumentation', function () { str: { type: 'string', value: 'foo' }, lstr: { type: 'string', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor i', truncated: true, size: 445 @@ -129,7 +129,7 @@ describe('Dynamic Instrumentation', function () { str: { type: 'string', value: 'foo' }, lstr: { type: 'string', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor i', truncated: true, size: 445 diff --git a/integration-tests/debugger/target-app/basic.js b/integration-tests/debugger/target-app/basic.js index f8330012278..d9d1e0e9185 100644 --- a/integration-tests/debugger/target-app/basic.js +++ b/integration-tests/debugger/target-app/basic.js @@ -5,8 +5,12 @@ const Fastify = require('fastify') const fastify = Fastify() -fastify.get('/:name', function handler (request) { - return { hello: request.params.name } // BREAKPOINT +fastify.get('/foo/:name', function fooHandler (request) { + return { hello: request.params.name } // BREAKPOINT: /foo/bar +}) + +fastify.get('/bar/:name', function barHandler (request) { + return { hello: request.params.name } // BREAKPOINT: /bar/baz }) fastify.listen({ port: process.env.APP_PORT }, (err) => { diff --git a/integration-tests/debugger/target-app/snapshot-pruning.js b/integration-tests/debugger/target-app/snapshot-pruning.js index 58752006192..6b14405e61d 100644 --- a/integration-tests/debugger/target-app/snapshot-pruning.js +++ b/integration-tests/debugger/target-app/snapshot-pruning.js @@ -14,7 +14,7 @@ fastify.get('/:name', function handler (request) { // eslint-disable-next-line no-unused-vars const obj = generateObjectWithJSONSizeLargerThan1MB() - return { hello: request.params.name } // BREAKPOINT + return { hello: request.params.name } // BREAKPOINT: /foo }) fastify.listen({ port: process.env.APP_PORT }, (err) => { diff --git a/integration-tests/debugger/target-app/snapshot.js b/integration-tests/debugger/target-app/snapshot.js index a7b1810c10b..03cfc758556 100644 --- a/integration-tests/debugger/target-app/snapshot.js +++ b/integration-tests/debugger/target-app/snapshot.js @@ -11,7 +11,7 @@ const fastify = Fastify() fastify.get('/:name', function handler (request) { // eslint-disable-next-line no-unused-vars const { nil, undef, bool, num, bigint, str, lstr, sym, regex, arr, obj, emptyObj, fn, p } = getSomeData() - return { hello: request.params.name } // BREAKPOINT + return { hello: request.params.name } // BREAKPOINT: /foo }) fastify.listen({ port: process.env.APP_PORT }, (err) => { @@ -30,7 +30,7 @@ function getSomeData () { num: 42, bigint: 42n, str: 'foo', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len lstr: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', sym: Symbol('foo'), regex: /bar/i, diff --git a/integration-tests/debugger/target-app/unreffed.js b/integration-tests/debugger/target-app/unreffed.js new file mode 100644 index 00000000000..c3c73d72d8b --- /dev/null +++ b/integration-tests/debugger/target-app/unreffed.js @@ -0,0 +1,15 @@ +'use strict' + +require('dd-trace/init') +const http = require('http') + +const server = http.createServer((req, res) => { + res.end('hello world') // BREAKPOINT: / + setImmediate(() => { + server.close() + }) +}) + +server.listen(process.env.APP_PORT, () => { + process.send({ port: process.env.APP_PORT }) +}) diff --git a/integration-tests/debugger/unreffed.spec.js b/integration-tests/debugger/unreffed.spec.js new file mode 100644 index 00000000000..3ce9458f341 --- /dev/null +++ b/integration-tests/debugger/unreffed.spec.js @@ -0,0 +1,17 @@ +'use strict' + +const { assert } = require('chai') +const { setup } = require('./utils') + +describe('Dynamic Instrumentation', function () { + const t = setup() + + it('should not hinder the program from exiting', function (done) { + // Expect the instrumented app to exit after receiving an HTTP request. Will time out otherwise. + t.proc.on('exit', (code) => { + assert.strictEqual(code, 0) + done() + }) + t.axios.get(t.breakpoint.url) + }) +}) diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index c5760a0e9d4..b260e5eabe5 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -8,69 +8,68 @@ const getPort = require('get-port') const Axios = require('axios') const { createSandbox, FakeAgent, spawnProc } = require('../helpers') +const { generateProbeConfig } = require('../../packages/dd-trace/test/debugger/devtools_client/utils') +const BREAKPOINT_TOKEN = '// BREAKPOINT' const pollInterval = 1 module.exports = { pollInterval, - setup, - getBreakpointInfo + setup } -function setup () { - let sandbox, cwd, appPort, proc - const breakpoint = getBreakpointInfo(1) // `1` to disregard the `setup` function +function setup (env) { + let sandbox, cwd, appPort + const breakpoints = getBreakpointInfo(1) // `1` to disregard the `setup` function const t = { - breakpoint, + breakpoint: breakpoints[0], + breakpoints, + axios: null, appFile: null, agent: null, + + // Default to the first breakpoint in the file (normally there's only one) rcConfig: null, - triggerBreakpoint, - generateRemoteConfig, - generateProbeConfig + triggerBreakpoint: triggerBreakpoint.bind(null, breakpoints[0].url), + generateRemoteConfig: generateRemoteConfig.bind(null, breakpoints[0]), + generateProbeConfig: generateProbeConfig.bind(null, breakpoints[0]) } - function triggerBreakpoint () { + // Allow specific access to each breakpoint + for (let i = 0; i < breakpoints.length; i++) { + t.breakpoints[i] = { + rcConfig: null, + triggerBreakpoint: triggerBreakpoint.bind(null, breakpoints[i].url), + generateRemoteConfig: generateRemoteConfig.bind(null, breakpoints[i]), + generateProbeConfig: generateProbeConfig.bind(null, breakpoints[i]), + ...breakpoints[i] + } + } + + function triggerBreakpoint (url) { // Trigger the breakpoint once probe is successfully installed t.agent.on('debugger-diagnostics', ({ payload }) => { if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get('/foo') + t.axios.get(url) } }) } - function generateRemoteConfig (overrides = {}) { + function generateRemoteConfig (breakpoint, overrides = {}) { overrides.id = overrides.id || randomUUID() return { product: 'LIVE_DEBUGGING', id: `logProbe_${overrides.id}`, - config: generateProbeConfig(overrides) - } - } - - function generateProbeConfig (overrides = {}) { - overrides.capture = { maxReferenceDepth: 3, ...overrides.capture } - overrides.sampling = { snapshotsPerSecond: 5000, ...overrides.sampling } - return { - id: randomUUID(), - version: 0, - type: 'LOG_PROBE', - language: 'javascript', - where: { sourceFile: breakpoint.file, lines: [String(breakpoint.line)] }, - tags: [], - template: 'Hello World!', - segments: [{ str: 'Hello World!' }], - captureSnapshot: false, - evaluateAt: 'EXIT', - ...overrides + config: generateProbeConfig(breakpoint, overrides) } } before(async function () { - sandbox = await createSandbox(['fastify']) + sandbox = await createSandbox(['fastify']) // TODO: Make this dynamic cwd = sandbox.folder - t.appFile = join(cwd, ...breakpoint.file.split('/')) + // The sandbox uses the `integration-tests` folder as its root + t.appFile = join(cwd, 'debugger', breakpoints[0].file) }) after(async function () { @@ -78,17 +77,22 @@ function setup () { }) beforeEach(async function () { - t.rcConfig = generateRemoteConfig(breakpoint) + // Default to the first breakpoint in the file (normally there's only one) + t.rcConfig = generateRemoteConfig(breakpoints[0]) + // Allow specific access to each breakpoint + t.breakpoints.forEach((breakpoint) => { breakpoint.rcConfig = generateRemoteConfig(breakpoint) }) + appPort = await getPort() t.agent = await new FakeAgent().start() - proc = await spawnProc(t.appFile, { + t.proc = await spawnProc(t.appFile, { cwd, env: { APP_PORT: appPort, DD_DYNAMIC_INSTRUMENTATION_ENABLED: true, DD_TRACE_AGENT_PORT: t.agent.port, DD_TRACE_DEBUG: process.env.DD_TRACE_DEBUG, // inherit to make debugging the sandbox easier - DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: pollInterval + DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS: pollInterval, + ...env } }) t.axios = Axios.create({ @@ -97,7 +101,7 @@ function setup () { }) afterEach(async function () { - proc.kill() + t.proc.kill() await t.agent.stop() }) @@ -112,13 +116,19 @@ function getBreakpointInfo (stackIndex = 0) { .slice(0, -1) .split(':')[0] - // Then, find the corresponding file in which the breakpoint exists - const filename = basename(testFile).replace('.spec', '') - - // Finally, find the line number of the breakpoint - const line = readFileSync(join(__dirname, 'target-app', filename), 'utf8') - .split('\n') - .findIndex(line => line.includes('// BREAKPOINT')) + 1 + // Then, find the corresponding file in which the breakpoint(s) exists + const file = join('target-app', basename(testFile).replace('.spec', '')) + + // Finally, find the line number(s) of the breakpoint(s) + const lines = readFileSync(join(__dirname, file), 'utf8').split('\n') + const result = [] + for (let i = 0; i < lines.length; i++) { + const index = lines[i].indexOf(BREAKPOINT_TOKEN) + if (index !== -1) { + const url = lines[i].slice(index + BREAKPOINT_TOKEN.length + 1).trim() + result.push({ file, line: i + 1, url }) + } + } - return { file: `debugger/target-app/${filename}`, line } + return result } diff --git a/integration-tests/esbuild/build-and-test-typescript.mjs b/integration-tests/esbuild/build-and-test-typescript.mjs index bba9500cdd3..2fd2966384d 100755 --- a/integration-tests/esbuild/build-and-test-typescript.mjs +++ b/integration-tests/esbuild/build-and-test-typescript.mjs @@ -18,8 +18,8 @@ await esbuild.build({ external: [ 'graphql/language/visitor', 'graphql/language/printer', - 'graphql/utilities', - ], + 'graphql/utilities' + ] }) console.log('ok') // eslint-disable-line no-console diff --git a/integration-tests/esbuild/complex-app.mjs b/integration-tests/esbuild/complex-app.mjs index 5f097655eeb..5936a2c3983 100755 --- a/integration-tests/esbuild/complex-app.mjs +++ b/integration-tests/esbuild/complex-app.mjs @@ -4,10 +4,11 @@ import 'dd-trace/init.js' import assert from 'assert' import express from 'express' import redis from 'redis' -const app = express() -const PORT = 3000 import pg from 'pg' import PGP from 'pg-promise' // transient dep of 'pg' + +const app = express() +const PORT = 3000 const pgp = PGP() assert.equal(redis.Graph.name, 'Graph') diff --git a/integration-tests/helpers/fake-agent.js b/integration-tests/helpers/fake-agent.js index f1054720d92..4902c80d9a1 100644 --- a/integration-tests/helpers/fake-agent.js +++ b/integration-tests/helpers/fake-agent.js @@ -363,6 +363,14 @@ function buildExpressServer (agent) { }) }) + // Ensure that any failure inside of Express isn't swallowed and returned as a 500, but instead crashes the test + app.use((err, req, res, next) => { + if (!err) next() + process.nextTick(() => { + throw err + }) + }) + return app } diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index 09cc6c5bee4..22074c3af20 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -306,7 +306,7 @@ async function spawnPluginIntegrationTestProc (cwd, serverFile, agentPort, stdio NODE_OPTIONS: `--loader=${hookFile}`, DD_TRACE_AGENT_PORT: agentPort } - env = { ...env, ...additionalEnvArgs } + env = { ...process.env, ...env, ...additionalEnvArgs } return spawnProc(path.join(cwd, serverFile), { cwd, env diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index 571179276e1..fc274fb1480 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -34,12 +34,14 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { const NODE_OPTIONS = `--no-warnings --${arg} ${path.join(__dirname, '..', filename)}` useEnv({ NODE_OPTIONS }) - context('without DD_INJECTION_ENABLED', () => { - it('should initialize the tracer', () => doTest('init/trace.js', 'true\n')) - it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n')) - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - doTest('init/instrument.mjs', `${esmWorks}\n`)) - }) + if (currentVersionIsSupported) { + context('without DD_INJECTION_ENABLED', () => { + it('should initialize the tracer', () => doTest('init/trace.js', 'true\n')) + it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n')) + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + doTest('init/instrument.mjs', `${esmWorks}\n`)) + }) + } context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) @@ -87,8 +89,8 @@ function testRuntimeVersionChecks (arg, filename) { context('when node version is less than engines field', () => { useEnv({ NODE_OPTIONS }) - it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => - doTest('true\n')) + it('should not initialize the tracer', () => + doTest('false\n')) context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) @@ -165,17 +167,22 @@ describe('init.js', () => { testRuntimeVersionChecks('require', 'init.js') }) -// ESM is not supportable prior to Node.js 12 -if (semver.satisfies(process.versions.node, '>=12')) { +// ESM is not supportable prior to Node.js 12.17.0, 14.13.1 on the 14.x line, +// or on 18.0.0 in particular. +if ( + semver.satisfies(process.versions.node, '>=12.17.0') && + semver.satisfies(process.versions.node, '>=14.13.1') +) { describe('initialize.mjs', () => { useSandbox() stubTracerIfNeeded() context('as --loader', () => { - testInjectionScenarios('loader', 'initialize.mjs', true) + testInjectionScenarios('loader', 'initialize.mjs', + process.versions.node !== '18.0.0') testRuntimeVersionChecks('loader', 'initialize.mjs') }) - if (Number(process.versions.node.split('.')[0]) >= 18) { + if (semver.satisfies(process.versions.node, '>=20.6.0')) { context('as --import', () => { testInjectionScenarios('import', 'initialize.mjs', true) testRuntimeVersionChecks('loader', 'initialize.mjs') diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index 789019100da..d8d9f8231a6 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -33,7 +33,11 @@ const { TEST_SOURCE_START, TEST_CODE_OWNERS, TEST_SESSION_NAME, - TEST_LEVEL_EVENT_TYPES + TEST_LEVEL_EVENT_TYPES, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_LINE } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -57,7 +61,13 @@ describe('jest CommonJS', () => { let testOutput = '' before(async function () { - sandbox = await createSandbox(['jest', 'chai@v4', 'jest-jasmine2', 'jest-environment-jsdom'], true) + sandbox = await createSandbox([ + 'jest', + 'chai@v4', + 'jest-jasmine2', + 'jest-environment-jsdom', + 'office-addin-mock' + ], true) cwd = sandbox.folder startupTestFile = path.join(cwd, testFile) }) @@ -1469,6 +1479,48 @@ describe('jest CommonJS', () => { eventsPromise.then(done).catch(done) }) }) + + it('reports code coverage relative to the repository root, not working directory', (done) => { + receiver.setSettings({ + itr_enabled: true, + code_coverage: true, + tests_skipping: false + }) + + const codeCoveragesPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcov'), (payloads) => { + const coveredFiles = payloads + .flatMap(({ payload }) => payload) + .flatMap(({ content: { coverages } }) => coverages) + .flatMap(({ files }) => files) + .map(({ filename }) => filename) + + assert.includeMembers(coveredFiles, [ + 'ci-visibility/subproject/dependency.js', + 'ci-visibility/subproject/subproject-test.js' + ]) + }, 5000) + + childProcess = exec( + 'node ./node_modules/jest/bin/jest --config config-jest.js --rootDir ci-visibility/subproject', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + PROJECTS: JSON.stringify([{ + testMatch: ['**/subproject-test*'] + }]) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + codeCoveragesPromise.then(() => { + done() + }).catch(done) + }) + }) }) context('early flake detection', () => { @@ -2357,4 +2409,276 @@ describe('jest CommonJS', () => { }) }) }) + + context('dynamic instrumentation', () => { + it('does not activate dynamic instrumentation if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + + it('does not activate dynamic instrumentation if remote settings are disabled', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: false + }) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + + const notRetriedTest = tests.find(test => test.meta[TEST_NAME].includes('is not retried')) + + assert.notProperty(notRetriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '3' + }, + localVariable: { + type: 'number', + value: '2' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-not-hit-breakpoint', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + }) + + // This happens when using office-addin-mock + context('a test imports a file whose name includes a library we should bypass jest require cache for', () => { + it('does not crash', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false, + early_flake_detection: { + enabled: false + } + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + + assert.equal(tests.length, 1) + }) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'office-addin-mock/test' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + eventsPromise.then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + }) }) diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index 3fa11871204..d6d13673485 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -35,7 +35,11 @@ const { TEST_CODE_OWNERS, TEST_SESSION_NAME, TEST_LEVEL_EVENT_TYPES, - TEST_EARLY_FLAKE_ABORT_REASON + TEST_EARLY_FLAKE_ABORT_REASON, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_LINE } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -1085,6 +1089,45 @@ describe('mocha CommonJS', function () { }).catch(done) }) }) + + it('reports code coverage relative to the repository root, not working directory', (done) => { + receiver.setSettings({ + itr_enabled: true, + code_coverage: true, + tests_skipping: false + }) + + const codeCoveragesPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcov'), (payloads) => { + const coveredFiles = payloads + .flatMap(({ payload }) => payload) + .flatMap(({ content: { coverages } }) => coverages) + .flatMap(({ files }) => files) + .map(({ filename }) => filename) + + assert.includeMembers(coveredFiles, [ + 'ci-visibility/subproject/dependency.js', + 'ci-visibility/subproject/subproject-test.js' + ]) + }, 5000) + + childProcess = exec( + '../../node_modules/nyc/bin/nyc.js node ../../node_modules/mocha/bin/mocha subproject-test.js', + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...getCiVisAgentlessConfig(receiver.port) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + codeCoveragesPromise.then(() => { + done() + }).catch(done) + }) + }) }) context('early flake detection', () => { @@ -2105,4 +2148,252 @@ describe('mocha CommonJS', function () { }) }) }) + + context('dynamic instrumentation', () => { + it('does not activate dynamic instrumentation if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-hit-breakpoint' + ]) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + + it('does not activate dynamic instrumentation if remote settings are disabled', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: false + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-hit-breakpoint' + ]), + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + + const notRetriedTest = tests.find(test => test.meta[TEST_NAME].includes('is not retried')) + + assert.notProperty(notRetriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '3' + }, + localVariable: { + type: 'number', + value: '2' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }, 5000) + + childProcess = exec( + 'node ./ci-visibility/run-mocha.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-hit-breakpoint' + ]), + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + 'node ./ci-visibility/run-mocha.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-not-hit-breakpoint' + ]), + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + }) }) diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index 440cf13d637..3f6a49e01b7 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -123,11 +123,15 @@ versions.forEach((version) => { }) assert.includeMembers(testEvents.map(test => test.content.resource), [ - 'landing-page-test.js.should work with passing tests', - 'landing-page-test.js.should work with skipped tests', - 'landing-page-test.js.should work with fixme', - 'landing-page-test.js.should work with annotated tests', - 'todo-list-page-test.js.should work with failing tests', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with passing tests', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with skipped tests', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with fixme', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with annotated tests', + 'todo-list-page-test.js.playwright should work with failing tests', 'todo-list-page-test.js.should work with fixme root' ]) @@ -155,7 +159,7 @@ versions.forEach((version) => { assert.property(stepEvent.content.meta, 'playwright.step') }) const annotatedTest = testEvents.find(test => - test.content.resource === 'landing-page-test.js.should work with annotated tests' + test.content.resource.endsWith('should work with annotated tests') ) assert.propertyVal(annotatedTest.content.meta, 'test.memory.usage', 'low') @@ -187,8 +191,8 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const testEvents = events.filter(event => event.type === 'test') assert.includeMembers(testEvents.map(test => test.content.resource), [ - 'playwright-tests-ts/one-test.js.should work with passing tests', - 'playwright-tests-ts/one-test.js.should work with skipped tests' + 'playwright-tests-ts/one-test.js.playwright should work with passing tests', + 'playwright-tests-ts/one-test.js.playwright should work with skipped tests' ]) assert.include(testOutput, '1 passed') assert.include(testOutput, '1 skipped') @@ -263,16 +267,17 @@ versions.forEach((version) => { { playwright: { 'landing-page-test.js': [ - // 'should work with passing tests', // it will be considered new - 'should work with skipped tests', - 'should work with fixme', - 'should work with annotated tests' + // it will be considered new + // 'highest-level-describe leading and trailing spaces should work with passing tests', + 'highest-level-describe leading and trailing spaces should work with skipped tests', + 'highest-level-describe leading and trailing spaces should work with fixme', + 'highest-level-describe leading and trailing spaces should work with annotated tests' ], 'skipped-suite-test.js': [ 'should work with fixme root' ], 'todo-list-page-test.js': [ - 'should work with failing tests', + 'playwright should work with failing tests', 'should work with fixme root' ] } @@ -288,8 +293,7 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter(test => - test.resource === - 'landing-page-test.js.should work with passing tests' + test.resource.endsWith('should work with passing tests') ) newTests.forEach(test => { assert.propertyVal(test.meta, TEST_IS_NEW, 'true') @@ -337,16 +341,17 @@ versions.forEach((version) => { { playwright: { 'landing-page-test.js': [ - // 'should work with passing tests', // it will be considered new - 'should work with skipped tests', - 'should work with fixme', - 'should work with annotated tests' + // it will be considered new + // 'highest-level-describe leading and trailing spaces should work with passing tests', + 'highest-level-describe leading and trailing spaces should work with skipped tests', + 'highest-level-describe leading and trailing spaces should work with fixme', + 'highest-level-describe leading and trailing spaces should work with annotated tests' ], 'skipped-suite-test.js': [ 'should work with fixme root' ], 'todo-list-page-test.js': [ - 'should work with failing tests', + 'playwright should work with failing tests', 'should work with fixme root' ] } @@ -359,8 +364,7 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter(test => - test.resource === - 'landing-page-test.js.should work with passing tests' + test.resource.endsWith('should work with passing tests') ) newTests.forEach(test => { assert.notProperty(test.meta, TEST_IS_NEW) @@ -406,16 +410,18 @@ versions.forEach((version) => { { playwright: { 'landing-page-test.js': [ - 'should work with passing tests', - // 'should work with skipped tests', // new but not retried because it's skipped - // 'should work with fixme', // new but not retried because it's skipped - 'should work with annotated tests' + 'highest-level-describe leading and trailing spaces should work with passing tests', + // new but not retried because it's skipped + // 'highest-level-describe leading and trailing spaces should work with skipped tests', + // new but not retried because it's skipped + // 'highest-level-describe leading and trailing spaces should work with fixme', + 'highest-level-describe leading and trailing spaces should work with annotated tests' ], 'skipped-suite-test.js': [ 'should work with fixme root' ], 'todo-list-page-test.js': [ - 'should work with failing tests', + 'playwright should work with failing tests', 'should work with fixme root' ] } @@ -428,9 +434,8 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter(test => - test.resource === - 'landing-page-test.js.should work with skipped tests' || - test.resource === 'landing-page-test.js.should work with fixme' + test.resource.endsWith('should work with skipped tests') || + test.resource.endsWith('should work with fixme') ) // no retries assert.equal(newTests.length, 2) diff --git a/integration-tests/profiler/index.js b/integration-tests/profiler/index.js index f261c3d7f39..5a7fba3989c 100644 --- a/integration-tests/profiler/index.js +++ b/integration-tests/profiler/index.js @@ -21,4 +21,5 @@ function busyWait (ms) { }) } -setImmediate(async () => busyWait(500)) +const durationMs = Number.parseInt(process.env.TEST_DURATION_MS ?? '500') +setImmediate(async () => busyWait(durationMs)) diff --git a/integration-tests/profiler/profiler.spec.js b/integration-tests/profiler/profiler.spec.js index f4760a0a167..80be4c8fd36 100644 --- a/integration-tests/profiler/profiler.spec.js +++ b/integration-tests/profiler/profiler.spec.js @@ -75,6 +75,12 @@ 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') } +} + +async function readLatestFile (cwd, pattern) { const dirEntries = await fs.readdir(cwd) // Get the latest file matching the pattern const pprofEntries = dirEntries.filter(name => pattern.test(name)) @@ -83,9 +89,7 @@ async function getLatestProfile (cwd, pattern) { .map(name => ({ name, modified: fsync.statSync(path.join(cwd, name), { bigint: true }).mtimeNs })) .reduce((a, b) => a.modified > b.modified ? a : b) .name - const pprofGzipped = await fs.readFile(path.join(cwd, pprofEntry)) - const pprofUnzipped = zlib.gunzipSync(pprofGzipped) - return { profile: Profile.decode(pprofUnzipped), encoded: pprofGzipped.toString('base64') } + return await fs.readFile(path.join(cwd, pprofEntry)) } function expectTimeout (messagePromise, allowErrors = false) { @@ -105,10 +109,9 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args const proc = fork(path.join(cwd, scriptFilePath), args, { cwd, env: { - DD_PROFILING_PROFILERS: 'wall', DD_PROFILING_EXPORTERS: 'file', DD_PROFILING_ENABLED: 1, - DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED: 1 + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED: 0 // capture all events } }) @@ -130,6 +133,7 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args const events = [] for (const sample of profile.sample) { let ts, event, host, address, port, name, spanId, localRootSpanId + const unexpectedLabels = [] for (const label of sample.label) { switch (label.key) { case tsKey: ts = label.num; break @@ -140,23 +144,28 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args case portKey: port = label.num; break case spanIdKey: spanId = label.str; break case localRootSpanIdKey: localRootSpanId = label.str; break - default: assert.fail(`Unexpected label key ${label.key} ${strings.strings[label.key]} ${encoded}`) + default: unexpectedLabels.push(label.key) } } - // Timestamp must be defined and be between process start and end time - assert.isDefined(ts, encoded) - assert.isTrue(ts <= procEnd, encoded) - assert.isTrue(ts >= procStart, encoded) - if (process.platform !== 'win32') { - assert.isDefined(spanId, encoded) - assert.isDefined(localRootSpanId, encoded) - } else { - assert.isUndefined(spanId, encoded) - assert.isUndefined(localRootSpanId, encoded) - } // Gather only DNS events; ignore sporadic GC events if (event === eventValue) { + // Timestamp must be defined and be between process start and end time + assert.isDefined(ts, encoded) + assert.isTrue(ts <= procEnd, encoded) + assert.isTrue(ts >= procStart, encoded) + if (process.platform !== 'win32') { + assert.isDefined(spanId, encoded) + assert.isDefined(localRootSpanId, encoded) + } else { + assert.isUndefined(spanId, encoded) + assert.isUndefined(localRootSpanId, encoded) + } assert.isDefined(name, encoded) + if (unexpectedLabels.length > 0) { + const labelsStr = JSON.stringify(unexpectedLabels) + const labelsStrStr = unexpectedLabels.map(k => strings.strings[k]).join(',') + assert.fail(`Unexpected labels: ${labelsStr}\n${labelsStrStr}\n${encoded}`) + } // Exactly one of these is defined assert.isTrue(!!address !== !!host, encoded) const ev = { name: strings.strings[name] } @@ -205,18 +214,18 @@ describe('profiler', () => { const proc = fork(path.join(cwd, 'profiler/codehotspots.js'), { cwd, env: { - DD_PROFILING_PROFILERS: 'wall', DD_PROFILING_EXPORTERS: 'file', - DD_PROFILING_ENABLED: 1, - DD_PROFILING_CODEHOTSPOTS_ENABLED: 1, - DD_PROFILING_ENDPOINT_COLLECTION_ENABLED: 1, - DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED: 1 + DD_PROFILING_ENABLED: 1 } }) await processExitPromise(proc, 30000) const procEnd = BigInt(Date.now() * 1000000) + // 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 }) + const { profile, encoded } = await getLatestProfile(cwd, /^wall_.+\.pprof$/) // We check the profile for following invariants: @@ -544,6 +553,71 @@ describe('profiler', () => { }) }) + context('Profiler API telemetry', () => { + beforeEach(async () => { + agent = await new FakeAgent().start() + }) + + afterEach(async () => { + proc.kill() + await agent.stop() + }) + + it('sends profiler API telemetry', () => { + proc = fork(profilerTestFile, { + cwd, + env: { + DD_TRACE_AGENT_PORT: agent.port, + DD_PROFILING_ENABLED: 1, + DD_PROFILING_UPLOAD_PERIOD: 1, + TEST_DURATION_MS: 2500 + } + }) + + let requestCount = 0 + let pointsCount = 0 + + const checkMetrics = agent.assertTelemetryReceived(({ _, payload }) => { + const pp = payload.payload + assert.equal(pp.namespace, 'profilers') + const series = pp.series + assert.lengthOf(series, 2) + assert.equal(series[0].metric, 'profile_api.requests') + assert.equal(series[0].type, 'count') + // There's a race between metrics and on-shutdown profile, so metric + // value will be between 2 and 3 + requestCount = series[0].points[0][1] + assert.isAtLeast(requestCount, 2) + assert.isAtMost(requestCount, 3) + + assert.equal(series[1].metric, 'profile_api.responses') + assert.equal(series[1].type, 'count') + assert.include(series[1].tags, 'status_code:200') + + // Same number of requests and responses + assert.equal(series[1].points[0][1], requestCount) + }, timeout, 'generate-metrics') + + const checkDistributions = agent.assertTelemetryReceived(({ _, payload }) => { + const pp = payload.payload + assert.equal(pp.namespace, 'profilers') + const series = pp.series + assert.lengthOf(series, 2) + assert.equal(series[0].metric, 'profile_api.bytes') + assert.equal(series[1].metric, 'profile_api.ms') + + // Same number of points + pointsCount = series[0].points.length + assert.equal(pointsCount, series[1].points.length) + }, timeout, 'distributions') + + return Promise.all([checkProfiles(agent, proc, timeout), checkMetrics, checkDistributions]).then(() => { + // Same number of requests and points + assert.equal(requestCount, pointsCount) + }) + }) + }) + function forkSsi (args, whichEnv) { const profilerEnablingEnv = whichEnv ? { DD_PROFILING_ENABLED: 'auto' } : { DD_INJECTION_ENABLED: 'profiler' } return fork(ssiTestFile, args, { diff --git a/integration-tests/standalone-asm.spec.js b/integration-tests/standalone-asm.spec.js index 4e57b25bad6..fec30ad012b 100644 --- a/integration-tests/standalone-asm.spec.js +++ b/integration-tests/standalone-asm.spec.js @@ -81,33 +81,42 @@ describe('Standalone ASM', () => { }) }) - it('should keep second req because RateLimiter allows 1 req/min and discard the next', async () => { - // 1st req kept because waf init - // 2nd req kept because it's the first one hitting RateLimiter - // next in the first minute are dropped - await doWarmupRequests(proc) - - return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { + it('should keep fifth req because RateLimiter allows 1 req/min', async () => { + const promise = curlAndAssertMessage(agent, proc, ({ headers, payload }) => { assert.propertyVal(headers, 'datadog-client-computed-stats', 'yes') assert.isArray(payload) - assert.strictEqual(payload.length, 4) - - const secondReq = payload[1] - assert.isArray(secondReq) - assert.strictEqual(secondReq.length, 5) + if (payload.length === 4) { + assertKeep(payload[0][0]) + assertDrop(payload[1][0]) + assertDrop(payload[2][0]) + assertDrop(payload[3][0]) + + // req after a minute + } else { + const fifthReq = payload[0] + assert.isArray(fifthReq) + assert.strictEqual(fifthReq.length, 5) + + const { meta, metrics } = fifthReq[0] + assert.notProperty(meta, 'manual.keep') + assert.notProperty(meta, '_dd.p.appsec') + + assert.propertyVal(metrics, '_sampling_priority_v1', AUTO_KEEP) + assert.propertyVal(metrics, '_dd.apm.enabled', 0) + } + }, 70000, 2) - const { meta, metrics } = secondReq[0] - assert.notProperty(meta, 'manual.keep') - assert.notProperty(meta, '_dd.p.appsec') + // 1st req kept because waf init + // next in the first minute are dropped + // 5nd req kept because RateLimiter allows 1 req/min + await doWarmupRequests(proc) - assert.propertyVal(metrics, '_sampling_priority_v1', AUTO_KEEP) - assert.propertyVal(metrics, '_dd.apm.enabled', 0) + await new Promise(resolve => setTimeout(resolve, 60000)) - assertDrop(payload[2][0]) + await curl(proc) - assertDrop(payload[3][0]) - }) - }) + return promise + }).timeout(70000) it('should keep attack requests', async () => { await doWarmupRequests(proc) diff --git a/integration-tests/test-api-manual.spec.js b/integration-tests/test-api-manual.spec.js index 419c7c736c5..c403168206a 100644 --- a/integration-tests/test-api-manual.spec.js +++ b/integration-tests/test-api-manual.spec.js @@ -10,24 +10,20 @@ const { getCiVisAgentlessConfig } = require('./helpers') const { FakeCiVisIntake } = require('./ci-visibility-intake') -const webAppServer = require('./ci-visibility/web-app-server') const { TEST_STATUS } = require('../packages/dd-trace/src/plugins/util/test') describe('test-api-manual', () => { - let sandbox, cwd, receiver, childProcess, webAppPort + let sandbox, cwd, receiver, childProcess before(async () => { sandbox = await createSandbox([], true) cwd = sandbox.folder - webAppPort = await getPort() - webAppServer.listen(webAppPort) }) after(async () => { await sandbox.remove() - await new Promise(resolve => webAppServer.close(resolve)) }) beforeEach(async function () { diff --git a/integration-tests/test-optimization-startup.spec.js b/integration-tests/test-optimization-startup.spec.js new file mode 100644 index 00000000000..a15d49cf8ef --- /dev/null +++ b/integration-tests/test-optimization-startup.spec.js @@ -0,0 +1,84 @@ +'use strict' + +const { exec } = require('child_process') + +const getPort = require('get-port') +const { assert } = require('chai') + +const { createSandbox } = require('./helpers') +const { FakeCiVisIntake } = require('./ci-visibility-intake') + +const packageManagers = ['yarn', 'npm', 'pnpm'] + +describe('test optimization startup', () => { + let sandbox, cwd, receiver, childProcess, processOutput + + before(async () => { + sandbox = await createSandbox(packageManagers, true) + cwd = sandbox.folder + }) + + after(async () => { + await sandbox.remove() + }) + + beforeEach(async function () { + processOutput = '' + const port = await getPort() + receiver = await new FakeCiVisIntake(port).start() + }) + + afterEach(async () => { + childProcess.kill() + await receiver.stop() + }) + + packageManagers.forEach(packageManager => { + it(`skips initialization for ${packageManager}`, (done) => { + childProcess = exec(`node ./node_modules/.bin/${packageManager} -v`, + { + cwd, + env: { + ...process.env, + NODE_OPTIONS: '-r dd-trace/ci/init', + DD_TRACE_DEBUG: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.stdout.on('data', (chunk) => { + processOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + assert.include(processOutput, 'dd-trace is not initialized in a package manager') + done() + }) + }) + }) + + it('does not skip initialization for non package managers', (done) => { + childProcess = exec('node -e "console.log(\'hello!\')"', + { + cwd, + env: { + ...process.env, + NODE_OPTIONS: '-r dd-trace/ci/init', + DD_TRACE_DEBUG: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.stdout.on('data', (chunk) => { + processOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + assert.include(processOutput, 'hello!') + assert.notInclude(processOutput, 'dd-trace is not initialized in a package manager') + done() + }) + }) +}) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index de38feee9da..2007baefd52 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -24,7 +24,11 @@ const { TEST_NAME, TEST_EARLY_FLAKE_ENABLED, TEST_EARLY_FLAKE_ABORT_REASON, - TEST_SUITE + TEST_SUITE, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE, + DI_DEBUG_ERROR_SNAPSHOT_ID } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -896,5 +900,251 @@ versions.forEach((version) => { }) }) }) + + // dynamic instrumentation only supported from >=2.0.0 + if (version === 'latest') { + context('dynamic instrumentation', () => { + it('does not activate it if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + + it('does not activate dynamic instrumentation if remote settings are disabled', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: false + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/vitest-tests/bad-sum.mjs' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + + const notRetriedTest = tests.find(test => test.meta[TEST_NAME].includes('is not retried')) + + assert.notProperty(notRetriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '2' + }, + localVar: { + type: 'number', + value: '10' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + flaky_test_retries_enabled: true, + di_enabled: true + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/vitest-tests/bad-sum.mjs' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/breakpoint-not-hit*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + }) + } }) }) diff --git a/loader-hook.mjs b/loader-hook.mjs index 40bbdbade81..fc2a250e3a1 100644 --- a/loader-hook.mjs +++ b/loader-hook.mjs @@ -1 +1,5 @@ +// TODO(bengl): Not sure why `import/export` fails on this line, but it's just +// a passthrough to another module so it should be fine. Disabling for now. + +// eslint-disable-next-line import/export export * from 'import-in-the-middle/hook.mjs' diff --git a/package.json b/package.json index 54417923020..9b0abdb34db 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "env": "bash ./plugin-env", "preinstall": "node scripts/preinstall.js", "bench": "node benchmark", - "bench:profiler": "node benchmark/profiler", "bench:e2e": "SERVICES=mongo yarn services && cd benchmark/e2e && node benchmark-run.js --duration=30", "bench:e2e:ci-visibility": "node benchmark/e2e-ci/benchmark-run.js", "type:doc": "cd docs && yarn && yarn build", "type:test": "cd docs && yarn && yarn test", "lint": "node scripts/check_licenses.js && eslint . && yarn audit", "lint-fix": "node scripts/check_licenses.js && eslint . --fix && yarn audit", + "release:proposal": "node scripts/release/proposal", "services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services", "test": "SERVICES=* yarn services && mocha --expose-gc 'packages/dd-trace/test/setup/node.js' 'packages/*/test/**/*.spec.js'", "test:appsec": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" --exclude \"packages/dd-trace/test/appsec/**/*.plugin.spec.js\" \"packages/dd-trace/test/appsec/**/*.spec.js\"", @@ -81,20 +81,20 @@ "node": ">=18" }, "dependencies": { - "@datadog/libdatadog": "^0.2.2", - "@datadog/native-appsec": "8.2.1", - "@datadog/native-iast-rewriter": "2.5.0", + "@datadog/libdatadog": "^0.3.0", + "@datadog/native-appsec": "8.3.0", + "@datadog/native-iast-rewriter": "2.6.1", "@datadog/native-iast-taint-tracking": "3.2.0", - "@datadog/native-metrics": "^3.0.1", + "@datadog/native-metrics": "^3.1.0", "@datadog/pprof": "5.4.1", "@datadog/sketches-js": "^2.1.0", + "@isaacs/ttlcache": "^1.4.1", "@opentelemetry/api": ">=1.0.0 <1.9.0", "@opentelemetry/core": "^1.14.0", "crypto-randomuuid": "^1.0.0", "dc-polyfill": "^0.1.4", "ignore": "^5.2.4", "import-in-the-middle": "1.11.2", - "int64-buffer": "^0.1.9", "istanbul-lib-coverage": "3.2.0", "jest-docblock": "^29.7.0", "koalas": "^1.0.2", @@ -102,20 +102,23 @@ "lodash.sortby": "^4.7.0", "lru-cache": "^7.14.0", "module-details-from-path": "^1.0.3", - "msgpack-lite": "^0.1.26", "opentracing": ">=0.12.1", - "path-to-regexp": "^0.1.10", + "path-to-regexp": "^0.1.12", "pprof-format": "^2.1.0", "protobufjs": "^7.2.5", "retry": "^0.13.1", "rfdc": "^1.3.1", "semver": "^7.5.4", "shell-quote": "^1.8.1", + "source-map": "^0.7.4", "tlhunter-sorted-set": "^0.1.0" }, "devDependencies": { "@apollo/server": "^4.11.0", - "@types/node": "^16.18.103", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.11.1", + "@stylistic/eslint-plugin-js": "^2.8.0", + "@types/node": "^16.0.0", "autocannon": "^4.5.2", "aws-sdk": "^2.1446.0", "axios": "^1.7.4", @@ -133,14 +136,17 @@ "eslint-plugin-mocha": "^10.4.3", "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.4.0", - "express": "^4.18.2", + "express": "^4.21.2", "get-port": "^3.2.0", "glob": "^7.1.6", + "globals": "^15.10.0", "graphql": "0.13.2", + "int64-buffer": "^0.1.9", "jszip": "^3.5.0", "knex": "^2.4.2", "mkdirp": "^3.0.1", "mocha": "^9", + "msgpack-lite": "^0.1.26", "multer": "^1.4.5-lts.1", "nock": "^11.3.3", "nyc": "^15.1.0", @@ -149,6 +155,7 @@ "sinon": "^16.1.3", "sinon-chai": "^3.7.0", "tap": "^16.3.7", - "tiktoken": "^1.0.15" + "tiktoken": "^1.0.15", + "yaml": "^2.5.0" } } diff --git a/packages/datadog-core/src/storage.js b/packages/datadog-core/src/storage.js index d28420ed259..15c9fff239c 100644 --- a/packages/datadog-core/src/storage.js +++ b/packages/datadog-core/src/storage.js @@ -2,12 +2,47 @@ const { AsyncLocalStorage } = require('async_hooks') +class DatadogStorage { + constructor () { + this._storage = new AsyncLocalStorage() + } + + disable () { + this._storage.disable() + } + + enterWith (store) { + const handle = {} + stores.set(handle, store) + this._storage.enterWith(handle) + } + + exit (callback, ...args) { + this._storage.exit(callback, ...args) + } + + getStore () { + const handle = this._storage.getStore() + return stores.get(handle) + } + + run (store, fn, ...args) { + const prior = this._storage.getStore() + this.enterWith(store) + try { + return Reflect.apply(fn, null, args) + } finally { + this._storage.enterWith(prior) + } + } +} + const storages = Object.create(null) -const legacyStorage = new AsyncLocalStorage() +const legacyStorage = new DatadogStorage() const storage = function (namespace) { if (!storages[namespace]) { - storages[namespace] = new AsyncLocalStorage() + storages[namespace] = new DatadogStorage() } return storages[namespace] } @@ -18,4 +53,6 @@ storage.exit = legacyStorage.exit.bind(legacyStorage) storage.getStore = legacyStorage.getStore.bind(legacyStorage) storage.run = legacyStorage.run.bind(legacyStorage) +const stores = new WeakMap() + module.exports = storage diff --git a/packages/datadog-core/test/storage.spec.js b/packages/datadog-core/test/storage.spec.js index 89839f1fca3..e5bca4e7d5d 100644 --- a/packages/datadog-core/test/storage.spec.js +++ b/packages/datadog-core/test/storage.spec.js @@ -3,6 +3,7 @@ require('../../dd-trace/test/setup/tap') const { expect } = require('chai') +const { executionAsyncResource } = require('async_hooks') const storage = require('../src/storage') describe('storage', () => { @@ -47,4 +48,16 @@ describe('storage', () => { it('should return the same storage for a namespace', () => { expect(storage('test')).to.equal(testStorage) }) + + it('should not have its store referenced by the underlying async resource', () => { + const resource = executionAsyncResource() + + testStorage.enterWith({ internal: 'internal' }) + + for (const sym of Object.getOwnPropertySymbols(resource)) { + if (sym.toString() === 'Symbol(kResourceStore)' && resource[sym]) { + expect(resource[sym]).to.not.have.property('internal') + } + } + }) }) diff --git a/packages/datadog-instrumentations/src/aerospike.js b/packages/datadog-instrumentations/src/aerospike.js index 724c518e050..497a64aaf80 100644 --- a/packages/datadog-instrumentations/src/aerospike.js +++ b/packages/datadog-instrumentations/src/aerospike.js @@ -40,7 +40,7 @@ function wrapProcess (process) { addHook({ name: 'aerospike', file: 'lib/commands/command.js', - versions: ['^3.16.2', '4', '5'] + versions: ['4', '5'] }, commandFactory => { return shimmer.wrapFunction(commandFactory, f => wrapCreateCommand(f)) diff --git a/packages/datadog-instrumentations/src/azure-functions.js b/packages/datadog-instrumentations/src/azure-functions.js index 2527d9afb3f..791d3a9025f 100644 --- a/packages/datadog-instrumentations/src/azure-functions.js +++ b/packages/datadog-instrumentations/src/azure-functions.js @@ -6,7 +6,7 @@ const { const shimmer = require('../../datadog-shimmer') const dc = require('dc-polyfill') -const azureFunctionsChannel = dc.tracingChannel('datadog:azure-functions:invoke') +const azureFunctionsChannel = dc.tracingChannel('datadog:azure:functions:invoke') addHook({ name: '@azure/functions', versions: ['>=4'] }, azureFunction => { const { app } = azureFunction diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 0f84d717381..7b9a2db5a02 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -126,6 +126,20 @@ function getTestStatusFromRetries (testStatuses) { return 'pass' } +function getErrorFromCucumberResult (cucumberResult) { + if (!cucumberResult.message) { + return + } + + const [message] = cucumberResult.message.split('\n') + const error = new Error(message) + if (cucumberResult.exception) { + error.type = cucumberResult.exception.type + } + error.stack = cucumberResult.message + return error +} + function getChannelPromise (channelToPublishTo) { return new Promise(resolve => { sessionAsyncResource.runInAsyncScope(() => { @@ -230,9 +244,19 @@ function wrapRun (pl, isLatestVersion) { if (testCase?.testCaseFinished) { const { testCaseFinished: { willBeRetried } } = testCase if (willBeRetried) { // test case failed and will be retried + let error + try { + const cucumberResult = this.getWorstStepResult() + error = getErrorFromCucumberResult(cucumberResult) + } catch (e) { + // ignore error + } + const failedAttemptAsyncResource = numAttemptToAsyncResource.get(numAttempt) + const isRetry = numAttempt++ > 0 failedAttemptAsyncResource.runInAsyncScope(() => { - testRetryCh.publish(numAttempt++ > 0) // the current span will be finished and a new one will be created + // the current span will be finished and a new one will be created + testRetryCh.publish({ isRetry, error }) }) const newAsyncResource = new AsyncResource('bound-anonymous-fn') @@ -251,7 +275,7 @@ function wrapRun (pl, isLatestVersion) { }) promise.finally(() => { const result = this.getWorstStepResult() - const { status, skipReason, errorMessage } = isLatestVersion + const { status, skipReason } = isLatestVersion ? getStatusFromResultLatest(result) : getStatusFromResult(result) @@ -270,8 +294,10 @@ function wrapRun (pl, isLatestVersion) { } const attemptAsyncResource = numAttemptToAsyncResource.get(numAttempt) + const error = getErrorFromCucumberResult(result) + attemptAsyncResource.runInAsyncScope(() => { - testFinishCh.publish({ status, skipReason, errorMessage, isNew, isEfdRetry, isFlakyRetry: numAttempt > 0 }) + testFinishCh.publish({ status, skipReason, error, isNew, isEfdRetry, isFlakyRetry: numAttempt > 0 }) }) }) return promise diff --git a/packages/datadog-instrumentations/src/express.js b/packages/datadog-instrumentations/src/express.js index b093eab7830..1b328ba4c13 100644 --- a/packages/datadog-instrumentations/src/express.js +++ b/packages/datadog-instrumentations/src/express.js @@ -29,7 +29,7 @@ function wrapResponseJson (json) { obj = arguments[1] } - responseJsonChannel.publish({ req: this.req, body: obj }) + responseJsonChannel.publish({ req: this.req, res: this, body: obj }) } return json.apply(this, arguments) @@ -59,8 +59,6 @@ function wrapResponseRender (render) { addHook({ name: 'express', versions: ['>=4'] }, express => { shimmer.wrap(express.application, 'handle', wrapHandle) - shimmer.wrap(express.Router, 'use', wrapRouterMethod) - shimmer.wrap(express.Router, 'route', wrapRouterMethod) shimmer.wrap(express.response, 'json', wrapResponseJson) shimmer.wrap(express.response, 'jsonp', wrapResponseJson) @@ -69,6 +67,20 @@ addHook({ name: 'express', versions: ['>=4'] }, express => { return express }) +addHook({ name: 'express', versions: ['4'] }, express => { + shimmer.wrap(express.Router, 'use', wrapRouterMethod) + shimmer.wrap(express.Router, 'route', wrapRouterMethod) + + return express +}) + +addHook({ name: 'express', versions: ['>=5.0.0'] }, express => { + shimmer.wrap(express.Router.prototype, 'use', wrapRouterMethod) + shimmer.wrap(express.Router.prototype, 'route', wrapRouterMethod) + + return express +}) + const queryParserReadCh = channel('datadog:query:read:finish') function publishQueryParsedAndNext (req, res, next) { @@ -88,7 +100,7 @@ function publishQueryParsedAndNext (req, res, next) { addHook({ name: 'express', - versions: ['>=4'], + versions: ['4'], file: 'lib/middleware/query.js' }, query => { return shimmer.wrapFunction(query, query => function () { @@ -129,7 +141,29 @@ addHook({ name: 'express', versions: ['>=4.0.0 <4.3.0'] }, express => { return express }) -addHook({ name: 'express', versions: ['>=4.3.0'] }, express => { +addHook({ name: 'express', versions: ['>=4.3.0 <5.0.0'] }, express => { shimmer.wrap(express.Router, 'process_params', wrapProcessParamsMethod(2)) return express }) + +const queryReadCh = channel('datadog:express:query:finish') + +addHook({ name: 'express', file: ['lib/request.js'], versions: ['>=5.0.0'] }, request => { + const requestDescriptor = Object.getOwnPropertyDescriptor(request, 'query') + + shimmer.wrap(requestDescriptor, 'get', function (originalGet) { + return function wrappedGet () { + const query = originalGet.apply(this, arguments) + + if (queryReadCh.hasSubscribers && query) { + queryReadCh.publish({ query }) + } + + return query + } + }) + + Object.defineProperty(request, 'query', requestDescriptor) + + return request +}) diff --git a/packages/datadog-instrumentations/src/handlebars.js b/packages/datadog-instrumentations/src/handlebars.js new file mode 100644 index 00000000000..333889db3c6 --- /dev/null +++ b/packages/datadog-instrumentations/src/handlebars.js @@ -0,0 +1,40 @@ +'use strict' + +const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') + +const handlebarsCompileCh = channel('datadog:handlebars:compile:start') +const handlebarsRegisterPartialCh = channel('datadog:handlebars:register-partial:start') + +function wrapCompile (compile) { + return function wrappedCompile (source) { + if (handlebarsCompileCh.hasSubscribers) { + handlebarsCompileCh.publish({ source }) + } + + return compile.apply(this, arguments) + } +} + +function wrapRegisterPartial (registerPartial) { + return function wrappedRegisterPartial (name, partial) { + if (handlebarsRegisterPartialCh.hasSubscribers) { + handlebarsRegisterPartialCh.publish({ partial }) + } + + return registerPartial.apply(this, arguments) + } +} + +addHook({ name: 'handlebars', file: 'dist/cjs/handlebars/compiler/compiler.js', versions: ['>=4.0.0'] }, compiler => { + shimmer.wrap(compiler, 'compile', wrapCompile) + shimmer.wrap(compiler, 'precompile', wrapCompile) + + return compiler +}) + +addHook({ name: 'handlebars', file: 'dist/cjs/handlebars/base.js', versions: ['>=4.0.0'] }, base => { + shimmer.wrap(base.HandlebarsEnvironment.prototype, 'registerPartial', wrapRegisterPartial) + + return base +}) diff --git a/packages/datadog-instrumentations/src/helpers/bundler-register.js b/packages/datadog-instrumentations/src/helpers/bundler-register.js index a5dfead9669..6c11329bc36 100644 --- a/packages/datadog-instrumentations/src/helpers/bundler-register.js +++ b/packages/datadog-instrumentations/src/helpers/bundler-register.js @@ -30,12 +30,12 @@ dc.subscribe(CHANNEL, (payload) => { try { hooks[payload.package]() } catch (err) { - log.error(`esbuild-wrapped ${payload.package} missing in list of hooks`) + log.error('esbuild-wrapped %s missing in list of hooks', payload.package) throw err } if (!instrumentations[payload.package]) { - log.error(`esbuild-wrapped ${payload.package} missing in list of instrumentations`) + log.error('esbuild-wrapped %s missing in list of instrumentations', payload.package) return } @@ -47,7 +47,7 @@ dc.subscribe(CHANNEL, (payload) => { loadChannel.publish({ name, version: payload.version, file }) payload.module = hook(payload.module, payload.version) } catch (e) { - log.error(e) + log.error('Error executing bundler hook', e) } } }) diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 21bdf21298e..4ea35f50218 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -19,6 +19,8 @@ module.exports = { '@jest/test-sequencer': () => require('../jest'), '@jest/transform': () => require('../jest'), '@koa/router': () => require('../koa'), + '@langchain/core': () => require('../langchain'), + '@langchain/openai': () => require('../langchain'), '@node-redis/client': () => require('../redis'), '@opensearch-project/opensearch': () => require('../opensearch'), '@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'), @@ -51,6 +53,7 @@ module.exports = { 'generic-pool': () => require('../generic-pool'), graphql: () => require('../graphql'), grpc: () => require('../grpc'), + handlebars: () => require('../handlebars'), hapi: () => require('../hapi'), http: () => require('../http'), http2: () => require('../http2'), @@ -66,6 +69,7 @@ module.exports = { koa: () => require('../koa'), 'koa-router': () => require('../koa'), kafkajs: () => require('../kafkajs'), + langchain: () => require('../langchain'), ldapjs: () => require('../ldapjs'), 'limitd-client': () => require('../limitd-client'), lodash: () => require('../lodash'), @@ -105,8 +109,8 @@ module.exports = { 'promise-js': () => require('../promise-js'), promise: () => require('../promise'), protobufjs: () => require('../protobufjs'), + pug: () => require('../pug'), q: () => require('../q'), - qs: () => require('../qs'), redis: () => require('../redis'), restify: () => require('../restify'), rhea: () => require('../rhea'), diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 4b4185423c0..5a28f066c1f 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -7,7 +7,7 @@ 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/telemetry/init-telemetry') +const telemetry = require('../../../dd-trace/src/guardrails/telemetry') const { DD_TRACE_DISABLED_INSTRUMENTATIONS = '', @@ -103,8 +103,7 @@ for (const packageName of names) { try { version = version || getVersion(moduleBaseDir) } catch (e) { - log.error(`Error getting version for "${name}": ${e.message}`) - log.error(e) + log.error('Error getting version for "%s": %s', name, e.message, e) continue } if (typeof namesAndSuccesses[`${name}@${version}`] === 'undefined') { @@ -146,7 +145,7 @@ for (const packageName of names) { `integration:${name}`, `integration_version:${version}` ]) - log.info(`Found incompatible integration version: ${nameVersion}`) + log.info('Found incompatible integration version: %s', nameVersion) seenCombo.add(nameVersion) } } diff --git a/packages/datadog-instrumentations/src/http/client.js b/packages/datadog-instrumentations/src/http/client.js index 29547df61dc..6ab01a34513 100644 --- a/packages/datadog-instrumentations/src/http/client.js +++ b/packages/datadog-instrumentations/src/http/client.js @@ -39,7 +39,7 @@ function patch (http, methodName) { try { args = normalizeArgs.apply(null, arguments) } catch (e) { - log.error(e) + log.error('Error normalising http req arguments', e) return request.apply(this, arguments) } diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index e006f311dc3..2d27fdc0acb 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -127,11 +127,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { if (repositoryRoot) { this.testSourceFile = getTestSuitePath(context.testPath, repositoryRoot) + this.repositoryRoot = repositoryRoot } this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled this.isFlakyTestRetriesEnabled = this.testEnvironmentOptions._ddIsFlakyTestRetriesEnabled this.flakyTestRetriesCount = this.testEnvironmentOptions._ddFlakyTestRetriesCount + this.isDiEnabled = this.testEnvironmentOptions._ddIsDiEnabled if (this.isEarlyFlakeDetectionEnabled) { const hasKnownTests = !!knownTests.jest @@ -236,7 +238,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { name: removeEfdStringFromTestName(testName), suite: this.testSuite, testSourceFile: this.testSourceFile, - runner: 'jest-circus', displayName: this.displayName, testParameters, frameworkVersion: jestVersion, @@ -273,13 +274,23 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } if (event.name === 'test_done') { + const probe = {} const asyncResource = asyncResources.get(event.test) asyncResource.runInAsyncScope(() => { let status = 'pass' if (event.test.errors && event.test.errors.length) { status = 'fail' - const formattedError = formatJestError(event.test.errors[0]) - testErrCh.publish(formattedError) + const numRetries = this.global[RETRY_TIMES] + const numTestExecutions = event.test?.invocations + const willBeRetried = numRetries > 0 && numTestExecutions - 1 < numRetries + + const error = formatJestError(event.test.errors[0]) + testErrCh.publish({ + error, + willBeRetried, + probe, + isDiEnabled: this.isDiEnabled + }) } testRunFinishCh.publish({ status, @@ -301,6 +312,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } }) + if (probe.setProbePromise) { + await probe.setProbePromise + } } if (event.name === 'test_skip' || event.name === 'test_todo') { const asyncResource = new AsyncResource('bound-anonymous-fn') @@ -309,7 +323,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { name: getJestTestName(event.test), suite: this.testSuite, testSourceFile: this.testSourceFile, - runner: 'jest-circus', displayName: this.displayName, frameworkVersion: jestVersion, testStartLine: getTestLineStart(event.test.asyncError, this.testSuite) @@ -444,7 +457,7 @@ function cliWrapper (cli, jestVersion) { earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold } } catch (err) { - log.error(err) + log.error('Jest library configuration error', err) } if (isEarlyFlakeDetectionEnabled) { @@ -465,7 +478,7 @@ function cliWrapper (cli, jestVersion) { isEarlyFlakeDetectionEnabled = false } } catch (err) { - log.error(err) + log.error('Jest known tests error', err) } } @@ -484,7 +497,7 @@ function cliWrapper (cli, jestVersion) { skippableSuites = receivedSkippableSuites } } catch (err) { - log.error(err) + log.error('Jest test-suite skippable error', err) } } @@ -667,10 +680,13 @@ function jestAdapterWrapper (jestAdapter, jestVersion) { * controls whether coverage is reported. */ if (environment.testEnvironmentOptions?._ddTestCodeCoverageEnabled) { + const root = environment.repositoryRoot || environment.rootDir + const coverageFiles = getCoveredFilenamesFromCoverage(environment.global.__coverage__) - .map(filename => getTestSuitePath(filename, environment.rootDir)) + .map(filename => getTestSuitePath(filename, root)) + asyncResource.runInAsyncScope(() => { - testSuiteCodeCoverageCh.publish({ coverageFiles, testSuite: environment.testSuite }) + testSuiteCodeCoverageCh.publish({ coverageFiles, testSuite: environment.testSourceFile }) }) } testSuiteFinishCh.publish({ status, errorMessage }) @@ -776,6 +792,7 @@ addHook({ _ddRepositoryRoot, _ddIsFlakyTestRetriesEnabled, _ddFlakyTestRetriesCount, + _ddIsDiEnabled, ...restOfTestEnvironmentOptions } = testEnvironmentOptions @@ -851,12 +868,18 @@ addHook({ const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = [ 'selenium-webdriver', + 'selenium-webdriver/chrome', + 'selenium-webdriver/edge', + 'selenium-webdriver/safari', + 'selenium-webdriver/firefox', + 'selenium-webdriver/ie', + 'selenium-webdriver/chromium', 'winston' ] function shouldBypassJestRequireEngine (moduleName) { return ( - LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.some(library => moduleName.includes(library)) + LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.includes(moduleName) ) } diff --git a/packages/datadog-instrumentations/src/langchain.js b/packages/datadog-instrumentations/src/langchain.js new file mode 100644 index 00000000000..6b9321c5ab5 --- /dev/null +++ b/packages/datadog-instrumentations/src/langchain.js @@ -0,0 +1,77 @@ +'use strict' + +const { addHook } = require('./helpers/instrument') +const shimmer = require('../../datadog-shimmer') + +const tracingChannel = require('dc-polyfill').tracingChannel + +const invokeTracingChannel = tracingChannel('apm:langchain:invoke') + +function wrapLangChainPromise (fn, type, namespace = []) { + return function () { + if (!invokeTracingChannel.start.hasSubscribers) { + return fn.apply(this, arguments) + } + + // Runnable interfaces have an `lc_namespace` property + const ns = this.lc_namespace || namespace + const resource = [...ns, this.constructor.name].join('.') + + const ctx = { + args: arguments, + instance: this, + type, + resource + } + + return invokeTracingChannel.tracePromise(fn, ctx, this, ...arguments) + } +} + +// langchain compiles into ESM and CommonJS, with ESM being the default and landing in the `.js` files +// however, CommonJS ends up in `cjs` files, and are required under the hood with `.cjs` files +// we patch each separately and explicitly to match against exports only once, and not rely on file regex matching +const extensions = ['js', 'cjs'] + +for (const extension of extensions) { + addHook({ name: '@langchain/core', file: `dist/runnables/base.${extension}`, versions: ['>=0.1'] }, exports => { + const RunnableSequence = exports.RunnableSequence + shimmer.wrap(RunnableSequence.prototype, 'invoke', invoke => wrapLangChainPromise(invoke, 'chain')) + shimmer.wrap(RunnableSequence.prototype, 'batch', batch => wrapLangChainPromise(batch, 'chain')) + return exports + }) + + addHook({ + name: '@langchain/core', + file: `dist/language_models/chat_models.${extension}`, + versions: ['>=0.1'] + }, exports => { + const BaseChatModel = exports.BaseChatModel + shimmer.wrap( + BaseChatModel.prototype, + 'generate', + generate => wrapLangChainPromise(generate, 'chat_model') + ) + return exports + }) + + addHook({ name: '@langchain/core', file: `dist/language_models/llms.${extension}`, versions: ['>=0.1'] }, exports => { + const BaseLLM = exports.BaseLLM + shimmer.wrap(BaseLLM.prototype, 'generate', generate => wrapLangChainPromise(generate, 'llm')) + return exports + }) + + addHook({ name: '@langchain/openai', file: `dist/embeddings.${extension}`, versions: ['>=0.1'] }, exports => { + const OpenAIEmbeddings = exports.OpenAIEmbeddings + + // OpenAI (and Embeddings in general) do not define an lc_namespace + const namespace = ['langchain', 'embeddings', 'openai'] + shimmer.wrap(OpenAIEmbeddings.prototype, 'embedDocuments', embedDocuments => + wrapLangChainPromise(embedDocuments, 'embedding', namespace) + ) + shimmer.wrap(OpenAIEmbeddings.prototype, 'embedQuery', embedQuery => + wrapLangChainPromise(embedQuery, 'embedding', namespace) + ) + return exports + }) +} diff --git a/packages/datadog-instrumentations/src/mocha/utils.js b/packages/datadog-instrumentations/src/mocha/utils.js index 2b51fd6e73b..ce462f13256 100644 --- a/packages/datadog-instrumentations/src/mocha/utils.js +++ b/packages/datadog-instrumentations/src/mocha/utils.js @@ -284,8 +284,9 @@ function getOnTestRetryHandler () { const asyncResource = getTestAsyncResource(test) if (asyncResource) { const isFirstAttempt = test._currentRetry === 0 + const willBeRetried = test._currentRetry < test._retries asyncResource.runInAsyncScope(() => { - testRetryCh.publish({ isFirstAttempt, err }) + testRetryCh.publish({ isFirstAttempt, err, willBeRetried }) }) } const key = getTestToArKey(test) diff --git a/packages/datadog-instrumentations/src/mysql2.js b/packages/datadog-instrumentations/src/mysql2.js index 096eec0e80e..bd5c48daf56 100644 --- a/packages/datadog-instrumentations/src/mysql2.js +++ b/packages/datadog-instrumentations/src/mysql2.js @@ -8,7 +8,7 @@ const { const shimmer = require('../../datadog-shimmer') const semver = require('semver') -addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['>=1'] }, (Connection, version) => { +function wrapConnection (Connection, version) { const startCh = channel('apm:mysql2:query:start') const finishCh = channel('apm:mysql2:query:finish') const errorCh = channel('apm:mysql2:query:error') @@ -151,9 +151,8 @@ addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['>=1'] }, (Conne } }, cmd)) } -}) - -addHook({ name: 'mysql2', file: 'lib/pool.js', versions: ['>=1'] }, (Pool, version) => { +} +function wrapPool (Pool, version) { const startOuterQueryCh = channel('datadog:mysql2:outerquery:start') const shouldEmitEndAfterQueryAbort = semver.intersects(version, '>=1.3.3') @@ -221,10 +220,9 @@ addHook({ name: 'mysql2', file: 'lib/pool.js', versions: ['>=1'] }, (Pool, versi }) return Pool -}) +} -// PoolNamespace.prototype.query does not exist in mysql2<2.3.0 -addHook({ name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['>=2.3.0'] }, PoolCluster => { +function wrapPoolCluster (PoolCluster) { const startOuterQueryCh = channel('datadog:mysql2:outerquery:start') const wrappedPoolNamespaces = new WeakSet() @@ -297,4 +295,11 @@ addHook({ name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['>=2.3.0'] }, }) return PoolCluster -}) +} + +addHook({ name: 'mysql2', file: 'lib/base/connection.js', versions: ['>=3.11.5'] }, wrapConnection) +addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['1 - 3.11.4'] }, wrapConnection) +addHook({ name: 'mysql2', file: 'lib/pool.js', versions: ['1 - 3.11.4'] }, wrapPool) + +// PoolNamespace.prototype.query does not exist in mysql2<2.3.0 +addHook({ name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['2.3.0 - 3.11.4'] }, wrapPoolCluster) diff --git a/packages/datadog-instrumentations/src/next.js b/packages/datadog-instrumentations/src/next.js index 57f90f71ee4..770d340d567 100644 --- a/packages/datadog-instrumentations/src/next.js +++ b/packages/datadog-instrumentations/src/next.js @@ -2,7 +2,6 @@ const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') -const { DD_MAJOR } = require('../../../version') const startChannel = channel('apm:next:request:start') const finishChannel = channel('apm:next:request:finish') @@ -14,8 +13,14 @@ const queryParsedChannel = channel('apm:next:query-parsed') const requests = new WeakSet() const nodeNextRequestsToNextRequests = new WeakMap() +// Next.js <= 14.2.6 const MIDDLEWARE_HEADER = 'x-middleware-invoke' +// Next.js >= 14.2.7 +const NEXT_REQUEST_META = Symbol.for('NextInternalRequestMeta') +const META_IS_MIDDLEWARE = 'middlewareInvoke' +const encounteredMiddleware = new WeakSet() + function wrapHandleRequest (handleRequest) { return function (req, res, pathname, query) { return instrument(req, res, () => handleRequest.apply(this, arguments)) @@ -111,6 +116,11 @@ function getPageFromPath (page, dynamicRoutes = []) { return getPagePath(page) } +function getRequestMeta (req, key) { + const meta = req[NEXT_REQUEST_META] || {} + return typeof key === 'string' ? meta[key] : meta +} + function instrument (req, res, error, handler) { if (typeof error === 'function') { handler = error @@ -121,8 +131,9 @@ function instrument (req, res, error, handler) { res = res.originalResponse || res // TODO support middleware properly in the future? - const isMiddleware = req.headers[MIDDLEWARE_HEADER] - if (isMiddleware || requests.has(req)) { + const isMiddleware = req.headers[MIDDLEWARE_HEADER] || getRequestMeta(req, META_IS_MIDDLEWARE) + if ((isMiddleware && !encounteredMiddleware.has(req)) || requests.has(req)) { + encounteredMiddleware.add(req) if (error) { errorChannel.publish({ error }) } @@ -188,7 +199,7 @@ function finish (ctx, result, err) { // however, it is not provided as a class function or exported property addHook({ name: 'next', - versions: ['>=13.3.0 <14.2.7'], + versions: ['>=13.3.0'], file: 'dist/server/web/spec-extension/adapters/next-request.js' }, NextRequestAdapter => { shimmer.wrap(NextRequestAdapter.NextRequestAdapter, 'fromNodeNextRequest', fromNodeNextRequest => { @@ -203,17 +214,17 @@ addHook({ addHook({ name: 'next', - versions: ['>=11.1 <14.2.7'], + versions: ['>=11.1'], file: 'dist/server/serve-static.js' }, serveStatic => shimmer.wrap(serveStatic, 'serveStatic', wrapServeStatic)) addHook({ name: 'next', - versions: DD_MAJOR >= 4 ? ['>=10.2 <11.1'] : ['>=9.5 <11.1'], + versions: ['>=10.2 <11.1'], file: 'dist/next-server/server/serve-static.js' }, serveStatic => shimmer.wrap(serveStatic, 'serveStatic', wrapServeStatic)) -addHook({ name: 'next', versions: ['>=11.1 <14.2.7'], file: 'dist/server/next-server.js' }, nextServer => { +addHook({ name: 'next', versions: ['>=11.1'], file: 'dist/server/next-server.js' }, nextServer => { const Server = nextServer.default shimmer.wrap(Server.prototype, 'handleRequest', wrapHandleRequest) @@ -230,13 +241,17 @@ addHook({ name: 'next', versions: ['>=11.1 <14.2.7'], file: 'dist/server/next-se }) // `handleApiRequest` changes parameters/implementation at 13.2.0 -addHook({ name: 'next', versions: ['>=13.2 <14.2.7'], file: 'dist/server/next-server.js' }, nextServer => { +addHook({ name: 'next', versions: ['>=13.2'], file: 'dist/server/next-server.js' }, nextServer => { const Server = nextServer.default shimmer.wrap(Server.prototype, 'handleApiRequest', wrapHandleApiRequestWithMatch) return nextServer }) -addHook({ name: 'next', versions: ['>=11.1 <13.2'], file: 'dist/server/next-server.js' }, nextServer => { +addHook({ + name: 'next', + versions: ['>=11.1 <13.2'], + file: 'dist/server/next-server.js' +}, nextServer => { const Server = nextServer.default shimmer.wrap(Server.prototype, 'handleApiRequest', wrapHandleApiRequest) return nextServer @@ -244,7 +259,7 @@ addHook({ name: 'next', versions: ['>=11.1 <13.2'], file: 'dist/server/next-serv addHook({ name: 'next', - versions: DD_MAJOR >= 4 ? ['>=10.2 <11.1'] : ['>=9.5 <11.1'], + versions: ['>=10.2 <11.1'], file: 'dist/next-server/server/next-server.js' }, nextServer => { const Server = nextServer.default @@ -264,7 +279,7 @@ addHook({ addHook({ name: 'next', - versions: ['>=13 <14.2.7'], + versions: ['>=13'], file: 'dist/server/web/spec-extension/request.js' }, request => { const nextUrlDescriptor = Object.getOwnPropertyDescriptor(request.NextRequest.prototype, 'nextUrl') diff --git a/packages/datadog-instrumentations/src/passport-http.js b/packages/datadog-instrumentations/src/passport-http.js index 0969d2d3fc9..3b930a1a1cc 100644 --- a/packages/datadog-instrumentations/src/passport-http.js +++ b/packages/datadog-instrumentations/src/passport-http.js @@ -1,22 +1,10 @@ 'use strict' -const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const { wrapVerify } = require('./passport-utils') +const { strategyHook } = require('./passport-utils') addHook({ name: 'passport-http', file: 'lib/passport-http/strategies/basic.js', versions: ['>=0.3.0'] -}, BasicStrategy => { - return shimmer.wrapFunction(BasicStrategy, BasicStrategy => function () { - const type = 'http' - - if (typeof arguments[0] === 'function') { - arguments[0] = wrapVerify(arguments[0], false, type) - } else { - arguments[1] = wrapVerify(arguments[1], (arguments[0] && arguments[0].passReqToCallback), type) - } - return BasicStrategy.apply(this, arguments) - }) -}) +}, strategyHook) diff --git a/packages/datadog-instrumentations/src/passport-local.js b/packages/datadog-instrumentations/src/passport-local.js index dab74eb470e..c6dcec9a48d 100644 --- a/packages/datadog-instrumentations/src/passport-local.js +++ b/packages/datadog-instrumentations/src/passport-local.js @@ -1,22 +1,10 @@ 'use strict' -const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const { wrapVerify } = require('./passport-utils') +const { strategyHook } = require('./passport-utils') addHook({ name: 'passport-local', file: 'lib/strategy.js', versions: ['>=1.0.0'] -}, Strategy => { - return shimmer.wrapFunction(Strategy, Strategy => function () { - const type = 'local' - - if (typeof arguments[0] === 'function') { - arguments[0] = wrapVerify(arguments[0], false, type) - } else { - arguments[1] = wrapVerify(arguments[1], (arguments[0] && arguments[0].passReqToCallback), type) - } - return Strategy.apply(this, arguments) - }) -}) +}, strategyHook) diff --git a/packages/datadog-instrumentations/src/passport-utils.js b/packages/datadog-instrumentations/src/passport-utils.js index 7969ab486b4..de1cd090a71 100644 --- a/packages/datadog-instrumentations/src/passport-utils.js +++ b/packages/datadog-instrumentations/src/passport-utils.js @@ -5,33 +5,57 @@ const { channel } = require('./helpers/instrument') const passportVerifyChannel = channel('datadog:passport:verify:finish') -function wrapVerifiedAndPublish (username, password, verified, type) { - if (!passportVerifyChannel.hasSubscribers) { - return verified - } +function wrapVerifiedAndPublish (framework, username, verified) { + return shimmer.wrapFunction(verified, function wrapVerified (verified) { + return function wrappedVerified (err, user) { + // if there is an error, it's neither an auth success nor a failure + if (!err) { + const abortController = new AbortController() + + passportVerifyChannel.publish({ framework, login: username, user, success: !!user, abortController }) + + if (abortController.signal.aborted) return + } - // eslint-disable-next-line n/handle-callback-err - return shimmer.wrapFunction(verified, verified => function (err, user, info) { - const credentials = { type, username } - passportVerifyChannel.publish({ credentials, user }) - return verified.apply(this, arguments) + return verified.apply(this, arguments) + } }) } -function wrapVerify (verify, passReq, type) { - if (passReq) { - return function (req, username, password, verified) { - arguments[3] = wrapVerifiedAndPublish(username, password, verified, type) - return verify.apply(this, arguments) +function wrapVerify (verify) { + return function wrappedVerify (req, username, password, verified) { + if (passportVerifyChannel.hasSubscribers) { + const framework = `passport-${this.name}` + + // replace the callback with our own wrapper to get the result + if (this._passReqToCallback) { + arguments[3] = wrapVerifiedAndPublish(framework, arguments[1], arguments[3]) + } else { + arguments[2] = wrapVerifiedAndPublish(framework, arguments[0], arguments[2]) + } } - } else { - return function (username, password, verified) { - arguments[2] = wrapVerifiedAndPublish(username, password, verified, type) - return verify.apply(this, arguments) + + return verify.apply(this, arguments) + } +} + +function wrapStrategy (Strategy) { + return function wrappedStrategy () { + // verify function can be either the first or second argument + if (typeof arguments[0] === 'function') { + arguments[0] = wrapVerify(arguments[0]) + } else { + arguments[1] = wrapVerify(arguments[1]) } + + return Strategy.apply(this, arguments) } } +function strategyHook (Strategy) { + return shimmer.wrapFunction(Strategy, wrapStrategy) +} + module.exports = { - wrapVerify + strategyHook } diff --git a/packages/datadog-instrumentations/src/pg.js b/packages/datadog-instrumentations/src/pg.js index 55642d82e96..331557cd239 100644 --- a/packages/datadog-instrumentations/src/pg.js +++ b/packages/datadog-instrumentations/src/pg.js @@ -62,17 +62,17 @@ function wrapQuery (query) { abortController }) - const finish = asyncResource.bind(function (error) { + const finish = asyncResource.bind(function (error, res) { if (error) { errorCh.publish(error) } - finishCh.publish() + finishCh.publish({ result: res?.rows }) }) if (abortController.signal.aborted) { const error = abortController.signal.reason || new Error('Aborted') - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len // Based on: https://github.com/brianc/node-postgres/blob/54eb0fa216aaccd727765641e7d1cf5da2bc483d/packages/pg/lib/client.js#L510 const reusingQuery = typeof pgQuery.submit === 'function' const callback = arguments[arguments.length - 1] @@ -119,15 +119,15 @@ function wrapQuery (query) { if (newQuery.callback) { const originalCallback = callbackResource.bind(newQuery.callback) newQuery.callback = function (err, res) { - finish(err) + finish(err, res) return originalCallback.apply(this, arguments) } } else if (newQuery.once) { newQuery .once('error', finish) - .once('end', () => finish()) + .once('end', (res) => finish(null, res)) } else { - newQuery.then(() => finish(), finish) + newQuery.then((res) => finish(null, res), finish) } try { diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index e8332d65c8d..4eab55b1797 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -47,7 +47,7 @@ function isNewTest (test) { const testSuite = getTestSuitePath(test._requireFile, rootDir) const testsForSuite = knownTests?.playwright?.[testSuite] || [] - return !testsForSuite.includes(test.title) + return !testsForSuite.includes(getTestFullname(test)) } function getSuiteType (test, type) { @@ -224,10 +224,21 @@ function testWillRetry (test, testStatus) { return testStatus === 'fail' && test.results.length <= test.retries } +function getTestFullname (test) { + let parent = test.parent + const names = [test.title] + while (parent?._type === 'describe' || parent?._isDescribe) { + if (parent.title) { + names.unshift(parent.title) + } + parent = parent.parent + } + return names.join(' ') +} + function testBeginHandler (test, browserName) { const { _requireFile: testSuiteAbsolutePath, - title: testName, _type, location: { line: testSourceLine @@ -238,6 +249,8 @@ function testBeginHandler (test, browserName) { return } + const testName = getTestFullname(test) + const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath) if (isNewTestSuite) { @@ -412,7 +425,7 @@ function runnerHook (runnerExport, playwrightVersion) { } } catch (e) { isEarlyFlakeDetectionEnabled = false - log.error(e) + log.error('Playwright session start error', e) } if (isEarlyFlakeDetectionEnabled && semver.gte(playwrightVersion, MINIMUM_SUPPORTED_VERSION_EFD)) { @@ -425,7 +438,7 @@ function runnerHook (runnerExport, playwrightVersion) { } } catch (err) { isEarlyFlakeDetectionEnabled = false - log.error(err) + log.error('Playwright known tests error', err) } } diff --git a/packages/datadog-instrumentations/src/pug.js b/packages/datadog-instrumentations/src/pug.js new file mode 100644 index 00000000000..4322ed265cb --- /dev/null +++ b/packages/datadog-instrumentations/src/pug.js @@ -0,0 +1,23 @@ +'use strict' + +const shimmer = require('../../datadog-shimmer') +const { channel, addHook } = require('./helpers/instrument') + +const pugCompileCh = channel('datadog:pug:compile:start') + +function wrapCompile (compile) { + return function wrappedCompile (source) { + if (pugCompileCh.hasSubscribers) { + pugCompileCh.publish({ source }) + } + + return compile.apply(this, arguments) + } +} + +addHook({ name: 'pug', versions: ['>=2.0.4'] }, compiler => { + shimmer.wrap(compiler, 'compile', wrapCompile) + shimmer.wrap(compiler, 'compileClientWithDependenciesTracked', wrapCompile) + + return compiler +}) diff --git a/packages/datadog-instrumentations/src/qs.js b/packages/datadog-instrumentations/src/qs.js deleted file mode 100644 index 3901f61b169..00000000000 --- a/packages/datadog-instrumentations/src/qs.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -const { addHook, channel } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') - -const qsParseCh = channel('datadog:qs:parse:finish') - -function wrapParse (originalParse) { - return function () { - const qsParsedObj = originalParse.apply(this, arguments) - if (qsParseCh.hasSubscribers && qsParsedObj) { - qsParseCh.publish({ qs: qsParsedObj }) - } - return qsParsedObj - } -} - -addHook({ - name: 'qs', - versions: ['>=1'] -}, qs => { - shimmer.wrap(qs, 'parse', wrapParse) - return qs -}) diff --git a/packages/datadog-instrumentations/src/router.js b/packages/datadog-instrumentations/src/router.js index 00fbb6cec1a..bc9ff6152e5 100644 --- a/packages/datadog-instrumentations/src/router.js +++ b/packages/datadog-instrumentations/src/router.js @@ -169,11 +169,107 @@ function createWrapRouterMethod (name) { const wrapRouterMethod = createWrapRouterMethod('router') -addHook({ name: 'router', versions: ['>=1'] }, Router => { +addHook({ name: 'router', versions: ['>=1 <2'] }, Router => { shimmer.wrap(Router.prototype, 'use', wrapRouterMethod) shimmer.wrap(Router.prototype, 'route', wrapRouterMethod) return Router }) +const queryParserReadCh = channel('datadog:query:read:finish') + +addHook({ name: 'router', versions: ['>=2'] }, Router => { + const WrappedRouter = shimmer.wrapFunction(Router, function (originalRouter) { + return function wrappedMethod () { + const router = originalRouter.apply(this, arguments) + + shimmer.wrap(router, 'handle', function wrapHandle (originalHandle) { + return function wrappedHandle (req, res, next) { + const abortController = new AbortController() + + if (queryParserReadCh.hasSubscribers && req) { + queryParserReadCh.publish({ req, res, query: req.query, abortController }) + + if (abortController.signal.aborted) return + } + + return originalHandle.apply(this, arguments) + } + }) + + return router + } + }) + + shimmer.wrap(WrappedRouter.prototype, 'use', wrapRouterMethod) + shimmer.wrap(WrappedRouter.prototype, 'route', wrapRouterMethod) + + return WrappedRouter +}) + +const routerParamStartCh = channel('datadog:router:param:start') +const visitedParams = new WeakSet() + +function wrapHandleRequest (original) { + return function wrappedHandleRequest (req, res, next) { + if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) { + visitedParams.add(req.params) + + const abortController = new AbortController() + + routerParamStartCh.publish({ + req, + res, + params: req?.params, + abortController + }) + + if (abortController.signal.aborted) return + } + + return original.apply(this, arguments) + } +} + +addHook({ + name: 'router', file: 'lib/layer.js', versions: ['>=2'] +}, Layer => { + shimmer.wrap(Layer.prototype, 'handleRequest', wrapHandleRequest) + return Layer +}) + +function wrapParam (original) { + return function wrappedProcessParams () { + arguments[1] = shimmer.wrapFunction(arguments[1], (originalFn) => { + return function wrappedFn (req, res) { + if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) { + visitedParams.add(req.params) + + const abortController = new AbortController() + + routerParamStartCh.publish({ + req, + res, + params: req?.params, + abortController + }) + + if (abortController.signal.aborted) return + } + + return originalFn.apply(this, arguments) + } + }) + + return original.apply(this, arguments) + } +} + +addHook({ + name: 'router', versions: ['>=2'] +}, router => { + shimmer.wrap(router.prototype, 'param', wrapParam) + return router +}) + module.exports = { createWrapRouterMethod } diff --git a/packages/datadog-instrumentations/src/sequelize.js b/packages/datadog-instrumentations/src/sequelize.js index 8ba56ee8909..d8e41b17704 100644 --- a/packages/datadog-instrumentations/src/sequelize.js +++ b/packages/datadog-instrumentations/src/sequelize.js @@ -13,7 +13,7 @@ addHook({ name: 'sequelize', versions: ['>=4'] }, Sequelize => { const finishCh = channel('datadog:sequelize:query:finish') shimmer.wrap(Sequelize.prototype, 'query', query => { - return function (sql) { + return function (sql, options) { if (!startCh.hasSubscribers) { return query.apply(this, arguments) } @@ -27,9 +27,14 @@ addHook({ name: 'sequelize', versions: ['>=4'] }, Sequelize => { dialect = this.dialect.name } - function onFinish () { + function onFinish (result) { + const type = options?.type || 'RAW' + if (type === 'RAW' && result?.length > 1) { + result = result[0] + } + asyncResource.bind(function () { - finishCh.publish() + finishCh.publish({ result }) }, this).apply(this) } @@ -40,7 +45,7 @@ addHook({ name: 'sequelize', versions: ['>=4'] }, Sequelize => { }) const promise = query.apply(this, arguments) - promise.then(onFinish, onFinish) + promise.then(onFinish, () => { onFinish() }) return promise }, this).apply(this, arguments) diff --git a/packages/datadog-instrumentations/src/url.js b/packages/datadog-instrumentations/src/url.js index 18edb0079e3..67bef7e8947 100644 --- a/packages/datadog-instrumentations/src/url.js +++ b/packages/datadog-instrumentations/src/url.js @@ -59,6 +59,10 @@ addHook({ name: names }, function (url) { isURL: true }) } + + static [Symbol.hasInstance] (instance) { + return instance instanceof URL + } } }) diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index f0117e0e8c0..de7c6d2dc30 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -117,6 +117,7 @@ function getSortWrapper (sort) { let isEarlyFlakeDetectionEnabled = false let earlyFlakeDetectionNumRetries = 0 let isEarlyFlakeDetectionFaulty = false + let isDiEnabled = false let knownTests = {} try { @@ -126,10 +127,12 @@ function getSortWrapper (sort) { flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries + isDiEnabled = libraryConfig.isDiEnabled } } catch (e) { isFlakyTestRetriesEnabled = false isEarlyFlakeDetectionEnabled = false + isDiEnabled = false } if (isFlakyTestRetriesEnabled && !this.ctx.config.retry && flakyTestRetriesCount > 0) { @@ -169,6 +172,15 @@ function getSortWrapper (sort) { } } + if (isDiEnabled) { + try { + const workspaceProject = this.ctx.getCoreWorkspaceProject() + workspaceProject._provided._ddIsDiEnabled = isDiEnabled + } catch (e) { + log.warn('Could not send Dynamic Instrumentation configuration to workers.') + } + } + let testCodeCoverageLinesTotal if (this.ctx.coverageProvider?.generateCoverage) { @@ -298,13 +310,16 @@ addHook({ const testName = getTestName(task) let isNew = false let isEarlyFlakeDetectionEnabled = false + let isDiEnabled = false try { const { - _ddIsEarlyFlakeDetectionEnabled + _ddIsEarlyFlakeDetectionEnabled, + _ddIsDiEnabled } = globalThis.__vitest_worker__.providedContext isEarlyFlakeDetectionEnabled = _ddIsEarlyFlakeDetectionEnabled + isDiEnabled = _ddIsDiEnabled if (isEarlyFlakeDetectionEnabled) { isNew = newTasks.has(task) @@ -316,12 +331,22 @@ addHook({ // We finish the previous test here because we know it has failed already if (numAttempt > 0) { + const probe = {} const asyncResource = taskToAsync.get(task) const testError = task.result?.errors?.[0] if (asyncResource) { asyncResource.runInAsyncScope(() => { - testErrorCh.publish({ error: testError }) + testErrorCh.publish({ + error: testError, + willBeRetried: true, + probe, + isDiEnabled + }) }) + // We wait for the probe to be set + if (probe.setProbePromise) { + await probe.setProbePromise + } } } diff --git a/packages/datadog-instrumentations/test/express.spec.js b/packages/datadog-instrumentations/test/express.spec.js index d21b9be3e0a..534bfd041e8 100644 --- a/packages/datadog-instrumentations/test/express.spec.js +++ b/packages/datadog-instrumentations/test/express.spec.js @@ -14,7 +14,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../versions/express').get() + const express = require(`../../../versions/express@${version}`).get() const app = express() app.get('/', (req, res) => { requestBody() diff --git a/packages/datadog-instrumentations/test/passport-http.spec.js b/packages/datadog-instrumentations/test/passport-http.spec.js index 2918c935e20..5cb0282ec2f 100644 --- a/packages/datadog-instrumentations/test/passport-http.spec.js +++ b/packages/datadog-instrumentations/test/passport-http.spec.js @@ -1,8 +1,9 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') -const axios = require('axios') +const axios = require('axios').create({ validateStatus: null }) const dc = require('dc-polyfill') +const { storage } = require('../../datadog-core') withVersions('passport-http', 'passport-http', version => { describe('passport-http instrumentation', () => { @@ -10,7 +11,7 @@ withVersions('passport-http', 'passport-http', version => { let port, server, subscriberStub before(() => { - return agent.load(['express', 'passport', 'passport-http'], { client: false }) + return agent.load(['http', 'express', 'passport', 'passport-http'], { client: false }) }) before((done) => { @@ -19,7 +20,17 @@ withVersions('passport-http', 'passport-http', version => { const BasicStrategy = require(`../../../versions/passport-http@${version}`).get().BasicStrategy const app = express() - passport.use(new BasicStrategy((username, password, done) => { + function validateUser (req, username, password, done) { + // support with or without passReqToCallback + if (typeof done !== 'function') { + done = password + password = username + username = req + } + + // simulate db error + if (username === 'error') return done('error') + const users = [{ _id: 1, username: 'test', @@ -35,7 +46,18 @@ withVersions('passport-http', 'passport-http', version => { return done(null, user) } } - )) + + passport.use('basic', new BasicStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: false + }, validateUser)) + + passport.use('basic-withreq', new BasicStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true + }, validateUser)) app.use(passport.initialize()) app.use(express.json()) @@ -44,16 +66,14 @@ withVersions('passport-http', 'passport-http', version => { passport.authenticate('basic', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: false, session: false }) ) - app.post('/req', - passport.authenticate('basic', { + app.get('/req', + passport.authenticate('basic-withreq', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: true, session: false }) ) @@ -66,9 +86,7 @@ withVersions('passport-http', 'passport-http', version => { res.send('Denied') }) - passportVerifyChannel.subscribe(function ({ credentials, user, err, info }) { - subscriberStub(arguments[0]) - }) + passportVerifyChannel.subscribe((data) => subscriberStub(data)) server = app.listen(0, () => { port = server.address().port @@ -85,6 +103,18 @@ withVersions('passport-http', 'passport-http', version => { return agent.close({ ritmReset: false }) }) + it('should not call subscriber when an error occurs', async () => { + const res = await axios.get(`http://localhost:${port}/`, { + headers: { + // error:1234 + Authorization: 'Basic ZXJyb3I6MTIzNA==' + } + }) + + expect(res.status).to.equal(500) + expect(subscriberStub).to.not.be.called + }) + it('should call subscriber with proper arguments on success', async () => { const res = await axios.get(`http://localhost:${port}/`, { headers: { @@ -95,16 +125,17 @@ withVersions('passport-http', 'passport-http', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'http', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on success with passReqToCallback set to true', async () => { - const res = await axios.get(`http://localhost:${port}/`, { + const res = await axios.get(`http://localhost:${port}/req`, { headers: { // test:1234 Authorization: 'Basic dGVzdDoxMjM0' @@ -113,12 +144,13 @@ withVersions('passport-http', 'passport-http', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'http', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on failure', async () => { @@ -131,12 +163,37 @@ withVersions('passport-http', 'passport-http', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Denied') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'http', username: 'test' }, - user: false + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: false, + success: false, + abortController: new AbortController() + }) + }) + + it('should block when subscriber aborts', async () => { + subscriberStub = sinon.spy(({ abortController }) => { + storage.getStore().req.res.writeHead(403).end('Blocked') + abortController.abort() + }) + + const res = await axios.get(`http://localhost:${port}/`, { + headers: { + // test:1234 + Authorization: 'Basic dGVzdDoxMjM0' } - ) + }) + + expect(res.status).to.equal(403) + expect(res.data).to.equal('Blocked') + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) }) }) diff --git a/packages/datadog-instrumentations/test/passport-local.spec.js b/packages/datadog-instrumentations/test/passport-local.spec.js index d54f02b289f..bcfc2e56dc9 100644 --- a/packages/datadog-instrumentations/test/passport-local.spec.js +++ b/packages/datadog-instrumentations/test/passport-local.spec.js @@ -1,8 +1,9 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') -const axios = require('axios') +const axios = require('axios').create({ validateStatus: null }) const dc = require('dc-polyfill') +const { storage } = require('../../datadog-core') withVersions('passport-local', 'passport-local', version => { describe('passport-local instrumentation', () => { @@ -10,7 +11,7 @@ withVersions('passport-local', 'passport-local', version => { let port, server, subscriberStub before(() => { - return agent.load(['express', 'passport', 'passport-local'], { client: false }) + return agent.load(['http', 'express', 'passport', 'passport-local'], { client: false }) }) before((done) => { @@ -19,24 +20,44 @@ withVersions('passport-local', 'passport-local', version => { const LocalStrategy = require(`../../../versions/passport-local@${version}`).get().Strategy const app = express() - passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password' }, - (username, password, done) => { - const users = [{ - _id: 1, - username: 'test', - password: '1234', - email: 'testuser@ddog.com' - }] - - const user = users.find(user => (user.username === username) && (user.password === password)) - - if (!user) { - return done(null, false) - } else { - return done(null, user) - } + function validateUser (req, username, password, done) { + // support with or without passReqToCallback + if (typeof done !== 'function') { + done = password + password = username + username = req } - )) + + // simulate db error + if (username === 'error') return done('error') + + const users = [{ + _id: 1, + username: 'test', + password: '1234', + email: 'testuser@ddog.com' + }] + + const user = users.find(user => (user.username === username) && (user.password === password)) + + if (!user) { + return done(null, false) + } else { + return done(null, user) + } + } + + passport.use('local', new LocalStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: false + }, validateUser)) + + passport.use('local-withreq', new LocalStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true + }, validateUser)) app.use(passport.initialize()) app.use(express.json()) @@ -45,16 +66,14 @@ withVersions('passport-local', 'passport-local', version => { passport.authenticate('local', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: false, session: false }) ) app.post('/req', - passport.authenticate('local', { + passport.authenticate('local-withreq', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: true, session: false }) ) @@ -67,9 +86,7 @@ withVersions('passport-local', 'passport-local', version => { res.send('Denied') }) - passportVerifyChannel.subscribe(function ({ credentials, user, err, info }) { - subscriberStub(arguments[0]) - }) + passportVerifyChannel.subscribe((data) => subscriberStub(data)) server = app.listen(0, () => { port = server.address().port @@ -86,17 +103,25 @@ withVersions('passport-local', 'passport-local', version => { return agent.close({ ritmReset: false }) }) + it('should not call subscriber when an error occurs', async () => { + const res = await axios.post(`http://localhost:${port}/`, { username: 'error', password: '1234' }) + + expect(res.status).to.equal(500) + expect(subscriberStub).to.not.be.called + }) + it('should call subscriber with proper arguments on success', async () => { const res = await axios.post(`http://localhost:${port}/`, { username: 'test', password: '1234' }) expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'local', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on success with passReqToCallback set to true', async () => { @@ -104,12 +129,13 @@ withVersions('passport-local', 'passport-local', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'local', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on failure', async () => { @@ -117,12 +143,32 @@ withVersions('passport-local', 'passport-local', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Denied') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'local', username: 'test' }, - user: false - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: false, + success: false, + abortController: new AbortController() + }) + }) + + it('should block when subscriber aborts', async () => { + subscriberStub = sinon.spy(({ abortController }) => { + storage.getStore().req.res.writeHead(403).end('Blocked') + abortController.abort() + }) + + const res = await axios.post(`http://localhost:${port}/`, { username: 'test', password: '1234' }) + + expect(res.status).to.equal(403) + expect(res.data).to.equal('Blocked') + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) }) }) diff --git a/packages/datadog-instrumentations/test/passport-utils.spec.js b/packages/datadog-instrumentations/test/passport-utils.spec.js deleted file mode 100644 index 3cf6a64a60a..00000000000 --- a/packages/datadog-instrumentations/test/passport-utils.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' - -const proxyquire = require('proxyquire') -const { channel } = require('../src/helpers/instrument') - -const passportVerifyChannel = channel('datadog:passport:verify:finish') - -describe('passport-utils', () => { - const shimmer = { - wrap: sinon.stub() - } - - let passportUtils - - beforeEach(() => { - passportUtils = proxyquire('../src/passport-utils', { - '../../datadog-shimmer': shimmer - }) - }) - - it('should not call wrap when there is no subscribers', () => { - const wrap = passportUtils.wrapVerify(() => {}, false, 'type') - - wrap() - expect(shimmer.wrap).not.to.have.been.called - }) - - it('should call wrap when there is subscribers', () => { - const wrap = passportUtils.wrapVerify(() => {}, false, 'type') - - passportVerifyChannel.subscribe(() => {}) - - wrap() - expect(shimmer.wrap).to.have.been.called - }) -}) diff --git a/packages/datadog-instrumentations/test/url.spec.js b/packages/datadog-instrumentations/test/url.spec.js index defb8f08193..57b99e5f897 100644 --- a/packages/datadog-instrumentations/test/url.spec.js +++ b/packages/datadog-instrumentations/test/url.spec.js @@ -1,6 +1,7 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') +const { assert } = require('chai') const { channel } = require('../src/helpers/instrument') const names = ['url', 'node:url'] @@ -68,6 +69,13 @@ names.forEach(name => { }, sinon.match.any) }) + it('instanceof should work also for original instances', () => { + const OriginalUrl = Object.getPrototypeOf(url.URL) + const originalUrl = new OriginalUrl('https://www.datadoghq.com') + + assert.isTrue(originalUrl instanceof url.URL) + }) + ;['host', 'origin', 'hostname'].forEach(property => { it(`should publish on get ${property}`, () => { const urlObject = new url.URL('/path', 'https://www.datadoghq.com') diff --git a/packages/datadog-plugin-avsc/src/schema_iterator.js b/packages/datadog-plugin-avsc/src/schema_iterator.js index c748bbf9e75..0b4874ceea8 100644 --- a/packages/datadog-plugin-avsc/src/schema_iterator.js +++ b/packages/datadog-plugin-avsc/src/schema_iterator.js @@ -108,10 +108,15 @@ class SchemaExtractor { if (!builder.shouldExtractSchema(schemaName, depth)) { return false } - for (const field of schema.fields) { - if (!this.extractProperty(field, schemaName, field.name, builder, depth)) { - log.warn(`DSM: Unable to extract field with name: ${field.name} from Avro schema with name: ${schemaName}`) + if (schema.fields?.[Symbol.iterator]) { + for (const field of schema.fields) { + if (!this.extractProperty(field, schemaName, field.name, builder, depth)) { + log.warn('DSM: Unable to extract field with name: %s from Avro schema with name: %s', field.name, + schemaName) + } } + } else { + log.warn('DSM: schema.fields is not iterable from Avro schema with name: %s', schemaName) } } return true diff --git a/packages/datadog-plugin-aws-sdk/src/base.js b/packages/datadog-plugin-aws-sdk/src/base.js index e815c1e00aa..bb0d5675280 100644 --- a/packages/datadog-plugin-aws-sdk/src/base.js +++ b/packages/datadog-plugin-aws-sdk/src/base.js @@ -93,6 +93,7 @@ class BaseAwsSdkPlugin extends ClientPlugin { this.responseExtractDSMContext(operation, params, response.data ?? response, span) } this.addResponseTags(span, response) + this.addSpanPointers(span, response) this.finish(span, response, response.error) }) } @@ -101,6 +102,10 @@ class BaseAwsSdkPlugin extends ClientPlugin { // implemented by subclasses, or not } + addSpanPointers (span, response) { + // Optionally implemented by subclasses, for services where we're unable to inject trace context + } + operationFromRequest (request) { // can be overriden by subclasses return this.operationName({ diff --git a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js index b316f75e6be..a5ca5f08de1 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +++ b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js @@ -45,7 +45,7 @@ class EventBridge extends BaseAwsSdkPlugin { } request.params.Entries[0].Detail = finalData } catch (e) { - log.error(e) + log.error('EventBridge error injecting request', e) } } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js index dd139e5a608..cdbd7c077e9 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +++ b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js @@ -97,7 +97,7 @@ class Kinesis extends BaseAwsSdkPlugin { parsedAttributes: decodedData._datadog } } catch (e) { - log.error(e) + log.error('Kinesis error extracting response', e) } } @@ -113,14 +113,15 @@ class Kinesis extends BaseAwsSdkPlugin { response.Records.forEach(record => { const parsedAttributes = JSON.parse(Buffer.from(record.Data).toString()) - if ( - parsedAttributes?._datadog && streamName - ) { - const payloadSize = getSizeOrZero(record.Data) + const payloadSize = getSizeOrZero(record.Data) + if (parsedAttributes?._datadog) { this.tracer.decodeDataStreamsContext(parsedAttributes._datadog) - this.tracer - .setCheckpoint(['direction:in', `topic:${streamName}`, 'type:kinesis'], span, payloadSize) } + const tags = streamName + ? ['direction:in', `topic:${streamName}`, 'type:kinesis'] + : ['direction:in', 'type:kinesis'] + this.tracer + .setCheckpoint(tags, span, payloadSize) }) } diff --git a/packages/datadog-plugin-aws-sdk/src/services/lambda.js b/packages/datadog-plugin-aws-sdk/src/services/lambda.js index f6ea874872e..b5fe1981c20 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/lambda.js +++ b/packages/datadog-plugin-aws-sdk/src/services/lambda.js @@ -43,7 +43,7 @@ class Lambda extends BaseAwsSdkPlugin { const newContextBase64 = Buffer.from(JSON.stringify(clientContext)).toString('base64') request.params.ClientContext = newContextBase64 } catch (err) { - log.error(err) + log.error('Lambda error injecting request', err) } } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/s3.js b/packages/datadog-plugin-aws-sdk/src/services/s3.js index 0b6da57f3c9..5fcfb6ed165 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/s3.js +++ b/packages/datadog-plugin-aws-sdk/src/services/s3.js @@ -1,6 +1,9 @@ 'use strict' const BaseAwsSdkPlugin = require('../base') +const log = require('../../../dd-trace/src/log') +const { generatePointerHash } = require('../../../dd-trace/src/util') +const { S3_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../../dd-trace/src/constants') class S3 extends BaseAwsSdkPlugin { static get id () { return 's3' } @@ -18,6 +21,37 @@ class S3 extends BaseAwsSdkPlugin { bucketname: params.Bucket }) } + + addSpanPointers (span, response) { + const request = response?.request + const operationName = request?.operation + if (!['putObject', 'copyObject', 'completeMultipartUpload'].includes(operationName)) { + // We don't create span links for other S3 operations. + return + } + + // AWS v2: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html + // AWS v3: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/ + const bucketName = request?.params?.Bucket + const objectKey = request?.params?.Key + let eTag = + response?.ETag || // v3 PutObject & CompleteMultipartUpload + response?.CopyObjectResult?.ETag || // v3 CopyObject + response?.data?.ETag || // v2 PutObject & CompleteMultipartUpload + response?.data?.CopyObjectResult?.ETag // v2 CopyObject + + if (!bucketName || !objectKey || !eTag) { + log.debug('Unable to calculate span pointer hash because of missing parameters.') + return + } + + // https://github.com/DataDog/dd-span-pointer-rules/blob/main/AWS/S3/Object/README.md + if (eTag.startsWith('"') && eTag.endsWith('"')) { + eTag = eTag.slice(1, -1) + } + const pointerHash = generatePointerHash([bucketName, objectKey, eTag]) + span.addSpanPointer(S3_PTR_KIND, SPAN_POINTER_DIRECTION.DOWNSTREAM, pointerHash) + } } module.exports = S3 diff --git a/packages/datadog-plugin-aws-sdk/src/services/sqs.js b/packages/datadog-plugin-aws-sdk/src/services/sqs.js index 38a5d03c775..9857e46bf28 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/sqs.js +++ b/packages/datadog-plugin-aws-sdk/src/services/sqs.js @@ -42,7 +42,7 @@ class Sqs extends BaseAwsSdkPlugin { // extract DSM context after as we might not have a parent-child but may have a DSM context this.responseExtractDSMContext( - request.operation, request.params, response, span || null, { parsedMessageAttributes } + request.operation, request.params, response, span || null, { parsedAttributes: parsedMessageAttributes } ) }) @@ -163,7 +163,7 @@ class Sqs extends BaseAwsSdkPlugin { return JSON.parse(buffer) } } catch (e) { - log.error(e) + log.error('Sqs error parsing DD attributes', e) } } @@ -195,16 +195,16 @@ class Sqs extends BaseAwsSdkPlugin { parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog) } } + const payloadSize = getHeadersSize({ + Body: message.Body, + MessageAttributes: message.MessageAttributes + }) + const queue = params.QueueUrl.split('/').pop() if (parsedAttributes) { - const payloadSize = getHeadersSize({ - Body: message.Body, - MessageAttributes: message.MessageAttributes - }) - const queue = params.QueueUrl.split('/').pop() this.tracer.decodeDataStreamsContext(parsedAttributes) - this.tracer - .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize) } + this.tracer + .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize) }) } diff --git a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js index 3f65acdab0b..342af3ea723 100644 --- a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const EventBridge = require('../src/services/eventbridge') diff --git a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js index cedeb14f000..2e3bf356f3e 100644 --- a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const sinon = require('sinon') @@ -303,6 +303,32 @@ describe('Kinesis', function () { }) }) + 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 }) + expect(statsPointsReceived).to.equal(1) + expect(agent.dsmStatsExistWithParentHash(agent, '0')).to.equal(true) + }, { timeoutMs: 10000 }).then(done, done) + + agent.reload('aws-sdk', { kinesis: { dsmEnabled: false } }, { dsmEnabled: false }) + helpers.putTestRecord(kinesis, streamNameDSM, helpers.dataBuffer, (err, data) => { + if (err) return done(err) + + agent.reload('aws-sdk', { kinesis: { dsmEnabled: true } }, { dsmEnabled: true }) + helpers.getTestData(kinesis, streamNameDSM, data, (err) => { + if (err) return done(err) + }) + }) + }) + 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 diff --git a/packages/datadog-plugin-aws-sdk/test/s3.spec.js b/packages/datadog-plugin-aws-sdk/test/s3.spec.js index 9ffb9a67215..6e896efa281 100644 --- a/packages/datadog-plugin-aws-sdk/test/s3.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/s3.spec.js @@ -4,6 +4,7 @@ const agent = require('../../dd-trace/test/plugins/agent') const { setup } = require('./spec_helpers') const axios = require('axios') const { rawExpectedSchema } = require('./s3-naming') +const { S3_PTR_KIND, SPAN_POINTER_DIRECTION } = require('../../dd-trace/src/constants') const bucketName = 's3-bucket-name-test' @@ -36,20 +37,19 @@ describe('Plugin', () => { before(done => { AWS = require(`../../../versions/${s3ClientName}@${version}`).get() + s3 = new AWS.S3({ endpoint: 'http://127.0.0.1:4566', s3ForcePathStyle: true, region: 'us-east-1' }) + + // Fix for LocationConstraint issue - only for SDK v2 + if (s3ClientName === 'aws-sdk') { + s3.api.globalEndpoint = '127.0.0.1' + } - s3 = new AWS.S3({ endpoint: 'http://127.0.0.1:4567', s3ForcePathStyle: true, region: 'us-east-1' }) s3.createBucket({ Bucket: bucketName }, (err) => { if (err) return done(err) done() }) }) - after(done => { - s3.deleteBucket({ Bucket: bucketName }, () => { - done() - }) - }) - after(async () => { await resetLocalStackS3() return agent.close({ ritmReset: false }) @@ -74,6 +74,138 @@ describe('Plugin', () => { rawExpectedSchema.outbound ) + describe('span pointers', () => { + it('should add span pointer for putObject operation', (done) => { + agent.use(traces => { + try { + const span = traces[0][0] + const links = JSON.parse(span.meta?.['_dd.span_links'] || '[]') + + expect(links).to.have.lengthOf(1) + expect(links[0].attributes).to.deep.equal({ + 'ptr.kind': S3_PTR_KIND, + 'ptr.dir': SPAN_POINTER_DIRECTION.DOWNSTREAM, + 'ptr.hash': '6d1a2fe194c6579187408f827f942be3', + 'link.kind': 'span-pointer' + }) + done() + } catch (error) { + done(error) + } + }).catch(done) + + s3.putObject({ + Bucket: bucketName, + Key: 'test-key', + Body: 'test body' + }, (err) => { + if (err) { + done(err) + } + }) + }) + + it('should add span pointer for copyObject operation', (done) => { + agent.use(traces => { + try { + const span = traces[0][0] + const links = JSON.parse(span.meta?.['_dd.span_links'] || '[]') + + expect(links).to.have.lengthOf(1) + expect(links[0].attributes).to.deep.equal({ + 'ptr.kind': S3_PTR_KIND, + 'ptr.dir': SPAN_POINTER_DIRECTION.DOWNSTREAM, + 'ptr.hash': '1542053ce6d393c424b1374bac1fc0c5', + 'link.kind': 'span-pointer' + }) + done() + } catch (error) { + done(error) + } + }).catch(done) + + s3.copyObject({ + Bucket: bucketName, + Key: 'new-key', + CopySource: `${bucketName}/test-key` + }, (err) => { + if (err) { + done(err) + } + }) + }) + + it('should add span pointer for completeMultipartUpload operation', (done) => { + // Create 5MiB+ buffers for parts + const partSize = 5 * 1024 * 1024 + const part1Data = Buffer.alloc(partSize, 'a') + const part2Data = Buffer.alloc(partSize, 'b') + + // Start the multipart upload process + s3.createMultipartUpload({ + Bucket: bucketName, + Key: 'multipart-test' + }, (err, multipartData) => { + if (err) return done(err) + + // Upload both parts in parallel + Promise.all([ + new Promise((resolve, reject) => { + s3.uploadPart({ + Bucket: bucketName, + Key: 'multipart-test', + PartNumber: 1, + UploadId: multipartData.UploadId, + Body: part1Data + }, (err, data) => err ? reject(err) : resolve({ PartNumber: 1, ETag: data.ETag })) + }), + new Promise((resolve, reject) => { + s3.uploadPart({ + Bucket: bucketName, + Key: 'multipart-test', + PartNumber: 2, + UploadId: multipartData.UploadId, + Body: part2Data + }, (err, data) => err ? reject(err) : resolve({ PartNumber: 2, ETag: data.ETag })) + }) + ]).then(parts => { + // Now complete the multipart upload + const completeParams = { + Bucket: bucketName, + Key: 'multipart-test', + UploadId: multipartData.UploadId, + MultipartUpload: { + Parts: parts + } + } + + s3.completeMultipartUpload(completeParams, (err) => { + if (err) done(err) + agent.use(traces => { + const span = traces[0][0] + const operation = span.meta?.['aws.operation'] + if (operation === 'completeMultipartUpload') { + try { + const links = JSON.parse(span.meta?.['_dd.span_links'] || '[]') + expect(links).to.have.lengthOf(1) + expect(links[0].attributes).to.deep.equal({ + 'ptr.kind': S3_PTR_KIND, + 'ptr.dir': SPAN_POINTER_DIRECTION.DOWNSTREAM, + 'ptr.hash': '422412aa6b472a7194f3e24f4b12b4a6', + 'link.kind': 'span-pointer' + }) + done() + } catch (error) { + done(error) + } + } + }) + }) + }).catch(done) + }) + }) + }) + it('should allow disabling a specific span kind of a service', (done) => { let total = 0 diff --git a/packages/datadog-plugin-aws-sdk/test/sns.spec.js b/packages/datadog-plugin-aws-sdk/test/sns.spec.js index 7b62156f06c..b205c652669 100644 --- a/packages/datadog-plugin-aws-sdk/test/sns.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sns.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const sinon = require('sinon') diff --git a/packages/datadog-plugin-aws-sdk/test/sqs.spec.js b/packages/datadog-plugin-aws-sdk/test/sqs.spec.js index 9c0c3686f9b..35e3ce39d8c 100644 --- a/packages/datadog-plugin-aws-sdk/test/sqs.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sqs.spec.js @@ -8,6 +8,7 @@ const { rawExpectedSchema } = require('./sqs-naming') const queueName = 'SQS_QUEUE_NAME' const queueNameDSM = 'SQS_QUEUE_NAME_DSM' +const queueNameDSMConsumerOnly = 'SQS_QUEUE_NAME_DSM_CONSUMER_ONLY' const getQueueParams = (queueName) => { return { @@ -20,6 +21,7 @@ const getQueueParams = (queueName) => { const queueOptions = getQueueParams(queueName) const queueOptionsDsm = getQueueParams(queueNameDSM) +const queueOptionsDsmConsumerOnly = getQueueParams(queueNameDSMConsumerOnly) describe('Plugin', () => { describe('aws-sdk (sqs)', function () { @@ -30,6 +32,7 @@ describe('Plugin', () => { let sqs const QueueUrl = 'http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME' const QueueUrlDsm = 'http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME_DSM' + const QueueUrlDsmConsumerOnly = 'http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME_DSM_CONSUMER_ONLY' let tracer const sqsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sqs' : 'aws-sdk' @@ -412,10 +415,25 @@ describe('Plugin', () => { }) }) + before(done => { + AWS = require(`../../../versions/${sqsClientName}@${version}`).get() + + sqs = new AWS.SQS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' }) + sqs.createQueue(queueOptionsDsmConsumerOnly, (err, res) => { + if (err) return done(err) + + done() + }) + }) + after(done => { sqs.deleteQueue({ QueueUrl: QueueUrlDsm }, done) }) + after(done => { + sqs.deleteQueue({ QueueUrl: QueueUrlDsmConsumerOnly }, done) + }) + after(() => { return agent.close({ ritmReset: false }) }) @@ -546,6 +564,28 @@ describe('Plugin', () => { }) }) + 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 + }) + } + }) + expect(statsPointsReceived).to.equal(1) + expect(agent.dsmStatsExistWithParentHash(agent, '0')).to.equal(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 diff --git a/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js b/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js index ed77ecd51b2..44677b4efed 100644 --- a/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const semver = require('semver') diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index 2c85403906c..c2f9783c039 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -20,7 +20,7 @@ class AzureFunctionsPlugin extends TracingPlugin { static get kind () { return 'server' } static get type () { return 'serverless' } - static get prefix () { return 'tracing:datadog:azure-functions:invoke' } + static get prefix () { return 'tracing:datadog:azure:functions:invoke' } bindStart (ctx) { const { functionName, methodName } = ctx diff --git a/packages/datadog-plugin-azure-functions/test/integration-test/client.spec.js b/packages/datadog-plugin-azure-functions/test/integration-test/client.spec.js index 8d5a0d43fdb..51dd4aba5fd 100644 --- a/packages/datadog-plugin-azure-functions/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-azure-functions/test/integration-test/client.spec.js @@ -47,7 +47,7 @@ describe('esm', () => { assert.strictEqual(payload.length, 1) assert.isArray(payload[0]) assert.strictEqual(payload[0].length, 1) - assert.propertyVal(payload[0][0], 'name', 'azure-functions.invoke') + assert.propertyVal(payload[0][0], 'name', 'azure.functions.invoke') }) }).timeout(50000) }) diff --git a/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs b/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs index 91ff60029fb..c65ebffe78d 100644 --- a/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs +++ b/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs @@ -9,4 +9,4 @@ const client = new Client({ await client.connect() await client.execute('SELECT now() FROM local;') -await client.shutdown() \ No newline at end of file +await client.shutdown() diff --git a/packages/datadog-plugin-child_process/src/scrub-cmd-params.js b/packages/datadog-plugin-child_process/src/scrub-cmd-params.js index b5fb59bb781..595d8f5746a 100644 --- a/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +++ b/packages/datadog-plugin-child_process/src/scrub-cmd-params.js @@ -6,7 +6,7 @@ const ALLOWED_ENV_VARIABLES = ['LD_PRELOAD', 'LD_LIBRARY_PATH', 'PATH'] const PROCESS_DENYLIST = ['md5'] const VARNAMES_REGEX = /\$([\w\d_]*)(?:[^\w\d_]|$)/gmi -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const PARAM_PATTERN = '^-{0,2}(?:p(?:ass(?:w(?:or)?d)?)?|address|api[-_]?key|e?mail|secret(?:[-_]?key)?|a(?:ccess|uth)[-_]?token|mysql_pwd|credentials|(?:stripe)?token)$' const regexParam = new RegExp(PARAM_PATTERN, 'i') const ENV_PATTERN = '^(\\w+=\\w+;)*\\w+=\\w+;?$' diff --git a/packages/datadog-plugin-child_process/test/index.spec.js b/packages/datadog-plugin-child_process/test/index.spec.js index 33624eab4d8..bd29b9abdfe 100644 --- a/packages/datadog-plugin-child_process/test/index.spec.js +++ b/packages/datadog-plugin-child_process/test/index.spec.js @@ -62,7 +62,8 @@ describe('Child process plugin', () => { 'span.type': 'system', 'cmd.exec': JSON.stringify(['ls', '-l']) }, - integrationName: 'system' + integrationName: 'system', + links: undefined } ) }) @@ -84,7 +85,8 @@ describe('Child process plugin', () => { 'span.type': 'system', 'cmd.shell': 'ls -l' }, - integrationName: 'system' + integrationName: 'system', + links: undefined } ) }) @@ -109,7 +111,8 @@ describe('Child process plugin', () => { 'cmd.exec': JSON.stringify(['echo', arg, '']), 'cmd.truncated': 'true' }, - integrationName: 'system' + integrationName: 'system', + links: undefined } ) }) @@ -134,7 +137,8 @@ describe('Child process plugin', () => { 'cmd.shell': 'ls -l /h ', 'cmd.truncated': 'true' }, - integrationName: 'system' + integrationName: 'system', + links: undefined } ) }) @@ -160,7 +164,8 @@ describe('Child process plugin', () => { 'cmd.exec': JSON.stringify(['ls', '-l', '', '']), 'cmd.truncated': 'true' }, - integrationName: 'system' + integrationName: 'system', + links: undefined } ) }) @@ -186,7 +191,8 @@ describe('Child process plugin', () => { 'cmd.shell': 'ls -l /home -t', 'cmd.truncated': 'true' }, - integrationName: 'system' + integrationName: 'system', + links: undefined } ) }) diff --git a/packages/datadog-plugin-cucumber/src/index.js b/packages/datadog-plugin-cucumber/src/index.js index d24f97c33e6..1c4403b7ce6 100644 --- a/packages/datadog-plugin-cucumber/src/index.js +++ b/packages/datadog-plugin-cucumber/src/index.js @@ -26,7 +26,12 @@ const { TEST_MODULE, TEST_MODULE_ID, TEST_SUITE, - CUCUMBER_IS_PARALLEL + CUCUMBER_IS_PARALLEL, + TEST_NAME, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } = require('../../dd-trace/src/plugins/util/test') const { RESOURCE_NAME } = require('../../../ext/tags') const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants') @@ -46,6 +51,7 @@ const { const id = require('../../dd-trace/src/id') const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID +const debuggerParameterPerTest = new Map() function getTestSuiteTags (testSuiteSpan) { const suiteTags = { @@ -220,14 +226,40 @@ class CucumberPlugin extends CiPlugin { const testSpan = this.startTestSpan(testName, testSuite, extraTags) this.enter(testSpan, store) + + const debuggerParameters = debuggerParameterPerTest.get(testName) + + if (debuggerParameters) { + const spanContext = testSpan.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + testSpan.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + testSpan.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + testSpan.setTag(DI_DEBUG_ERROR_FILE, file) + testSpan.setTag(DI_DEBUG_ERROR_LINE, line) + } }) - this.addSub('ci:cucumber:test:retry', (isFlakyRetry) => { + this.addSub('ci:cucumber:test:retry', ({ isRetry, error }) => { const store = storage.getStore() const span = store.span - if (isFlakyRetry) { + if (isRetry) { span.setTag(TEST_IS_RETRY, 'true') } + span.setTag('error', error) + if (this.di && error && this.libraryConfig?.isDiEnabled) { + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(error) + debuggerParameterPerTest.set(testName, debuggerParameters) + } span.setTag(TEST_STATUS, 'fail') span.finish() finishAllTraceSpans(span) @@ -281,6 +313,7 @@ class CucumberPlugin extends CiPlugin { isStep, status, skipReason, + error, errorMessage, isNew, isEfdRetry, @@ -302,7 +335,9 @@ class CucumberPlugin extends CiPlugin { span.setTag(TEST_SKIP_REASON, skipReason) } - if (errorMessage) { + if (error) { + span.setTag('error', error) + } else if (errorMessage) { // we can't get a full error in cucumber steps span.setTag(ERROR_MESSAGE, errorMessage) } diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index 756bb89b82d..2ed62070fda 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -223,7 +223,7 @@ class CypressPlugin { this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration) .then((libraryConfigurationResponse) => { if (libraryConfigurationResponse.err) { - log.error(libraryConfigurationResponse.err) + log.error('Cypress plugin library config response error', libraryConfigurationResponse.err) } else { const { libraryConfig: { @@ -360,7 +360,7 @@ class CypressPlugin { this.testConfiguration ) if (knownTestsResponse.err) { - log.error(knownTestsResponse.err) + log.error('Cypress known tests response error', knownTestsResponse.err) this.isEarlyFlakeDetectionEnabled = false } else { // We use TEST_FRAMEWORK_NAME for the name of the module @@ -374,7 +374,7 @@ class CypressPlugin { this.testConfiguration ) if (skippableTestsResponse.err) { - log.error(skippableTestsResponse.err) + log.error('Cypress skippable tests response error', skippableTestsResponse.err) } else { const { skippableTests, correlationId } = skippableTestsResponse this.testsToSkip = skippableTests || [] @@ -658,10 +658,22 @@ class CypressPlugin { log.warn('There is no active test span in dd:afterEach handler') return null } - const { state, error, isRUMActive, testSourceLine, testSuite, testName, isNew, isEfdRetry } = test + const { + state, + error, + isRUMActive, + testSourceLine, + testSuite, + testSuiteAbsolutePath, + testName, + isNew, + isEfdRetry + } = test if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) { const coverageFiles = getCoveredFilenamesFromCoverage(coverage) - const relativeCoverageFiles = coverageFiles.map(file => getTestSuitePath(file, this.rootDir)) + const relativeCoverageFiles = [...coverageFiles, testSuiteAbsolutePath].map( + file => getTestSuitePath(file, this.repositoryRoot || this.rootDir) + ) if (!relativeCoverageFiles.length) { incrementCountMetric(TELEMETRY_CODE_COVERAGE_EMPTY) } diff --git a/packages/datadog-plugin-cypress/src/support.js b/packages/datadog-plugin-cypress/src/support.js index b9a739c94e4..8900f2695fb 100644 --- a/packages/datadog-plugin-cypress/src/support.js +++ b/packages/datadog-plugin-cypress/src/support.js @@ -88,6 +88,7 @@ afterEach(function () { const testInfo = { testName: currentTest.fullTitle(), testSuite: Cypress.mocha.getRootSuite().file, + testSuiteAbsolutePath: Cypress.spec && Cypress.spec.absolute, state: currentTest.state, error: currentTest.err, isNew: currentTest._ddIsNew, 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 eacd384c033..9f64b0c7c27 100644 --- a/packages/datadog-plugin-elasticsearch/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-elasticsearch/test/integration-test/client.spec.js @@ -13,7 +13,8 @@ describe('esm', () => { let proc let sandbox - withVersions('elasticsearch', ['@elastic/elasticsearch'], version => { + // excluding 8.16.0 for esm tests, because it is not working: https://github.com/elastic/elasticsearch-js/issues/2466 + withVersions('elasticsearch', ['@elastic/elasticsearch'], '<8.16.0 || >8.16.0', version => { before(async function () { this.timeout(20000) sandbox = await createSandbox([`'@elastic/elasticsearch@${version}'`], false, [ diff --git a/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs b/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs index a54efd22e4d..f3f2cc1d9a7 100644 --- a/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs +++ b/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs @@ -3,4 +3,4 @@ import { Client } from '@elastic/elasticsearch' const client = new Client({ node: 'http://localhost:9200' }) -await client.ping() \ No newline at end of file +await client.ping() diff --git a/packages/datadog-plugin-express/test/index.spec.js b/packages/datadog-plugin-express/test/index.spec.js index 55a608f4adf..8899c34ecb3 100644 --- a/packages/datadog-plugin-express/test/index.spec.js +++ b/packages/datadog-plugin-express/test/index.spec.js @@ -2,6 +2,7 @@ const { AsyncLocalStorage } = require('async_hooks') const axios = require('axios') +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 plugin = require('../src') @@ -214,34 +215,56 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const isExpress4 = semver.intersects(version, '<5.0.0') + let index = 0 + + const rootSpan = spans[index++] + expect(rootSpan).to.have.property('resource', 'GET /app/user/:id') + expect(rootSpan).to.have.property('name', 'express.request') + expect(rootSpan.meta).to.have.property('component', 'express') + + if (isExpress4) { + expect(spans[index]).to.have.property('resource', 'query') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + expect(spans[index]).to.have.property('resource', 'expressInit') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + } - expect(spans[0]).to.have.property('resource', 'GET /app/user/:id') - expect(spans[0]).to.have.property('name', 'express.request') - expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[1]).to.have.property('resource', 'query') - expect(spans[1]).to.have.property('name', 'express.middleware') - expect(spans[1].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[1].meta).to.have.property('component', 'express') - expect(spans[2]).to.have.property('resource', 'expressInit') - expect(spans[2]).to.have.property('name', 'express.middleware') - expect(spans[2].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[2].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('resource', 'named') - expect(spans[3]).to.have.property('name', 'express.middleware') - expect(spans[3].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[3].meta).to.have.property('component', 'express') - expect(spans[4]).to.have.property('resource', 'router') - expect(spans[4]).to.have.property('name', 'express.middleware') - expect(spans[4].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[4].meta).to.have.property('component', 'express') - expect(spans[5].resource).to.match(/^bound\s.*$/) - expect(spans[5]).to.have.property('name', 'express.middleware') - expect(spans[5].parent_id.toString()).to.equal(spans[4].span_id.toString()) - expect(spans[5].meta).to.have.property('component', 'express') - expect(spans[6]).to.have.property('resource', '') - expect(spans[6]).to.have.property('name', 'express.middleware') - expect(spans[6].parent_id.toString()).to.equal(spans[5].span_id.toString()) - expect(spans[6].meta).to.have.property('component', 'express') + expect(spans[index]).to.have.property('resource', 'named') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + expect(spans[index]).to.have.property('resource', 'router') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + if (isExpress4) { + expect(spans[index].resource).to.match(/^bound\s.*$/) + } else { + expect(spans[index]).to.have.property('resource', 'handle') + } + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(spans[index - 1].span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + expect(spans[index]).to.have.property('resource', '') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(spans[index - 1].span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + + expect(index).to.equal(spans.length - 1) }) .then(done) .catch(done) @@ -277,12 +300,14 @@ describe('Plugin', () => { .use(traces => { const spans = sort(traces[0]) + const breakingSpanIndex = semver.intersects(version, '<5.0.0') ? 3 : 1 + expect(spans[0]).to.have.property('resource', 'GET /user/:id') expect(spans[0]).to.have.property('name', 'express.request') expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('resource', 'breaking') - expect(spans[3]).to.have.property('name', 'express.middleware') - expect(spans[3].meta).to.have.property('component', 'express') + expect(spans[breakingSpanIndex]).to.have.property('resource', 'breaking') + expect(spans[breakingSpanIndex]).to.have.property('name', 'express.middleware') + expect(spans[breakingSpanIndex].meta).to.have.property('component', 'express') }) .then(done) .catch(done) @@ -321,12 +346,13 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const errorSpanIndex = semver.intersects(version, '<5.0.0') ? 4 : 2 expect(spans[0]).to.have.property('name', 'express.request') - expect(spans[4]).to.have.property('name', 'express.middleware') - expect(spans[4].meta).to.have.property(ERROR_TYPE, error.name) + expect(spans[errorSpanIndex]).to.have.property('name', 'express.middleware') + expect(spans[errorSpanIndex].meta).to.have.property(ERROR_TYPE, error.name) expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[4].meta).to.have.property('component', 'express') + expect(spans[errorSpanIndex].meta).to.have.property('component', 'express') }) .then(done) .catch(done) @@ -398,14 +424,14 @@ describe('Plugin', () => { const router = express.Router() router.use('/', (req, res, next) => next()) - router.use('*', (req, res, next) => next()) + router.use('/*splat', (req, res, next) => next()) router.use('/bar', (req, res, next) => next()) router.use('/bar', (req, res, next) => { res.status(200).send() }) app.use('/', (req, res, next) => next()) - app.use('*', (req, res, next) => next()) + app.use('/*splat', (req, res, next) => next()) app.use('/foo/bar', (req, res, next) => next()) app.use('/foo', router) @@ -1138,17 +1164,18 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const secondErrorIndex = spans.length - 2 expect(spans[0]).to.have.property('error', 1) expect(spans[0].meta).to.have.property(ERROR_TYPE, error.name) expect(spans[0].meta).to.have.property(ERROR_MESSAGE, error.message) expect(spans[0].meta).to.have.property(ERROR_STACK, error.stack) expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('error', 1) - expect(spans[3].meta).to.have.property(ERROR_TYPE, error.name) - expect(spans[3].meta).to.have.property(ERROR_MESSAGE, error.message) - expect(spans[3].meta).to.have.property(ERROR_STACK, error.stack) - expect(spans[3].meta).to.have.property('component', 'express') + expect(spans[secondErrorIndex]).to.have.property('error', 1) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_TYPE, error.name) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_MESSAGE, error.message) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_STACK, error.stack) + expect(spans[secondErrorIndex].meta).to.have.property('component', 'express') }) .then(done) .catch(done) @@ -1175,16 +1202,17 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const secondErrorIndex = spans.length - 2 expect(spans[0]).to.have.property('error', 1) expect(spans[0].meta).to.have.property(ERROR_TYPE, error.name) expect(spans[0].meta).to.have.property(ERROR_MESSAGE, error.message) expect(spans[0].meta).to.have.property(ERROR_STACK, error.stack) expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('error', 1) - expect(spans[3].meta).to.have.property(ERROR_TYPE, error.name) - expect(spans[3].meta).to.have.property(ERROR_MESSAGE, error.message) - expect(spans[3].meta).to.have.property(ERROR_STACK, error.stack) + expect(spans[secondErrorIndex]).to.have.property('error', 1) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_TYPE, error.name) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_MESSAGE, error.message) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_STACK, error.stack) expect(spans[0].meta).to.have.property('component', 'express') }) .then(done) @@ -1199,6 +1227,11 @@ describe('Plugin', () => { }) it('should support capturing groups in routes', done => { + if (semver.intersects(version, '>=5.0.0')) { + this.skip && this.skip() // mocha allows dynamic skipping, tap does not + return done() + } + const app = express() app.get('/:path(*)', (req, res) => { @@ -1224,6 +1257,32 @@ describe('Plugin', () => { }) }) + it('should support wildcard path prefix matching in routes', done => { + const app = express() + + app.get('/*user', (req, res) => { + res.status(200).send() + }) + + appListener = app.listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .use(traces => { + const spans = sort(traces[0]) + + expect(spans[0]).to.have.property('resource', 'GET /*user') + expect(spans[0].meta).to.have.property('http.url', `http://localhost:${port}/user`) + }) + .then(done) + .catch(done) + + axios + .get(`http://localhost:${port}/user`) + .catch(done) + }) + }) + it('should keep the properties untouched on nested router handlers', () => { const router = express.Router() const childRouter = express.Router() @@ -1234,7 +1293,12 @@ describe('Plugin', () => { router.use('/users', childRouter) - const layer = router.stack.find(layer => layer.regexp.test('/users')) + const layer = router.stack.find(layer => { + if (semver.intersects(version, '>=5.0.0')) { + return layer.matchers.find(matcher => matcher('/users')) + } + return layer.regexp.test('/users') + }) expect(layer.handle).to.have.ownProperty('stack') }) 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 a5c08d60ecb..c13a4249892 100644 --- a/packages/datadog-plugin-express/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-express/test/integration-test/client.spec.js @@ -7,6 +7,7 @@ const { spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') const { assert } = require('chai') +const semver = require('semver') describe('esm', () => { let agent @@ -36,13 +37,14 @@ describe('esm', () => { it('is instrumented', async () => { proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'server.mjs', agent.port) + const numberOfSpans = semver.intersects(version, '<5.0.0') ? 4 : 3 return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) assert.isArray(payload) assert.strictEqual(payload.length, 1) assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 4) + assert.strictEqual(payload[0].length, numberOfSpans) assert.propertyVal(payload[0][0], 'name', 'express.request') assert.propertyVal(payload[0][1], 'name', 'express.middleware') }) diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs b/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs index f315996ba58..fc3ab176f24 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs +++ b/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs @@ -8,4 +8,4 @@ const [subscription] = await topic.createSubscription('foo') await topic.publishMessage({ data: Buffer.from('Test message!') }) await subscription.close() -await pubsub.close() \ No newline at end of file +await pubsub.close() diff --git a/packages/datadog-plugin-graphql/test/integration-test/server.mjs b/packages/datadog-plugin-graphql/test/integration-test/server.mjs index 822155d1710..d7aab2d1b3b 100644 --- a/packages/datadog-plugin-graphql/test/integration-test/server.mjs +++ b/packages/datadog-plugin-graphql/test/integration-test/server.mjs @@ -15,8 +15,8 @@ const schema = new graphql.GraphQLSchema({ }) }) -await graphql.graphql({ - schema, - source: `query MyQuery { hello(name: "world") }`, +await graphql.graphql({ + schema, + source: 'query MyQuery { hello(name: "world") }', variableValues: { who: 'world' } }) diff --git a/packages/datadog-plugin-grpc/src/client.js b/packages/datadog-plugin-grpc/src/client.js index 1b130a1f93e..db8dd89b9bf 100644 --- a/packages/datadog-plugin-grpc/src/client.js +++ b/packages/datadog-plugin-grpc/src/client.js @@ -62,7 +62,7 @@ class GrpcClientPlugin extends ClientPlugin { return parentStore } - error ({ span, error }) { + error ({ span = this.activeSpan, error }) { this.addCode(span, error.code) if (error.code && !this._tracerConfig.grpc.client.error.statuses.includes(error.code)) { return @@ -108,7 +108,7 @@ class GrpcClientPlugin extends ClientPlugin { } addCode (span, code) { - if (code !== undefined) { + if (code !== undefined && span) { span.setTag('grpc.status.code', code) } } diff --git a/packages/datadog-plugin-grpc/src/util.js b/packages/datadog-plugin-grpc/src/util.js index 1c1937e7ea7..ec7d0f33570 100644 --- a/packages/datadog-plugin-grpc/src/util.js +++ b/packages/datadog-plugin-grpc/src/util.js @@ -54,7 +54,7 @@ module.exports = { } if (config.hasOwnProperty(filter)) { - log.error(`Expected '${filter}' to be an array or function.`) + log.error('Expected \'%s\' to be an array or function.', filter) } return () => ({}) diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index 4362094b0be..0287f837653 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -22,7 +22,12 @@ const { TEST_EARLY_FLAKE_ABORT_REASON, JEST_DISPLAY_NAME, TEST_IS_RUM_ACTIVE, - TEST_BROWSER_DRIVER + TEST_BROWSER_DRIVER, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE, + TEST_NAME } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const id = require('../../dd-trace/src/id') @@ -39,6 +44,7 @@ const { } = require('../../dd-trace/src/ci-visibility/telemetry') const isJestWorker = !!process.env.JEST_WORKER_ID +const debuggerParameterPerTest = new Map() // https://github.com/facebook/jest/blob/d6ad15b0f88a05816c2fe034dd6900d28315d570/packages/jest-worker/src/types.ts#L38 const CHILD_MESSAGE_END = 2 @@ -155,6 +161,7 @@ class JestPlugin extends CiPlugin { config._ddRepositoryRoot = this.repositoryRoot config._ddIsFlakyTestRetriesEnabled = this.libraryConfig?.isFlakyTestRetriesEnabled ?? false config._ddFlakyTestRetriesCount = this.libraryConfig?.flakyTestRetriesCount + config._ddIsDiEnabled = this.libraryConfig?.isDiEnabled ?? false }) }) @@ -301,6 +308,29 @@ class JestPlugin extends CiPlugin { const span = this.startTestSpan(test) this.enter(span, store) + + const { name: testName } = test + + const debuggerParameters = debuggerParameterPerTest.get(testName) + + // If we have a debugger probe, we need to add the snapshot id to the span + if (debuggerParameters) { + const spanContext = span.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + span.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + span.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + span.setTag(DI_DEBUG_ERROR_FILE, file) + span.setTag(DI_DEBUG_ERROR_LINE, line) + } }) this.addSub('ci:jest:test:finish', ({ status, testStartLine }) => { @@ -326,13 +356,19 @@ class JestPlugin extends CiPlugin { finishAllTraceSpans(span) }) - this.addSub('ci:jest:test:err', (error) => { + this.addSub('ci:jest:test:err', ({ error, willBeRetried, probe, isDiEnabled }) => { if (error) { const store = storage.getStore() if (store && store.span) { const span = store.span span.setTag(TEST_STATUS, 'fail') span.setTag('error', error) + if (willBeRetried && this.di && isDiEnabled) { + // if we use numTestExecutions, we have to remove the breakpoint after each execution + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(error, probe) + debuggerParameterPerTest.set(testName, debuggerParameters) + } } } }) @@ -348,7 +384,6 @@ class JestPlugin extends CiPlugin { const { suite, name, - runner, displayName, testParameters, frameworkVersion, @@ -360,7 +395,7 @@ class JestPlugin extends CiPlugin { } = test const extraTags = { - [JEST_TEST_RUNNER]: runner, + [JEST_TEST_RUNNER]: 'jest-circus', [TEST_PARAMETERS]: testParameters, [TEST_FRAMEWORK_VERSION]: frameworkVersion } diff --git a/packages/datadog-plugin-langchain/src/handlers/chain.js b/packages/datadog-plugin-langchain/src/handlers/chain.js new file mode 100644 index 00000000000..81374587cc6 --- /dev/null +++ b/packages/datadog-plugin-langchain/src/handlers/chain.js @@ -0,0 +1,50 @@ +'use strict' + +const LangChainHandler = require('./default') + +class LangChainChainHandler extends LangChainHandler { + getSpanStartTags (ctx) { + const tags = {} + + if (!this.isPromptCompletionSampled()) return tags + + let inputs = ctx.args?.[0] + inputs = Array.isArray(inputs) ? inputs : [inputs] + + for (const idx in inputs) { + const input = inputs[idx] + if (typeof input !== 'object') { + tags[`langchain.request.inputs.${idx}`] = this.normalize(input) + } else { + for (const [key, value] of Object.entries(input)) { + // these are mappings to the python client names, ie lc_kwargs + // only present on BaseMessage types + if (key.includes('lc_')) continue + tags[`langchain.request.inputs.${idx}.${key}`] = this.normalize(value) + } + } + } + + return tags + } + + getSpanEndTags (ctx) { + const tags = {} + + if (!this.isPromptCompletionSampled()) return tags + + let outputs = ctx.result + outputs = Array.isArray(outputs) ? outputs : [outputs] + + for (const idx in outputs) { + const output = outputs[idx] + tags[`langchain.response.outputs.${idx}`] = this.normalize( + typeof output === 'string' ? output : JSON.stringify(output) + ) + } + + return tags + } +} + +module.exports = LangChainChainHandler diff --git a/packages/datadog-plugin-langchain/src/handlers/default.js b/packages/datadog-plugin-langchain/src/handlers/default.js new file mode 100644 index 00000000000..103f7c1f98d --- /dev/null +++ b/packages/datadog-plugin-langchain/src/handlers/default.js @@ -0,0 +1,53 @@ +'use strict' + +const Sampler = require('../../../dd-trace/src/sampler') + +const RE_NEWLINE = /\n/g +const RE_TAB = /\t/g + +// TODO: should probably refactor the OpenAI integration to use a shared LLMTracingPlugin base class +// This logic isn't particular to LangChain +class LangChainHandler { + constructor (config) { + this.config = config + this.sampler = new Sampler(config.spanPromptCompletionSampleRate) + } + + // no-op for default handler + getSpanStartTags (ctx) {} + + // no-op for default handler + getSpanEndTags (ctx) {} + + // no-op for default handler + extractApiKey (instance) {} + + // no-op for default handler + extractProvider (instance) {} + + // no-op for default handler + extractModel (instance) {} + + normalize (text) { + if (!text) return + if (typeof text !== 'string' || !text || (typeof text === 'string' && text.length === 0)) return + + const max = this.config.spanCharLimit + + text = text + .replace(RE_NEWLINE, '\\n') + .replace(RE_TAB, '\\t') + + if (text.length > max) { + return text.substring(0, max) + '...' + } + + return text + } + + isPromptCompletionSampled () { + return this.sampler.isSampled() + } +} + +module.exports = LangChainHandler diff --git a/packages/datadog-plugin-langchain/src/handlers/embedding.js b/packages/datadog-plugin-langchain/src/handlers/embedding.js new file mode 100644 index 00000000000..aa37825b2d8 --- /dev/null +++ b/packages/datadog-plugin-langchain/src/handlers/embedding.js @@ -0,0 +1,63 @@ +'use strict' + +const LangChainHandler = require('./default') + +class LangChainEmbeddingHandler extends LangChainHandler { + getSpanStartTags (ctx) { + const tags = {} + + const inputTexts = ctx.args?.[0] + + const sampled = this.isPromptCompletionSampled() + if (typeof inputTexts === 'string') { + // embed query + if (sampled) { + tags['langchain.request.inputs.0.text'] = this.normalize(inputTexts) + } + tags['langchain.request.input_counts'] = 1 + } else { + // embed documents + if (sampled) { + for (const idx in inputTexts) { + const inputText = inputTexts[idx] + tags[`langchain.request.inputs.${idx}.text`] = this.normalize(inputText) + } + } + tags['langchain.request.input_counts'] = inputTexts.length + } + + return tags + } + + getSpanEndTags (ctx) { + const tags = {} + + const { result } = ctx + if (!Array.isArray(result)) return + + tags['langchain.response.outputs.embedding_length'] = ( + Array.isArray(result[0]) ? result[0] : result + ).length + + return tags + } + + extractApiKey (instance) { + const apiKey = instance.clientConfig?.apiKey + if (!apiKey || apiKey.length < 4) return '' + return `...${apiKey.slice(-4)}` + } + + extractProvider (instance) { + return instance.constructor.name.split('Embeddings')[0].toLowerCase() + } + + extractModel (instance) { + for (const attr of ['model', 'modelName', 'modelId', 'modelKey', 'repoId']) { + const modelName = instance[attr] + if (modelName) return modelName + } + } +} + +module.exports = LangChainEmbeddingHandler diff --git a/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js b/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js new file mode 100644 index 00000000000..56fabeecfc0 --- /dev/null +++ b/packages/datadog-plugin-langchain/src/handlers/language_models/chat_model.js @@ -0,0 +1,99 @@ +'use strict' + +const LangChainLanguageModelHandler = require('.') + +const COMPLETIONS = 'langchain.response.completions' + +class LangChainChatModelHandler extends LangChainLanguageModelHandler { + getSpanStartTags (ctx, provider) { + const tags = {} + + const inputs = ctx.args?.[0] + + for (const messageSetIndex in inputs) { + const messageSet = inputs[messageSetIndex] + + for (const messageIndex in messageSet) { + const message = messageSet[messageIndex] + if (this.isPromptCompletionSampled()) { + tags[`langchain.request.messages.${messageSetIndex}.${messageIndex}.content`] = + this.normalize(message.content) || '' + } + tags[`langchain.request.messages.${messageSetIndex}.${messageIndex}.message_type`] = message.constructor.name + } + } + + const instance = ctx.instance + const identifyingParams = (typeof instance._identifyingParams === 'function' && instance._identifyingParams()) || {} + for (const [param, val] of Object.entries(identifyingParams)) { + if (param.toLowerCase().includes('apikey') || param.toLowerCase().includes('apitoken')) continue + if (typeof val === 'object') { + for (const [key, value] of Object.entries(val)) { + tags[`langchain.request.${provider}.parameters.${param}.${key}`] = value + } + } else { + tags[`langchain.request.${provider}.parameters.${param}`] = val + } + } + + return tags + } + + getSpanEndTags (ctx) { + const { result } = ctx + + const tags = {} + + this.extractTokenMetrics(ctx.currentStore?.span, result) + + for (const messageSetIdx in result?.generations) { + const messageSet = result.generations[messageSetIdx] + + for (const chatCompletionIdx in messageSet) { + const chatCompletion = messageSet[chatCompletionIdx] + + const text = chatCompletion.text + const message = chatCompletion.message + let toolCalls = message.tool_calls + + if (text && this.isPromptCompletionSampled()) { + tags[ + `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.content` + ] = this.normalize(text) + } + + tags[ + `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.message_type` + ] = message.constructor.name + + if (toolCalls) { + if (!Array.isArray(toolCalls)) { + toolCalls = [toolCalls] + } + + for (const toolCallIndex in toolCalls) { + const toolCall = toolCalls[toolCallIndex] + + tags[ + `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.tool_calls.${toolCallIndex}.id` + ] = toolCall.id + tags[ + `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.tool_calls.${toolCallIndex}.name` + ] = toolCall.name + + const args = toolCall.args || {} + for (const [name, value] of Object.entries(args)) { + tags[ + `${COMPLETIONS}.${messageSetIdx}.${chatCompletionIdx}.tool_calls.${toolCallIndex}.args.${name}` + ] = this.normalize(value) + } + } + } + } + } + + return tags + } +} + +module.exports = LangChainChatModelHandler diff --git a/packages/datadog-plugin-langchain/src/handlers/language_models/index.js b/packages/datadog-plugin-langchain/src/handlers/language_models/index.js new file mode 100644 index 00000000000..b67dfa2e2dd --- /dev/null +++ b/packages/datadog-plugin-langchain/src/handlers/language_models/index.js @@ -0,0 +1,48 @@ +'use strict' + +const { getTokensFromLlmOutput } = require('../../tokens') +const LangChainHandler = require('../default') + +class LangChainLanguageModelHandler extends LangChainHandler { + extractApiKey (instance) { + const key = Object.keys(instance) + .find(key => { + const lower = key.toLowerCase() + return lower.includes('apikey') || lower.includes('apitoken') + }) + + let apiKey = instance[key] + if (apiKey?.secretValue && typeof apiKey.secretValue === 'function') { + apiKey = apiKey.secretValue() + } + if (!apiKey || apiKey.length < 4) return '' + return `...${apiKey.slice(-4)}` + } + + extractProvider (instance) { + return typeof instance._llmType === 'function' && instance._llmType().split('-')[0] + } + + extractModel (instance) { + for (const attr of ['model', 'modelName', 'modelId', 'modelKey', 'repoId']) { + const modelName = instance[attr] + if (modelName) return modelName + } + } + + extractTokenMetrics (span, result) { + if (!span || !result) return + + // we do not tag token metrics for non-openai providers + const provider = span.context()._tags['langchain.request.provider'] + if (provider !== 'openai') return + + const tokens = getTokensFromLlmOutput(result) + + for (const [tokenKey, tokenCount] of Object.entries(tokens)) { + span.setTag(`langchain.tokens.${tokenKey}_tokens`, tokenCount) + } + } +} + +module.exports = LangChainLanguageModelHandler diff --git a/packages/datadog-plugin-langchain/src/handlers/language_models/llm.js b/packages/datadog-plugin-langchain/src/handlers/language_models/llm.js new file mode 100644 index 00000000000..d7c489bbc0f --- /dev/null +++ b/packages/datadog-plugin-langchain/src/handlers/language_models/llm.js @@ -0,0 +1,57 @@ +'use strict' + +const LangChainLanguageModelHandler = require('.') + +class LangChainLLMHandler extends LangChainLanguageModelHandler { + getSpanStartTags (ctx, provider) { + const tags = {} + + const prompts = ctx.args?.[0] + for (const promptIdx in prompts) { + if (!this.isPromptCompletionSampled()) continue + + const prompt = prompts[promptIdx] + tags[`langchain.request.prompts.${promptIdx}.content`] = this.normalize(prompt) || '' + } + + const instance = ctx.instance + const identifyingParams = (typeof instance._identifyingParams === 'function' && instance._identifyingParams()) || {} + for (const [param, val] of Object.entries(identifyingParams)) { + if (param.toLowerCase().includes('apikey') || param.toLowerCase().includes('apitoken')) continue + if (typeof val === 'object') { + for (const [key, value] of Object.entries(val)) { + tags[`langchain.request.${provider}.parameters.${param}.${key}`] = value + } + } else { + tags[`langchain.request.${provider}.parameters.${param}`] = val + } + } + + return tags + } + + getSpanEndTags (ctx) { + const { result } = ctx + + const tags = {} + + this.extractTokenMetrics(ctx.currentStore?.span, result) + + for (const completionIdx in result?.generations) { + const completion = result.generations[completionIdx] + if (this.isPromptCompletionSampled()) { + tags[`langchain.response.completions.${completionIdx}.text`] = this.normalize(completion[0].text) || '' + } + + if (completion && completion[0].generationInfo) { + const generationInfo = completion[0].generationInfo + tags[`langchain.response.completions.${completionIdx}.finish_reason`] = generationInfo.finishReason + tags[`langchain.response.completions.${completionIdx}.logprobs`] = generationInfo.logprobs + } + } + + return tags + } +} + +module.exports = LangChainLLMHandler diff --git a/packages/datadog-plugin-langchain/src/index.js b/packages/datadog-plugin-langchain/src/index.js new file mode 100644 index 00000000000..19b6e7d9793 --- /dev/null +++ b/packages/datadog-plugin-langchain/src/index.js @@ -0,0 +1,89 @@ +'use strict' + +const { MEASURED } = require('../../../ext/tags') +const { storage } = require('../../datadog-core') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') + +const API_KEY = 'langchain.request.api_key' +const MODEL = 'langchain.request.model' +const PROVIDER = 'langchain.request.provider' +const TYPE = 'langchain.request.type' + +const LangChainHandler = require('./handlers/default') +const LangChainChatModelHandler = require('./handlers/language_models/chat_model') +const LangChainLLMHandler = require('./handlers/language_models/llm') +const LangChainChainHandler = require('./handlers/chain') +const LangChainEmbeddingHandler = require('./handlers/embedding') + +class LangChainPlugin extends TracingPlugin { + static get id () { return 'langchain' } + static get operation () { return 'invoke' } + static get system () { return 'langchain' } + static get prefix () { + return 'tracing:apm:langchain:invoke' + } + + constructor () { + super(...arguments) + + const langchainConfig = this._tracerConfig.langchain || {} + this.handlers = { + chain: new LangChainChainHandler(langchainConfig), + chat_model: new LangChainChatModelHandler(langchainConfig), + llm: new LangChainLLMHandler(langchainConfig), + embedding: new LangChainEmbeddingHandler(langchainConfig), + default: new LangChainHandler(langchainConfig) + } + } + + bindStart (ctx) { + const { resource, type } = ctx + const handler = this.handlers[type] + + const instance = ctx.instance + const apiKey = handler.extractApiKey(instance) + const provider = handler.extractProvider(instance) + const model = handler.extractModel(instance) + + const tags = handler.getSpanStartTags(ctx, provider) || [] + + if (apiKey) tags[API_KEY] = apiKey + if (provider) tags[PROVIDER] = provider + if (model) tags[MODEL] = model + if (type) tags[TYPE] = type + + const span = this.startSpan('langchain.request', { + service: this.config.service, + resource, + kind: 'client', + meta: { + [MEASURED]: 1, + ...tags + } + }, false) + + const store = storage.getStore() || {} + ctx.currentStore = { ...store, span } + + return ctx.currentStore + } + + asyncEnd (ctx) { + const span = ctx.currentStore.span + + const { type } = ctx + + const handler = this.handlers[type] + const tags = handler.getSpanEndTags(ctx) || {} + + span.addTags(tags) + + span.finish() + } + + getHandler (type) { + return this.handlers[type] || this.handlers.default + } +} + +module.exports = LangChainPlugin diff --git a/packages/datadog-plugin-langchain/src/tokens.js b/packages/datadog-plugin-langchain/src/tokens.js new file mode 100644 index 00000000000..e29bb80735c --- /dev/null +++ b/packages/datadog-plugin-langchain/src/tokens.js @@ -0,0 +1,35 @@ +'use strict' + +function getTokensFromLlmOutput (result) { + const tokens = { + input: 0, + output: 0, + total: 0 + } + const { llmOutput } = result + if (!llmOutput) return tokens + + const tokenUsage = llmOutput.tokenUsage || llmOutput.usage_metadata || llmOutput.usage_metadata + if (!tokenUsage) return tokens + + for (const tokenNames of [['input', 'prompt'], ['output', 'completion'], ['total']]) { + let token = 0 + for (const tokenName of tokenNames) { + const underScore = `${tokenName}_tokens` + const camelCase = `${tokenName}Tokens` + + token = tokenUsage[underScore] || tokenUsage[camelCase] || token + } + + tokens[tokenNames[0]] = token + } + + // assign total_tokens again in case it was improperly set the first time, or was not on tokenUsage + tokens.total = tokens.total || tokens.input + tokens.output + + return tokens +} + +module.exports = { + getTokensFromLlmOutput +} diff --git a/packages/datadog-plugin-langchain/test/index.spec.js b/packages/datadog-plugin-langchain/test/index.spec.js new file mode 100644 index 00000000000..24500b09c3b --- /dev/null +++ b/packages/datadog-plugin-langchain/test/index.spec.js @@ -0,0 +1,986 @@ +'use strict' + +const { useEnv } = require('../../../integration-tests/helpers') +const agent = require('../../dd-trace/test/plugins/agent') + +const nock = require('nock') + +function stubCall ({ base = '', path = '', code = 200, response = {} }) { + const responses = Array.isArray(response) ? response : [response] + const times = responses.length + nock(base).post(path).times(times).reply(() => { + return [code, responses.shift()] + }) +} +const openAiBaseCompletionInfo = { base: 'https://api.openai.com', path: '/v1/completions' } +const openAiBaseChatInfo = { base: 'https://api.openai.com', path: '/v1/chat/completions' } +const openAiBaseEmbeddingInfo = { base: 'https://api.openai.com', path: '/v1/embeddings' } + +describe('Plugin', () => { + let langchainOpenai + let langchainAnthropic + + let langchainMessages + let langchainOutputParsers + let langchainPrompts + let langchainRunnables + + // so we can verify it gets tagged properly + useEnv({ + OPENAI_API_KEY: '', + ANTHROPIC_API_KEY: '' + }) + + describe('langchain', () => { + withVersions('langchain', ['@langchain/core'], version => { + beforeEach(() => { + return agent.load('langchain') + }) + + afterEach(() => { + // wiping in order to read new env vars for the config each time + return agent.close({ ritmReset: false, wipe: true }) + }) + + beforeEach(() => { + langchainOpenai = require(`../../../versions/@langchain/openai@${version}`).get() + langchainAnthropic = require(`../../../versions/@langchain/anthropic@${version}`).get() + + // need to specify specific import in `get(...)` + langchainMessages = require(`../../../versions/@langchain/core@${version}`).get('@langchain/core/messages') + langchainOutputParsers = require(`../../../versions/@langchain/core@${version}`) + .get('@langchain/core/output_parsers') + langchainPrompts = require(`../../../versions/@langchain/core@${version}`).get('@langchain/core/prompts') + langchainRunnables = require(`../../../versions/@langchain/core@${version}`).get('@langchain/core/runnables') + }) + + afterEach(() => { + nock.cleanAll() + }) + + describe('with global configurations', () => { + describe('with sampling rate', () => { + useEnv({ + DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE: 0 + }) + + it('does not tag prompt or completion', async () => { + stubCall({ + ...openAiBaseCompletionInfo, + response: { + model: 'gpt-3.5-turbo-instruct', + choices: [{ + text: 'The answer is 4', + index: 0, + logprobs: null, + finish_reason: 'length' + }], + usage: { prompt_tokens: 8, completion_tokens: 12, otal_tokens: 20 } + } + }) + + const llm = new langchainOpenai.OpenAI({ model: 'gpt-3.5-turbo-instruct' }) + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.not.have.property('langchain.request.prompts.0.content') + expect(span.meta).to.not.have.property('langchain.response.completions.0.text') + }) + + const result = await llm.generate(['what is 2 + 2?']) + + expect(result.generations[0][0].text).to.equal('The answer is 4') + + await checkTraces + }) + }) + + describe('with span char limit', () => { + useEnv({ + DD_LANGCHAIN_SPAN_CHAR_LIMIT: 5 + }) + + it('truncates the prompt and completion', async () => { + stubCall({ + ...openAiBaseCompletionInfo, + response: { + model: 'gpt-3.5-turbo-instruct', + choices: [{ + text: 'The answer is 4', + index: 0, + logprobs: null, + finish_reason: 'length' + }], + usage: { prompt_tokens: 8, completion_tokens: 12, otal_tokens: 20 } + } + }) + + const llm = new langchainOpenai.OpenAI({ model: 'gpt-3.5-turbo-instruct' }) + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.have.property('langchain.request.prompts.0.content', 'what ...') + expect(span.meta).to.have.property('langchain.response.completions.0.text', 'The a...') + }) + + const result = await llm.generate(['what is 2 + 2?']) + + expect(result.generations[0][0].text).to.equal('The answer is 4') + + await checkTraces + }) + }) + }) + + describe('llm', () => { + it('does not tag output on error', async () => { + nock('https://api.openai.com').post('/v1/completions').reply(403) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + + const span = traces[0][0] + + const langchainResponseRegex = /^langchain\.response\.completions\./ + const hasMatching = Object.keys(span.meta).some(key => langchainResponseRegex.test(key)) + + expect(hasMatching).to.be.false + + expect(span.meta).to.have.property('error.message') + expect(span.meta).to.have.property('error.type') + expect(span.meta).to.have.property('error.stack') + }) + + try { + const llm = new langchainOpenai.OpenAI({ model: 'gpt-3.5-turbo-instruct', maxRetries: 0 }) + await llm.generate(['what is 2 + 2?']) + } catch {} + + await checkTraces + }) + + it('instruments a langchain llm call for a single prompt', async () => { + stubCall({ + ...openAiBaseCompletionInfo, + response: { + model: 'gpt-3.5-turbo-instruct', + choices: [{ + text: 'The answer is 4', + index: 0, + logprobs: null, + finish_reason: 'length' + }], + usage: { prompt_tokens: 8, completion_tokens: 12, otal_tokens: 20 } + } + }) + + const llm = new langchainOpenai.OpenAI({ model: 'gpt-3.5-turbo-instruct' }) + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span).to.have.property('name', 'langchain.request') + expect(span).to.have.property('resource', 'langchain.llms.openai.OpenAI') + + expect(span.meta).to.have.property('langchain.request.api_key', '...key>') + expect(span.meta).to.have.property('langchain.request.provider', 'openai') + expect(span.meta).to.have.property('langchain.request.model', 'gpt-3.5-turbo-instruct') + expect(span.meta).to.have.property('langchain.request.type', 'llm') + expect(span.meta).to.have.property('langchain.request.prompts.0.content', 'what is 2 + 2?') + + expect(span.meta).to.have.property('langchain.response.completions.0.text', 'The answer is 4') + expect(span.meta).to.have.property('langchain.response.completions.0.finish_reason', 'length') + + expect(span.metrics).to.have.property('langchain.tokens.input_tokens', 8) + expect(span.metrics).to.have.property('langchain.tokens.output_tokens', 12) + expect(span.metrics).to.have.property('langchain.tokens.total_tokens', 20) + }) + + const result = await llm.generate(['what is 2 + 2?']) + + expect(result.generations[0][0].text).to.equal('The answer is 4') + + await checkTraces + }) + + it('instruments a langchain openai llm call for multiple prompts', async () => { + stubCall({ + ...openAiBaseCompletionInfo, + response: { + model: 'gpt-3.5-turbo-instruct', + choices: [{ + text: 'The answer is 4', + index: 0, + logprobs: null, + finish_reason: 'length' + }, { + text: 'The circumference of the earth is 24,901 miles', + index: 1, + logprobs: null, + finish_reason: 'length' + }], + usage: { prompt_tokens: 8, completion_tokens: 12, otal_tokens: 20 } + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.have.property('langchain.request.prompts.0.content', 'what is 2 + 2?') + expect(span.meta).to.have.property( + 'langchain.request.prompts.1.content', 'what is the circumference of the earth?') + + expect(span.meta).to.have.property('langchain.response.completions.0.text', 'The answer is 4') + expect(span.meta).to.have.property( + 'langchain.response.completions.1.text', 'The circumference of the earth is 24,901 miles') + }) + + const llm = new langchainOpenai.OpenAI({ model: 'gpt-3.5-turbo-instruct' }) + const result = await llm.generate(['what is 2 + 2?', 'what is the circumference of the earth?']) + + expect(result.generations[0][0].text).to.equal('The answer is 4') + expect(result.generations[1][0].text).to.equal('The circumference of the earth is 24,901 miles') + + await checkTraces + }) + + it('instruments a langchain openai llm call for a single prompt and multiple responses', async () => { + // it should only use the first choice + stubCall({ + ...openAiBaseCompletionInfo, + response: { + model: 'gpt-3.5-turbo-instruct', + choices: [{ + text: 'The answer is 4', + index: 0, + logprobs: null, + finish_reason: 'length' + }, { + text: '2 + 2 = 4', + index: 1, + logprobs: null, + finish_reason: 'length' + }], + usage: { prompt_tokens: 8, completion_tokens: 12, otal_tokens: 20 } + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.metrics).to.have.property('langchain.request.openai.parameters.n', 2) + + expect(span.meta).to.have.property('langchain.request.prompts.0.content', 'what is 2 + 2?') + expect(span.meta).to.have.property('langchain.response.completions.0.text', 'The answer is 4') + + expect(span.meta).to.not.have.property('langchain.response.completions.1.text') + }) + + const llm = new langchainOpenai.OpenAI({ model: 'gpt-3.5-turbo-instruct', n: 2 }) + const result = await llm.generate(['what is 2 + 2?']) + + expect(result.generations[0][0].text).to.equal('The answer is 4') + expect(result.generations[0][1].text).to.equal('2 + 2 = 4') + + await checkTraces + }) + }) + + describe('chat model', () => { + it('does not tag output on error', async () => { + nock('https://api.openai.com').post('/v1/chat/completions').reply(403) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + + const span = traces[0][0] + + const langchainResponseRegex = /^langchain\.response\.completions\./ + const hasMatching = Object.keys(span.meta).some(key => langchainResponseRegex.test(key)) + expect(hasMatching).to.be.false + + expect(span.meta).to.have.property('error.message') + expect(span.meta).to.have.property('error.type') + expect(span.meta).to.have.property('error.stack') + }) + + try { + const chatModel = new langchainOpenai.ChatOpenAI({ model: 'gpt-4', maxRetries: 0 }) + await chatModel.invoke('Hello!') + } catch {} + + await checkTraces + }) + + it('instruments a langchain openai chat model call for a single string prompt', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Hello! How can I assist you today?' + }, + finish_reason: 'length', + index: 0 + }] + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span).to.have.property('name', 'langchain.request') + expect(span).to.have.property('resource', 'langchain.chat_models.openai.ChatOpenAI') + + expect(span.meta).to.have.property('langchain.request.api_key', '...key>') + expect(span.meta).to.have.property('langchain.request.provider', 'openai') + expect(span.meta).to.have.property('langchain.request.model', 'gpt-4') + expect(span.meta).to.have.property('langchain.request.type', 'chat_model') + + expect(span.meta).to.have.property('langchain.request.messages.0.0.content', 'Hello!') + expect(span.meta).to.have.property('langchain.request.messages.0.0.message_type', 'HumanMessage') + + expect(span.meta).to.have.property( + 'langchain.response.completions.0.0.content', 'Hello! How can I assist you today?' + ) + expect(span.meta).to.have.property('langchain.response.completions.0.0.message_type', 'AIMessage') + + expect(span.metrics).to.have.property('langchain.tokens.input_tokens', 37) + expect(span.metrics).to.have.property('langchain.tokens.output_tokens', 10) + expect(span.metrics).to.have.property('langchain.tokens.total_tokens', 47) + }) + + const chatModel = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + const result = await chatModel.invoke('Hello!') + + expect(result.content).to.equal('Hello! How can I assist you today?') + + await checkTraces + }) + + it('instruments a langchain openai chat model call for a JSON message input', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Hi!' + }, + finish_reason: 'length', + index: 0 + }] + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.have.property( + 'langchain.request.messages.0.0.content', 'You only respond with one word answers' + ) + expect(span.meta).to.have.property('langchain.request.messages.0.0.message_type', 'SystemMessage') + expect(span.meta).to.have.property('langchain.request.messages.0.1.content', 'Hello!') + expect(span.meta).to.have.property('langchain.request.messages.0.1.message_type', 'HumanMessage') + + expect(span.meta).to.have.property('langchain.response.completions.0.0.content', 'Hi!') + expect(span.meta).to.have.property('langchain.response.completions.0.0.message_type', 'AIMessage') + }) + + const chatModel = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + const messages = [ + { role: 'system', content: 'You only respond with one word answers' }, + { role: 'human', content: 'Hello!' } + ] + + const result = await chatModel.invoke(messages) + expect(result.content).to.equal('Hi!') + + await checkTraces + }) + + it('instruments a langchain openai chat model call for a BaseMessage-like input', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Hi!' + }, + finish_reason: 'length', + index: 0 + }] + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.have.property( + 'langchain.request.messages.0.0.content', 'You only respond with one word answers' + ) + expect(span.meta).to.have.property('langchain.request.messages.0.0.message_type', 'SystemMessage') + expect(span.meta).to.have.property('langchain.request.messages.0.1.content', 'Hello!') + expect(span.meta).to.have.property('langchain.request.messages.0.1.message_type', 'HumanMessage') + + expect(span.meta).to.have.property( + 'langchain.response.completions.0.0.content', 'Hi!' + ) + expect(span.meta).to.have.property('langchain.response.completions.0.0.message_type', 'AIMessage') + }) + + const chatModel = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + const messages = [ + new langchainMessages.SystemMessage('You only respond with one word answers'), + new langchainMessages.HumanMessage('Hello!') + ] + const result = await chatModel.invoke(messages) + + expect(result.content).to.equal('Hi!') + + await checkTraces + }) + + it('instruments a langchain openai chat model call with tool calls', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: { + model: 'gpt-4', + choices: [{ + message: { + role: 'assistant', + content: null, + tool_calls: [ + { + id: 'tool-1', + type: 'function', + function: { + name: 'extract_fictional_info', + arguments: '{"name":"SpongeBob","origin":"Bikini Bottom"}' + } + } + ] + }, + finish_reason: 'tool_calls', + index: 0 + }] + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.have.property( + 'langchain.request.messages.0.0.content', 'My name is SpongeBob and I live in Bikini Bottom.' + ) + expect(span.meta).to.have.property('langchain.request.messages.0.0.message_type', 'HumanMessage') + expect(span.meta).to.not.have.property('langchain.response.completions.0.0.content') + expect(span.meta).to.have.property('langchain.response.completions.0.0.message_type', 'AIMessage') + expect(span.meta).to.have.property('langchain.response.completions.0.0.tool_calls.0.id', 'tool-1') + expect(span.meta).to.have.property( + 'langchain.response.completions.0.0.tool_calls.0.name', 'extract_fictional_info' + ) + expect(span.meta).to.have.property( + 'langchain.response.completions.0.0.tool_calls.0.args.name', 'SpongeBob' + ) + expect(span.meta).to.have.property( + 'langchain.response.completions.0.0.tool_calls.0.args.origin', 'Bikini Bottom' + ) + }) + + const tools = [ + { + name: 'extract_fictional_info', + description: 'Get the fictional information from the body of the input text', + parameters: { + type: 'object', + properties: { + name: { type: 'string', description: 'Name of the character' }, + origin: { type: 'string', description: 'Where they live' } + } + } + } + ] + + const model = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + const modelWithTools = model.bindTools(tools) + + const result = await modelWithTools.invoke('My name is SpongeBob and I live in Bikini Bottom.') + expect(result.tool_calls).to.have.length(1) + expect(result.tool_calls[0].name).to.equal('extract_fictional_info') + + await checkTraces + }) + + it('instruments a langchain anthropic chat model call', async () => { + stubCall({ + base: 'https://api.anthropic.com', + path: '/v1/messages', + response: { + id: 'msg_01NE2EJQcjscRyLbyercys6p', + type: 'message', + role: 'assistant', + model: 'claude-3-opus-20240229', + content: [ + { type: 'text', text: 'Hello!' } + ], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { input_tokens: 11, output_tokens: 6 } + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span).to.have.property('name', 'langchain.request') + expect(span).to.have.property('resource', 'langchain.chat_models.anthropic.ChatAnthropic') + + expect(span.meta).to.have.property('langchain.request.api_key', '...key>') + expect(span.meta).to.have.property('langchain.request.provider', 'anthropic') + expect(span.meta).to.have.property('langchain.request.model') + expect(span.meta).to.have.property('langchain.request.type', 'chat_model') + + expect(span.meta).to.have.property('langchain.request.messages.0.0.content', 'Hello!') + expect(span.meta).to.have.property('langchain.request.messages.0.0.message_type', 'HumanMessage') + + expect(span.meta).to.have.property('langchain.response.completions.0.0.content', 'Hello!') + expect(span.meta).to.have.property('langchain.response.completions.0.0.message_type', 'AIMessage') + }) + + const chatModel = new langchainAnthropic.ChatAnthropic({ model: 'claude-3-opus-20240229' }) + + const result = await chatModel.invoke('Hello!') + expect(result.content).to.equal('Hello!') + + await checkTraces + }) + }) + + describe('chain', () => { + it('does not tag output on error', async () => { + nock('https://api.openai.com').post('/v1/chat/completions').reply(403) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(2) + + const chainSpan = traces[0][0] + + const langchainResponseRegex = /^langchain\.response\.outputs\./ + + const hasMatching = Object.keys(chainSpan.meta).some(key => langchainResponseRegex.test(key)) + expect(hasMatching).to.be.false + + expect(chainSpan.meta).to.have.property('error.message') + expect(chainSpan.meta).to.have.property('error.type') + expect(chainSpan.meta).to.have.property('error.stack') + }) + + try { + const model = new langchainOpenai.ChatOpenAI({ model: 'gpt-4', maxRetries: 0 }) + const parser = new langchainOutputParsers.StringOutputParser() + + const chain = model.pipe(parser) + + await chain.invoke('Hello!') + } catch {} + + await checkTraces + }) + + it('instruments a langchain chain with a single openai chat model call', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Hi!' + }, + finish_reason: 'length', + index: 0 + }] + } + }) + + const checkTraces = agent + .use(traces => { + const spans = traces[0] + expect(spans).to.have.length(2) + + const chainSpan = spans[0] + // we already check the chat model span in previous tests + expect(spans[1]).to.have.property('resource', 'langchain.chat_models.openai.ChatOpenAI') + + expect(chainSpan).to.have.property('name', 'langchain.request') + expect(chainSpan).to.have.property('resource', 'langchain_core.runnables.RunnableSequence') + + expect(chainSpan.meta).to.have.property('langchain.request.type', 'chain') + + expect(chainSpan.meta).to.have.property( + 'langchain.request.inputs.0.content', 'You only respond with one word answers' + ) + expect(chainSpan.meta).to.have.property('langchain.request.inputs.1.content', 'Hello!') + + expect(chainSpan.meta).to.have.property('langchain.response.outputs.0', 'Hi!') + }) + + const model = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + const parser = new langchainOutputParsers.StringOutputParser() + + const chain = model.pipe(parser) + const messages = [ + new langchainMessages.SystemMessage('You only respond with one word answers'), + new langchainMessages.HumanMessage('Hello!') + ] + const result = await chain.invoke(messages) + + expect(result).to.equal('Hi!') + + await checkTraces + }) + + it('instruments a complex langchain chain', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Why did the chicken cross the road? To get to the other side!' + } + }] + } + }) + + const prompt = langchainPrompts.ChatPromptTemplate.fromTemplate( + 'Tell me a short joke about {topic} in the style of {style}' + ) + + const model = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + + const parser = new langchainOutputParsers.StringOutputParser() + + const chain = langchainRunnables.RunnableSequence.from([ + { + topic: new langchainRunnables.RunnablePassthrough(), + style: new langchainRunnables.RunnablePassthrough() + }, + prompt, + model, + parser + ]) + + const checkTraces = agent + .use(traces => { + const spans = traces[0] + expect(spans).to.have.length(2) + + const chainSpan = spans[0] + // we already check the chat model span in previous tests + expect(spans[1]).to.have.property('resource', 'langchain.chat_models.openai.ChatOpenAI') + + expect(chainSpan.meta).to.have.property('langchain.request.type', 'chain') + expect(chainSpan.meta).to.have.property('langchain.request.inputs.0.topic', 'chickens') + expect(chainSpan.meta).to.have.property('langchain.request.inputs.0.style', 'dad joke') + expect(chainSpan.meta).to.have.property( + 'langchain.response.outputs.0', 'Why did the chicken cross the road? To get to the other side!' + ) + }) + + const result = await chain.invoke({ topic: 'chickens', style: 'dad joke' }) + + expect(result).to.equal('Why did the chicken cross the road? To get to the other side!') + + await checkTraces + }) + + it('instruments a batched call', async () => { + stubCall({ + ...openAiBaseChatInfo, + response: [ + { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Why did the chicken cross the road? To get to the other side!' + } + }] + }, + { + model: 'gpt-4', + usage: { + prompt_tokens: 37, + completion_tokens: 10, + total_tokens: 47 + }, + choices: [{ + message: { + role: 'assistant', + content: 'Why was the dog confused? It was barking up the wrong tree!' + } + }] + } + ] + }) + + const prompt = langchainPrompts.ChatPromptTemplate.fromTemplate( + 'Tell me a joke about {topic}' + ) + const parser = new langchainOutputParsers.StringOutputParser() + const model = new langchainOpenai.ChatOpenAI({ model: 'gpt-4' }) + + const chain = langchainRunnables.RunnableSequence.from([ + { + topic: new langchainRunnables.RunnablePassthrough() + }, + prompt, + model, + parser + ]) + + const checkTraces = agent + .use(traces => { + const spans = traces[0] + expect(spans).to.have.length(3) // 1 chain + 2 chat model + + const chainSpan = spans[0] + + expect(chainSpan.meta).to.have.property('langchain.request.type', 'chain') + expect(chainSpan.meta).to.have.property('langchain.request.inputs.0', 'chickens') + expect(chainSpan.meta).to.have.property('langchain.request.inputs.1', 'dogs') + expect(chainSpan.meta).to.have.property( + 'langchain.response.outputs.0', 'Why did the chicken cross the road? To get to the other side!' + ) + expect(chainSpan.meta).to.have.property( + 'langchain.response.outputs.1', 'Why was the dog confused? It was barking up the wrong tree!' + ) + }) + + const result = await chain.batch(['chickens', 'dogs']) + + expect(result).to.have.length(2) + expect(result[0]).to.equal('Why did the chicken cross the road? To get to the other side!') + expect(result[1]).to.equal('Why was the dog confused? It was barking up the wrong tree!') + + await checkTraces + }) + + it('instruments a chain with a JSON output parser and tags it correctly', async function () { + if (!langchainOutputParsers.JsonOutputParser) this.skip() + + stubCall({ + ...openAiBaseChatInfo, + response: { + choices: [{ + message: { + role: 'assistant', + content: '{\n "name": "John",\n "age": 30\n}', + refusal: null + } + }] + } + }) + + const checkTraces = agent + .use(traces => { + const spans = traces[0] + expect(spans).to.have.length(2) // 1 chain + 1 chat model + + const chainSpan = spans[0] + + expect(chainSpan.meta).to.have.property('langchain.request.type', 'chain') + expect(chainSpan.meta).to.have.property( + 'langchain.request.inputs.0', 'Generate a JSON object with name and age.' + ) + + expect(chainSpan.meta).to.have.property('langchain.response.outputs.0', '{"name":"John","age":30}') + }) + + const parser = new langchainOutputParsers.JsonOutputParser() + const model = new langchainOpenai.ChatOpenAI({ model: 'gpt-3.5-turbo' }) + + const chain = model.pipe(parser) + + const response = await chain.invoke('Generate a JSON object with name and age.') + expect(response).to.deep.equal({ + name: 'John', + age: 30 + }) + + await checkTraces + }) + }) + + describe('embeddings', () => { + describe('@langchain/openai', () => { + it('does not tag output on error', async () => { + nock('https://api.openai.com').post('/v1/embeddings').reply(403) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + + const span = traces[0][0] + + expect(span.meta).to.not.have.property('langchain.response.outputs.embedding_length') + + expect(span.meta).to.have.property('error.message') + expect(span.meta).to.have.property('error.type') + expect(span.meta).to.have.property('error.stack') + }) + + try { + const embeddings = new langchainOpenai.OpenAIEmbeddings() + await embeddings.embedQuery('Hello, world!') + } catch {} + + await checkTraces + }) + + it('instruments a langchain openai embedQuery call', async () => { + stubCall({ + ...openAiBaseEmbeddingInfo, + response: { + object: 'list', + data: [{ + object: 'embedding', + index: 0, + embedding: [-0.0034387498, -0.026400521] + }] + } + }) + const embeddings = new langchainOpenai.OpenAIEmbeddings() + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span).to.have.property('name', 'langchain.request') + expect(span).to.have.property('resource', 'langchain.embeddings.openai.OpenAIEmbeddings') + + expect(span.meta).to.have.property('langchain.request.api_key', '...key>') + expect(span.meta).to.have.property('langchain.request.provider', 'openai') + expect(span.meta).to.have.property('langchain.request.model', 'text-embedding-ada-002') + expect(span.meta).to.have.property('langchain.request.type', 'embedding') + + expect(span.meta).to.have.property('langchain.request.inputs.0.text', 'Hello, world!') + expect(span.metrics).to.have.property('langchain.request.input_counts', 1) + expect(span.metrics).to.have.property('langchain.response.outputs.embedding_length', 2) + }) + + const query = 'Hello, world!' + const result = await embeddings.embedQuery(query) + + expect(result).to.have.length(2) + expect(result).to.deep.equal([-0.0034387498, -0.026400521]) + + await checkTraces + }) + + it('instruments a langchain openai embedDocuments call', async () => { + stubCall({ + ...openAiBaseEmbeddingInfo, + response: { + object: 'list', + data: [{ + object: 'embedding', + index: 0, + embedding: [-0.0034387498, -0.026400521] + }, { + object: 'embedding', + index: 1, + embedding: [-0.026400521, -0.0034387498] + }] + } + }) + + const checkTraces = agent + .use(traces => { + expect(traces[0].length).to.equal(1) + const span = traces[0][0] + + expect(span.meta).to.have.property('langchain.request.inputs.0.text', 'Hello, world!') + expect(span.meta).to.have.property('langchain.request.inputs.1.text', 'Goodbye, world!') + expect(span.metrics).to.have.property('langchain.request.input_counts', 2) + + expect(span.metrics).to.have.property('langchain.response.outputs.embedding_length', 2) + }) + + const embeddings = new langchainOpenai.OpenAIEmbeddings() + + const documents = ['Hello, world!', 'Goodbye, world!'] + const result = await embeddings.embedDocuments(documents) + + expect(result).to.have.length(2) + expect(result[0]).to.deep.equal([-0.0034387498, -0.026400521]) + expect(result[1]).to.deep.equal([-0.026400521, -0.0034387498]) + + 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 new file mode 100644 index 00000000000..bc505687115 --- /dev/null +++ b/packages/datadog-plugin-langchain/test/integration-test/client.spec.js @@ -0,0 +1,55 @@ +'use strict' + +const { + FakeAgent, + createSandbox, + checkSpansForServiceName, + spawnPluginIntegrationTestProc +} = require('../../../../integration-tests/helpers') +const { assert } = require('chai') + +// there is currently an issue with langchain + esm loader hooks from IITM +// https://github.com/nodejs/import-in-the-middle/issues/163 +describe.skip('esm', () => { + let agent + let proc + let sandbox + + withVersions('langchain', ['@langchain/core'], '>=0.1', version => { + before(async function () { + this.timeout(20000) + sandbox = await createSandbox([ + `@langchain/core@${version}`, + `@langchain/openai@${version}`, + 'nock' + ], false, [ + './packages/datadog-plugin-langchain/test/integration-test/*' + ]) + }) + + after(async () => { + await sandbox.remove() + }) + + beforeEach(async () => { + agent = await new FakeAgent().start() + }) + + afterEach(async () => { + proc?.kill() + await agent.stop() + }) + + 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(checkSpansForServiceName(payload, 'langchain.request'), true) + }) + + proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'server.mjs', agent.port) + + await res + }).timeout(20000) + }) +}) diff --git a/packages/datadog-plugin-langchain/test/integration-test/server.mjs b/packages/datadog-plugin-langchain/test/integration-test/server.mjs new file mode 100644 index 00000000000..b929824b7dd --- /dev/null +++ b/packages/datadog-plugin-langchain/test/integration-test/server.mjs @@ -0,0 +1,18 @@ +import 'dd-trace/init.js' +import { OpenAI } from '@langchain/openai' +import { StringOutputParser } from '@langchain/core/output_parsers' +import nock from 'nock' + +nock('https://api.openai.com:443') + .post('/v1/completions') + .reply(200, {}) + +const llm = new OpenAI({ + apiKey: '' +}) + +const parser = new StringOutputParser() + +const chain = llm.pipe(parser) + +await chain.invoke('a test') diff --git a/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs b/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs index bf174c489da..ce72c80e82d 100644 --- a/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs +++ b/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs @@ -6,7 +6,7 @@ import getPort from 'get-port' const port = await getPort() const gateway = Gateway({ edgemicro: { - port: port, + port, logging: { level: 'info', dir: os.tmpdir() } }, proxies: [] diff --git a/packages/datadog-plugin-mocha/src/index.js b/packages/datadog-plugin-mocha/src/index.js index 0513a4a95d6..1b40b9c5a1c 100644 --- a/packages/datadog-plugin-mocha/src/index.js +++ b/packages/datadog-plugin-mocha/src/index.js @@ -30,7 +30,12 @@ const { TEST_SUITE, MOCHA_IS_PARALLEL, TEST_IS_RUM_ACTIVE, - TEST_BROWSER_DRIVER + TEST_BROWSER_DRIVER, + TEST_NAME, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -47,6 +52,8 @@ const { const id = require('../../dd-trace/src/id') const log = require('../../dd-trace/src/log') +const debuggerParameterPerTest = new Map() + function getTestSuiteLevelVisibilityTags (testSuiteSpan) { const testSuiteSpanContext = testSuiteSpan.context() const suiteTags = { @@ -85,7 +92,7 @@ class MochaPlugin extends CiPlugin { } const relativeCoverageFiles = [...coverageFiles, suiteFile] - .map(filename => getTestSuitePath(filename, this.sourceRoot)) + .map(filename => getTestSuitePath(filename, this.repositoryRoot || this.sourceRoot)) const { _traceId, _spanId } = testSuiteSpan.context() @@ -185,6 +192,28 @@ class MochaPlugin extends CiPlugin { const store = storage.getStore() const span = this.startTestSpan(testInfo) + const { testName } = testInfo + + const debuggerParameters = debuggerParameterPerTest.get(testName) + + if (debuggerParameters) { + const spanContext = span.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + span.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + span.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + span.setTag(DI_DEBUG_ERROR_FILE, file) + span.setTag(DI_DEBUG_ERROR_LINE, line) + } + this.enter(span, store) }) @@ -242,7 +271,7 @@ class MochaPlugin extends CiPlugin { } }) - this.addSub('ci:mocha:test:retry', ({ isFirstAttempt, err }) => { + this.addSub('ci:mocha:test:retry', ({ isFirstAttempt, willBeRetried, err }) => { const store = storage.getStore() const span = store?.span if (span) { @@ -265,6 +294,11 @@ class MochaPlugin extends CiPlugin { browserDriver: spanTags[TEST_BROWSER_DRIVER] } ) + if (willBeRetried && this.di && this.libraryConfig?.isDiEnabled) { + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(err) + debuggerParameterPerTest.set(testName, debuggerParameters) + } span.finish() finishAllTraceSpans(span) diff --git a/packages/datadog-plugin-moleculer/src/server.js b/packages/datadog-plugin-moleculer/src/server.js index 98a667b4cc1..1f238a4338e 100644 --- a/packages/datadog-plugin-moleculer/src/server.js +++ b/packages/datadog-plugin-moleculer/src/server.js @@ -9,7 +9,6 @@ class MoleculerServerPlugin extends ServerPlugin { start ({ action, ctx, broker }) { const followsFrom = this.tracer.extract('text_map', ctx.meta) - this.startSpan(this.operationName(), { childOf: followsFrom || this.activeSpan, service: this.config.service || this.serviceName(), diff --git a/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs b/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs index 11fa3ac576b..0c643c53a7b 100644 --- a/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs +++ b/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs @@ -7,4 +7,3 @@ const db = client.db('test_db') const collection = db.collection('test_collection') collection.insertOne({ a: 1 }, {}, () => {}) setTimeout(() => { client.close() }, 1500) - diff --git a/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs b/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs index 39127aaab23..c11c934993d 100644 --- a/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs +++ b/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs @@ -15,7 +15,7 @@ const connectPromise = new Promise((resolve, reject) => { await server.connect() await connectPromise -server.insert(`test.your_collection_name`, [{ a: 1 }], {}, (err) => { +server.insert('test.your_collection_name', [{ a: 1 }], {}, (err) => { if (err) { return } diff --git a/packages/datadog-plugin-net/test/integration-test/server.mjs b/packages/datadog-plugin-net/test/integration-test/server.mjs index 4575498e13a..fc7ec19a696 100644 --- a/packages/datadog-plugin-net/test/integration-test/server.mjs +++ b/packages/datadog-plugin-net/test/integration-test/server.mjs @@ -14,4 +14,4 @@ client.on('end', () => { client.on('error', (err) => { client.end() -}) \ No newline at end of file +}) 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 054e2fc6357..5bd4825ce93 100644 --- a/packages/datadog-plugin-next/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-next/test/integration-test/client.spec.js @@ -30,7 +30,7 @@ describe('esm', () => { before(async function () { // next builds slower in the CI, match timeout with unit tests this.timeout(120 * 1000) - sandbox = await createSandbox([`'next@${version}'`, 'react', 'react-dom'], + sandbox = await createSandbox([`'next@${version}'`, 'react@^18.2.0', 'react-dom@^18.2.0'], false, ['./packages/datadog-plugin-next/test/integration-test/*'], BUILD_COMMAND) }) diff --git a/packages/datadog-plugin-openai/test/integration-test/server.mjs b/packages/datadog-plugin-openai/test/integration-test/server.mjs index 56a046d56c0..62d812baea8 100644 --- a/packages/datadog-plugin-openai/test/integration-test/server.mjs +++ b/packages/datadog-plugin-openai/test/integration-test/server.mjs @@ -23,7 +23,7 @@ nock('https://api.openai.com:443') ]) const openaiApp = new openai.OpenAIApi(new openai.Configuration({ - apiKey: 'sk-DATADOG-ACCEPTANCE-TESTS', + apiKey: 'sk-DATADOG-ACCEPTANCE-TESTS' })) await openaiApp.createCompletion({ diff --git a/packages/datadog-plugin-opensearch/test/integration-test/server.mjs b/packages/datadog-plugin-opensearch/test/integration-test/server.mjs index 0b45b5eefb2..21be1cead43 100644 --- a/packages/datadog-plugin-opensearch/test/integration-test/server.mjs +++ b/packages/datadog-plugin-opensearch/test/integration-test/server.mjs @@ -1,5 +1,5 @@ import 'dd-trace/init.js' import opensearch from '@opensearch-project/opensearch' -const client = new opensearch.Client({ node: `http://localhost:9201` }) +const client = new opensearch.Client({ node: 'http://localhost:9201' }) await client.ping() diff --git a/packages/datadog-plugin-oracledb/src/index.js b/packages/datadog-plugin-oracledb/src/index.js index 7c2f1da029f..eb4fa037cac 100644 --- a/packages/datadog-plugin-oracledb/src/index.js +++ b/packages/datadog-plugin-oracledb/src/index.js @@ -33,7 +33,7 @@ function getUrl (connectString) { try { return new URL(`http://${connectString}`) } catch (e) { - log.error(e) + log.error('Invalid oracle connection string', e) return {} } } diff --git a/packages/datadog-plugin-oracledb/test/integration-test/server.mjs b/packages/datadog-plugin-oracledb/test/integration-test/server.mjs index b50a7b36d13..739877fbcd7 100644 --- a/packages/datadog-plugin-oracledb/test/integration-test/server.mjs +++ b/packages/datadog-plugin-oracledb/test/integration-test/server.mjs @@ -7,13 +7,11 @@ const config = { user: 'test', password: 'Oracle18', connectString: `${hostname}:1521/xepdb1` -}; +} const dbQuery = 'select current_timestamp from dual' -let connection; - -connection = await oracledb.getConnection(config) +const connection = await oracledb.getConnection(config) await connection.execute(dbQuery) if (connection) { diff --git a/packages/datadog-plugin-router/test/index.spec.js b/packages/datadog-plugin-router/test/index.spec.js index ac208f0e2a1..31c3cde8bf5 100644 --- a/packages/datadog-plugin-router/test/index.spec.js +++ b/packages/datadog-plugin-router/test/index.spec.js @@ -71,8 +71,9 @@ describe('Plugin', () => { }) router.use('/parent', childRouter) - expect(router.stack[0].handle.hello).to.equal('goodbye') - expect(router.stack[0].handle.foo).to.equal('bar') + const index = router.stack.length - 1 + expect(router.stack[index].handle.hello).to.equal('goodbye') + expect(router.stack[index].handle.foo).to.equal('bar') }) it('should add the route to the request span', done => { diff --git a/packages/datadog-plugin-sharedb/test/integration-test/server.mjs b/packages/datadog-plugin-sharedb/test/integration-test/server.mjs index c0b93fbcab2..8b593029fc9 100644 --- a/packages/datadog-plugin-sharedb/test/integration-test/server.mjs +++ b/packages/datadog-plugin-sharedb/test/integration-test/server.mjs @@ -4,4 +4,4 @@ import ShareDB from 'sharedb' const backend = new ShareDB({ presence: true }) const connection = backend.connect() await connection.get('some-collection', 'some-id').fetch() -connection.close() \ No newline at end of file +connection.close() diff --git a/packages/datadog-plugin-vitest/src/index.js b/packages/datadog-plugin-vitest/src/index.js index 34617bdb1ac..ba2554bf9f9 100644 --- a/packages/datadog-plugin-vitest/src/index.js +++ b/packages/datadog-plugin-vitest/src/index.js @@ -17,7 +17,12 @@ const { TEST_SOURCE_START, TEST_IS_NEW, TEST_EARLY_FLAKE_ENABLED, - TEST_EARLY_FLAKE_ABORT_REASON + TEST_EARLY_FLAKE_ABORT_REASON, + TEST_NAME, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -31,6 +36,8 @@ const { // This is because there's some loss of resolution. const MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION = 5 +const debuggerParameterPerTest = new Map() + class VitestPlugin extends CiPlugin { static get id () { return 'vitest' @@ -81,6 +88,26 @@ class VitestPlugin extends CiPlugin { extraTags ) + const debuggerParameters = debuggerParameterPerTest.get(testName) + + if (debuggerParameters) { + const spanContext = span.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + span.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + span.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + span.setTag(DI_DEBUG_ERROR_FILE, file) + span.setTag(DI_DEBUG_ERROR_LINE, line) + } + this.enter(span, store) }) @@ -110,11 +137,16 @@ class VitestPlugin extends CiPlugin { } }) - this.addSub('ci:vitest:test:error', ({ duration, error }) => { + this.addSub('ci:vitest:test:error', ({ duration, error, willBeRetried, probe, isDiEnabled }) => { const store = storage.getStore() const span = store?.span if (span) { + if (willBeRetried && this.di && isDiEnabled) { + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(error, probe) + debuggerParameterPerTest.set(testName, debuggerParameters) + } this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', { hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS] }) diff --git a/packages/datadog-shimmer/src/shimmer.js b/packages/datadog-shimmer/src/shimmer.js index d12c4c130ef..e5f2f189381 100644 --- a/packages/datadog-shimmer/src/shimmer.js +++ b/packages/datadog-shimmer/src/shimmer.js @@ -6,8 +6,12 @@ const log = require('../../dd-trace/src/log') const unwrappers = new WeakMap() function copyProperties (original, wrapped) { - Object.setPrototypeOf(wrapped, original) - + // TODO getPrototypeOf is not fast. Should we instead do this in specific + // instrumentations where needed? + const proto = Object.getPrototypeOf(original) + if (proto !== Function.prototype) { + Object.setPrototypeOf(wrapped, proto) + } const props = Object.getOwnPropertyDescriptors(original) const keys = Reflect.ownKeys(props) @@ -136,7 +140,7 @@ function wrapMethod (target, name, wrapper, noAssert) { if (callState.completed) { // error was thrown after original function returned/resolved, so // it was us. log it. - log.error(e) + log.error('Shimmer error was thrown after original function returned/resolved', e) // original ran and returned something. return it. return callState.retVal } @@ -144,7 +148,7 @@ function wrapMethod (target, name, wrapper, noAssert) { if (!callState.called) { // error was thrown before original function was called, so // it was us. log it. - log.error(e) + log.error('Shimmer error was thrown before original function was called', e) // original never ran. call it unwrapped. return original.apply(this, args) } diff --git a/packages/dd-trace/src/appsec/addresses.js b/packages/dd-trace/src/appsec/addresses.js index cb540bc4e6f..a492a5e454f 100644 --- a/packages/dd-trace/src/appsec/addresses.js +++ b/packages/dd-trace/src/appsec/addresses.js @@ -1,5 +1,6 @@ 'use strict' +// TODO: reorder all this, it's a mess module.exports = { HTTP_INCOMING_BODY: 'server.request.body', HTTP_INCOMING_QUERY: 'server.request.query', @@ -20,6 +21,8 @@ module.exports = { HTTP_CLIENT_IP: 'http.client_ip', USER_ID: 'usr.id', + USER_LOGIN: 'usr.login', + WAF_CONTEXT_PROCESSOR: 'waf.context.processor', HTTP_OUTGOING_URL: 'server.io.net.url', diff --git a/packages/dd-trace/src/appsec/api_security_sampler.js b/packages/dd-trace/src/appsec/api_security_sampler.js index 68bd896af7e..1e15b67a260 100644 --- a/packages/dd-trace/src/appsec/api_security_sampler.js +++ b/packages/dd-trace/src/appsec/api_security_sampler.js @@ -1,61 +1,84 @@ 'use strict' +const TTLCache = require('@isaacs/ttlcache') +const web = require('../plugins/util/web') const log = require('../log') +const { AUTO_REJECT, USER_REJECT } = require('../../../../ext/priority') + +const MAX_SIZE = 4096 let enabled -let requestSampling +let sampledRequests -const sampledRequests = new WeakSet() +class NoopTTLCache { + clear () { } + set (key) { return undefined } + has (key) { return false } +} function configure ({ apiSecurity }) { enabled = apiSecurity.enabled - setRequestSampling(apiSecurity.requestSampling) + sampledRequests = apiSecurity.sampleDelay === 0 + ? new NoopTTLCache() + : new TTLCache({ max: MAX_SIZE, ttl: apiSecurity.sampleDelay * 1000 }) } function disable () { enabled = false + sampledRequests?.clear() } -function setRequestSampling (sampling) { - requestSampling = parseRequestSampling(sampling) -} +function sampleRequest (req, res, force = false) { + if (!enabled) return false -function parseRequestSampling (requestSampling) { - let parsed = parseFloat(requestSampling) + const key = computeKey(req, res) + if (!key || isSampled(key)) return false - if (isNaN(parsed)) { - log.warn(`Incorrect API Security request sampling value: ${requestSampling}`) + const rootSpan = web.root(req) + if (!rootSpan) return false - parsed = 0 - } else { - parsed = Math.min(1, Math.max(0, parsed)) + let priority = getSpanPriority(rootSpan) + if (!priority) { + rootSpan._prioritySampler?.sample(rootSpan) + priority = getSpanPriority(rootSpan) } - return parsed -} - -function sampleRequest (req) { - if (!enabled || !requestSampling) { + if (priority === AUTO_REJECT || priority === USER_REJECT) { return false } - const shouldSample = Math.random() <= requestSampling - - if (shouldSample) { - sampledRequests.add(req) + if (force) { + sampledRequests.set(key) } - return shouldSample + return true +} + +function isSampled (key) { + return sampledRequests.has(key) +} + +function computeKey (req, res) { + const route = web.getContext(req)?.paths?.join('') || '' + const method = req.method + const status = res.statusCode + + if (!method || !status) { + log.warn('[ASM] Unsupported groupkey for API security') + return null + } + return method + route + status } -function isSampled (req) { - return sampledRequests.has(req) +function getSpanPriority (span) { + const spanContext = span.context?.() + return spanContext._sampling?.priority } module.exports = { configure, disable, - setRequestSampling, sampleRequest, - isSampled + isSampled, + computeKey } diff --git a/packages/dd-trace/src/appsec/blocked_templates.js b/packages/dd-trace/src/appsec/blocked_templates.js index 1eb62e22df0..3017d4de9db 100644 --- a/packages/dd-trace/src/appsec/blocked_templates.js +++ b/packages/dd-trace/src/appsec/blocked_templates.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const html = `You've been blocked

Sorry, you cannot access this page. Please contact the customer service team.

` diff --git a/packages/dd-trace/src/appsec/blocking.js b/packages/dd-trace/src/appsec/blocking.js index cdf92f7023a..d831b310eb3 100644 --- a/packages/dd-trace/src/appsec/blocking.js +++ b/packages/dd-trace/src/appsec/blocking.js @@ -101,7 +101,7 @@ function getBlockingData (req, specificType, actionParameters) { function block (req, res, rootSpan, abortController, actionParameters = defaultBlockingActionParameters) { if (res.headersSent) { - log.warn('Cannot send blocking response when headers have already been sent') + log.warn('[ASM] Cannot send blocking response when headers have already been sent') return } diff --git a/packages/dd-trace/src/appsec/channels.js b/packages/dd-trace/src/appsec/channels.js index 8e7f27211c6..1368e937dc9 100644 --- a/packages/dd-trace/src/appsec/channels.js +++ b/packages/dd-trace/src/appsec/channels.js @@ -19,6 +19,7 @@ module.exports = { nextBodyParsed: dc.channel('apm:next:body-parsed'), nextQueryParsed: dc.channel('apm:next:query-parsed'), expressProcessParams: dc.channel('datadog:express:process_params:start'), + routerParam: dc.channel('datadog:router:param:start'), responseBody: dc.channel('datadog:express:response:json:start'), responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'), httpClientRequestStart: dc.channel('apm:http:client:request:start'), diff --git a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js index 36f6036cf54..c1608ae1261 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js @@ -15,6 +15,7 @@ module.exports = { PATH_TRAVERSAL_ANALYZER: require('./path-traversal-analyzer'), SQL_INJECTION_ANALYZER: require('./sql-injection-analyzer'), SSRF: require('./ssrf-analyzer'), + TEMPLATE_INJECTION_ANALYZER: require('./template-injection-analyzer'), UNVALIDATED_REDIRECT_ANALYZER: require('./unvalidated-redirect-analyzer'), WEAK_CIPHER_ANALYZER: require('./weak-cipher-analyzer'), WEAK_HASH_ANALYZER: require('./weak-hash-analyzer'), 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 f8937417e42..3741c12ef8f 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 @@ -11,6 +11,10 @@ class CodeInjectionAnalyzer extends InjectionAnalyzer { onConfigure () { this.addSub('datadog:eval:call', ({ script }) => this.analyze(script)) } + + _areRangesVulnerable () { + return true + } } module.exports = new CodeInjectionAnalyzer() 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 2b125b88403..a898a0a379c 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js @@ -2,7 +2,7 @@ const Analyzer = require('./vulnerability-analyzer') const { getNodeModulesPaths } = require('../path-line') -const iastLog = require('../iast-log') +const log = require('../../../log') const EXCLUDED_PATHS = getNodeModulesPaths('express/lib/response.js') @@ -16,7 +16,7 @@ class CookieAnalyzer extends Analyzer { try { this.cookieFilterRegExp = new RegExp(config.iast.cookieFilterPattern) } catch { - iastLog.error('Invalid regex in cookieFilterPattern') + log.error('[ASM] Invalid regex in cookieFilterPattern') this.cookieFilterRegExp = /.{32,}/ } diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js index 2e204b72830..04e243c8b5a 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const { NameAndValue } = require('./hardcoded-rule-type') diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js index 88ec3d54254..1d61c5fcc91 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const { ValueOnly, NameAndValue } = require('./hardcoded-rule-type') diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js index 88ec3d54254..1d61c5fcc91 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const { ValueOnly, NameAndValue } = require('./hardcoded-rule-type') 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 cb4bc2866b0..f0d42bf95ae 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js @@ -1,12 +1,15 @@ 'use strict' const Analyzer = require('./vulnerability-analyzer') -const { isTainted, getRanges } = require('../taint-tracking/operations') +const { getRanges } = require('../taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../taint-tracking/source-types') class InjectionAnalyzer extends Analyzer { _isVulnerable (value, iastContext) { - if (value) { - return isTainted(iastContext, value) + const ranges = value && getRanges(iastContext, value) + if (ranges?.length > 0) { + return this._areRangesVulnerable(ranges) } + return false } @@ -14,6 +17,10 @@ class InjectionAnalyzer extends Analyzer { const ranges = getRanges(iastContext, value) return { value, ranges } } + + _areRangesVulnerable (ranges) { + return ranges?.some(range => range.iinfo.type !== SQL_ROW_VALUE) + } } module.exports = InjectionAnalyzer diff --git a/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js index 4d302ece1b6..8f7ca5a39ed 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js @@ -82,6 +82,10 @@ class SqlInjectionAnalyzer extends InjectionAnalyzer { return knexDialect.toUpperCase() } } + + _areRangesVulnerable () { + return true + } } module.exports = new SqlInjectionAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js new file mode 100644 index 00000000000..8a5af919b2d --- /dev/null +++ b/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js @@ -0,0 +1,22 @@ +'use strict' + +const InjectionAnalyzer = require('./injection-analyzer') +const { TEMPLATE_INJECTION } = require('../vulnerabilities') + +class TemplateInjectionAnalyzer extends InjectionAnalyzer { + constructor () { + super(TEMPLATE_INJECTION) + } + + onConfigure () { + this.addSub('datadog:handlebars:compile:start', ({ source }) => this.analyze(source)) + this.addSub('datadog:handlebars:register-partial:start', ({ partial }) => this.analyze(partial)) + this.addSub('datadog:pug:compile:start', ({ source }) => this.analyze(source)) + } + + _areRangesVulnerable () { + return true + } +} + +module.exports = new TemplateInjectionAnalyzer() diff --git a/packages/dd-trace/src/appsec/iast/iast-log.js b/packages/dd-trace/src/appsec/iast/iast-log.js deleted file mode 100644 index c126729f965..00000000000 --- a/packages/dd-trace/src/appsec/iast/iast-log.js +++ /dev/null @@ -1,86 +0,0 @@ -'use strict' - -const dc = require('dc-polyfill') -const log = require('../../log') - -const telemetryLog = dc.channel('datadog:telemetry:log') - -function getTelemetryLog (data, level) { - try { - data = typeof data === 'function' ? data() : data - - let message - if (typeof data !== 'object' || !data) { - message = String(data) - } else { - message = String(data.message || data) - } - - const logEntry = { - message, - level - } - if (data.stack) { - logEntry.stack_trace = data.stack - } - return logEntry - } catch (e) { - log.error(e) - } -} - -const iastLog = { - debug (data) { - log.debug(data) - return this - }, - - info (data) { - log.info(data) - return this - }, - - warn (data) { - log.warn(data) - return this - }, - - error (data) { - log.error(data) - return this - }, - - publish (data, level) { - if (telemetryLog.hasSubscribers) { - telemetryLog.publish(getTelemetryLog(data, level)) - } - return this - }, - - debugAndPublish (data) { - this.debug(data) - return this.publish(data, 'DEBUG') - }, - - /** - * forward 'INFO' log level to 'DEBUG' telemetry log level - * see also {@link ../../telemetry/logs#isLevelEnabled } method - */ - infoAndPublish (data) { - this.info(data) - return this.publish(data, 'DEBUG') - }, - - warnAndPublish (data) { - this.warn(data) - return this.publish(data, 'WARN') - }, - - errorAndPublish (data) { - this.error(data) - // publish is done automatically by log.error() - return this - } -} - -module.exports = iastLog diff --git a/packages/dd-trace/src/appsec/iast/iast-plugin.js b/packages/dd-trace/src/appsec/iast/iast-plugin.js index 5eb6e00410d..42dab0a4af1 100644 --- a/packages/dd-trace/src/appsec/iast/iast-plugin.js +++ b/packages/dd-trace/src/appsec/iast/iast-plugin.js @@ -2,7 +2,6 @@ const { channel } = require('dc-polyfill') -const iastLog = require('./iast-log') const Plugin = require('../../plugins/plugin') const iastTelemetry = require('./telemetry') const { getInstrumentedMetric, getExecutedMetric, TagKey, EXECUTED_SOURCE, formatTags } = @@ -10,6 +9,7 @@ const { getInstrumentedMetric, getExecutedMetric, TagKey, EXECUTED_SOURCE, forma 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 @@ -60,24 +60,10 @@ class IastPlugin extends Plugin { this.pluginSubs = [] } - _wrapHandler (handler) { - return (message, name) => { - try { - handler(message, name) - } catch (e) { - iastLog.errorAndPublish(e) - } - } - } - _getTelemetryHandler (iastSub) { return () => { - try { - const iastContext = getIastContext(storage.getStore()) - iastSub.increaseExecuted(iastContext) - } catch (e) { - iastLog.errorAndPublish(e) - } + const iastContext = getIastContext(storage.getStore()) + iastSub.increaseExecuted(iastContext) } } @@ -93,17 +79,17 @@ class IastPlugin extends Plugin { } return result } catch (e) { - iastLog.errorAndPublish(e) + log.error('[ASM] Error executing handler or increasing metrics', e) } } addSub (iastSub, handler) { if (typeof iastSub === 'string') { - super.addSub(iastSub, this._wrapHandler(handler)) + super.addSub(iastSub, handler) } else { iastSub = this._getAndRegisterSubscription(iastSub) if (iastSub) { - super.addSub(iastSub.channelName, this._wrapHandler(handler)) + super.addSub(iastSub.channelName, handler) if (iastTelemetry.isEnabled()) { super.addSub(iastSub.channelName, this._getTelemetryHandler(iastSub)) @@ -112,7 +98,8 @@ class IastPlugin extends Plugin { } } - enable () { + enable (iastConfig) { + this.iastConfig = iastConfig this.configure(true) } 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 5c7109c4cda..b541629f3b7 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/index.js @@ -18,10 +18,10 @@ module.exports = { enableTaintTracking (config, telemetryVerbosity) { enableRewriter(telemetryVerbosity) enableTaintOperations(telemetryVerbosity) - taintTrackingPlugin.enable() + taintTrackingPlugin.enable(config) - kafkaContextPlugin.enable() - kafkaConsumerPlugin.enable() + kafkaContextPlugin.enable(config) + kafkaConsumerPlugin.enable(config) setMaxTransactions(config.maxConcurrentRequests) }, 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 f678767394a..d8580061b9e 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,7 +2,7 @@ const TaintedUtils = require('@datadog/native-iast-taint-tracking') const { IAST_TRANSACTION_ID } = require('../iast-context') -const iastLog = require('../iast-log') +const log = require('../../../log') function taintObject (iastContext, object, type) { let result = object @@ -33,7 +33,7 @@ function taintObject (iastContext, object, type) { } } } catch (e) { - iastLog.error(`Error visiting property : ${property}`).errorAndPublish(e) + log.error('[ASM] Error in taintObject when visiting property : %s', property, e) } } } 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 ed46cbe5f2e..9e236666619 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js @@ -12,7 +12,8 @@ const { HTTP_REQUEST_HEADER_NAME, HTTP_REQUEST_PARAMETER, HTTP_REQUEST_PATH_PARAM, - HTTP_REQUEST_URI + HTTP_REQUEST_URI, + SQL_ROW_VALUE } = require('./source-types') const { EXECUTED_SOURCE } = require('../telemetry/iast-metric') @@ -26,6 +27,16 @@ class TaintTrackingPlugin extends SourceIastPlugin { this._taintedURLs = new WeakMap() } + configure (config) { + super.configure(config) + + let rowsToTaint = this.iastConfig?.dbRowsToTaint + if (typeof rowsToTaint !== 'number') { + rowsToTaint = 1 + } + this._rowsToTaint = rowsToTaint + } + onConfigure () { const onRequestBody = ({ req }) => { const iastContext = getIastContext(storage.getStore()) @@ -46,8 +57,13 @@ class TaintTrackingPlugin extends SourceIastPlugin { ) this.addSub( - { channelName: 'datadog:qs:parse:finish', tag: HTTP_REQUEST_PARAMETER }, - ({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs) + { channelName: 'datadog:query:read:finish', tag: HTTP_REQUEST_PARAMETER }, + ({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query) + ) + + this.addSub( + { channelName: 'datadog:express:query:finish', tag: HTTP_REQUEST_PARAMETER }, + ({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query) ) this.addSub( @@ -68,6 +84,16 @@ class TaintTrackingPlugin extends SourceIastPlugin { ({ cookies }) => this._cookiesTaintTrackingHandler(cookies) ) + this.addSub( + { channelName: 'datadog:sequelize:query:finish', tag: SQL_ROW_VALUE }, + ({ result }) => this._taintDatabaseResult(result, 'sequelize') + ) + + this.addSub( + { channelName: 'apm:pg:query:finish', tag: SQL_ROW_VALUE }, + ({ result }) => this._taintDatabaseResult(result, 'pg') + ) + this.addSub( { channelName: 'datadog:express:process_params:start', tag: HTTP_REQUEST_PATH_PARAM }, ({ req }) => { @@ -77,6 +103,15 @@ class TaintTrackingPlugin extends SourceIastPlugin { } ) + this.addSub( + { channelName: 'datadog:router:param:start', tag: HTTP_REQUEST_PATH_PARAM }, + ({ req }) => { + if (req && req.params !== null && typeof req.params === 'object') { + this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params') + } + } + ) + this.addSub( { channelName: 'apm:graphql:resolve:start', tag: HTTP_REQUEST_BODY }, (data) => { @@ -170,6 +205,32 @@ class TaintTrackingPlugin extends SourceIastPlugin { this.taintHeaders(req.headers, iastContext) this.taintUrl(req, iastContext) } + + _taintDatabaseResult (result, dbOrigin, iastContext = getIastContext(storage.getStore()), name) { + if (!iastContext) return result + + if (this._rowsToTaint === 0) return result + + if (Array.isArray(result)) { + for (let i = 0; i < result.length && i < this._rowsToTaint; i++) { + const nextName = name ? `${name}.${i}` : '' + i + result[i] = this._taintDatabaseResult(result[i], dbOrigin, iastContext, nextName) + } + } else if (result && typeof result === 'object') { + if (dbOrigin === 'sequelize' && result.dataValues) { + result.dataValues = this._taintDatabaseResult(result.dataValues, dbOrigin, iastContext, name) + } else { + for (const key in result) { + const nextName = name ? `${name}.${key}` : key + result[key] = this._taintDatabaseResult(result[key], dbOrigin, iastContext, nextName) + } + } + } else if (typeof result === 'string') { + result = newTaintedString(iastContext, result, name, SQL_ROW_VALUE) + } + + return result + } } module.exports = new TaintTrackingPlugin() 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 cad8e5d6b18..168408d5261 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js @@ -2,12 +2,12 @@ const Module = require('module') const shimmer = require('../../../../../datadog-shimmer') -const iastLog = require('../iast-log') const { isPrivateModule, isNotLibraryFile } = require('./filter') const { csiMethods } = require('./csi-methods') const { getName } = require('../telemetry/verbosity') const { getRewriteFunction } = require('./rewriter-telemetry') const dc = require('dc-polyfill') +const log = require('../../../log') const hardcodedSecretCh = dc.channel('datadog:secrets:result') let rewriter @@ -60,8 +60,7 @@ function getRewriter (telemetryVerbosity) { chainSourceMap }) } catch (e) { - iastLog.error('Unable to initialize TaintTracking Rewriter') - .errorAndPublish(e) + log.error('[ASM] Unable to initialize TaintTracking Rewriter', e) } } return rewriter @@ -99,8 +98,7 @@ function getCompileMethodFn (compileMethod) { } } } catch (e) { - iastLog.error(`Error rewriting ${filename}`) - .errorAndPublish(e) + log.error('[ASM] Error rewriting file %s', filename, e) } return compileMethod.apply(this, [content, filename]) } @@ -117,8 +115,7 @@ function enableRewriter (telemetryVerbosity) { shimmer.wrap(Module.prototype, '_compile', compileMethod => getCompileMethodFn(compileMethod)) } } catch (e) { - iastLog.error('Error enabling TaintTracking Rewriter') - .errorAndPublish(e) + log.error('[ASM] Error enabling TaintTracking Rewriter', e) } } @@ -132,7 +129,7 @@ function disableRewriter () { Error.prepareStackTrace = originalPrepareStackTrace } catch (e) { - iastLog.warn(e) + log.warn('[ASM] Error disabling TaintTracking rewriter', e) } } diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js b/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js index f5c2ca2e8b0..f3ccf0505c3 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js @@ -11,5 +11,6 @@ module.exports = { HTTP_REQUEST_PATH_PARAM: 'http.request.path.parameter', HTTP_REQUEST_URI: 'http.request.uri', KAFKA_MESSAGE_KEY: 'kafka.message.key', - KAFKA_MESSAGE_VALUE: 'kafka.message.value' + KAFKA_MESSAGE_VALUE: 'kafka.message.value', + SQL_ROW_VALUE: 'sql.row.value' } 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 5fa16d00d77..6b1554d6449 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 @@ -4,10 +4,10 @@ const dc = require('dc-polyfill') const TaintedUtils = require('@datadog/native-iast-taint-tracking') const { storage } = require('../../../../../datadog-core') const iastContextFunctions = require('../iast-context') -const iastLog = require('../iast-log') const { EXECUTED_PROPAGATION } = require('../telemetry/iast-metric') const { isDebugAllowed } = require('../telemetry/verbosity') const { taintObject } = require('./operations-taint-object') +const log = require('../../../log') const mathRandomCallCh = dc.channel('datadog:random:call') const evalCallCh = dc.channel('datadog:eval:call') @@ -60,8 +60,7 @@ function getFilteredCsiFn (cb, filter, getContext) { return cb(transactionId, res, target, ...rest) } } catch (e) { - iastLog.error(`Error invoking CSI ${target}`) - .errorAndPublish(e) + log.error('[ASM] Error invoking CSI %s', target, e) } return res } @@ -112,8 +111,7 @@ function csiMethodsOverrides (getContext) { return TaintedUtils.concat(transactionId, res, op1, op2) } } catch (e) { - iastLog.error('Error invoking CSI plusOperator') - .errorAndPublish(e) + log.error('[ASM] Error invoking CSI plusOperator', e) } return res }, @@ -126,8 +124,7 @@ function csiMethodsOverrides (getContext) { return TaintedUtils.concat(transactionId, res, ...rest) } } catch (e) { - iastLog.error('Error invoking CSI tplOperator') - .errorAndPublish(e) + log.error('[ASM] Error invoking CSI tplOperator', e) } return res }, @@ -178,7 +175,7 @@ function csiMethodsOverrides (getContext) { } } } catch (e) { - iastLog.error(e) + log.error('[ASM] Error invoking CSI JSON.parse', e) } } @@ -194,7 +191,7 @@ function csiMethodsOverrides (getContext) { res = TaintedUtils.arrayJoin(transactionId, res, target, separator) } } catch (e) { - iastLog.error(e) + log.error('[ASM] Error invoking CSI join', e) } } @@ -250,8 +247,7 @@ function lodashTaintTrackingHandler (message) { message.result = getLodashTaintedUtilFn(message.operation)(transactionId, message.result, ...message.arguments) } } catch (e) { - iastLog.error(`Error invoking CSI lodash ${message.operation}`) - .errorAndPublish(e) + log.error('[ASM] Error invoking CSI lodash %s', message.operation, e) } } diff --git a/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js b/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js index 77a0db04604..de460270405 100644 --- a/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js +++ b/packages/dd-trace/src/appsec/iast/telemetry/namespaces.js @@ -4,7 +4,6 @@ const log = require('../../../log') const { Namespace } = require('../../../telemetry/metrics') const { addMetricsToSpan } = require('./span-tags') const { IAST_TRACE_METRIC_PREFIX } = require('../tags') -const iastLog = require('../iast-log') const DD_IAST_METRICS_NAMESPACE = Symbol('_dd.iast.request.metrics.namespace') @@ -31,7 +30,7 @@ function finalizeRequestNamespace (context, rootSpan) { namespace.clear() } catch (e) { - log.error(e) + log.error('[ASM] Error merging request metrics', e) } finally { if (context) { delete context[DD_IAST_METRICS_NAMESPACE] @@ -79,7 +78,7 @@ class IastNamespace extends Namespace { if (metrics.size === this.maxMetricTagsSize) { metrics.clear() - iastLog.warnAndPublish(`Tags cache max size reached for metric ${name}`) + log.error('[ASM] Tags cache max size reached for metric %s', name) } metrics.set(tags, metric) diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js index abf341a1a1f..eb9e550b00e 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const iastLog = require('../../../iast-log') +const log = require('../../../../../log') const COMMAND_PATTERN = '^(?:\\s*(?:sudo|doas)\\s+)?\\b\\S+\\b\\s(.*)' const pattern = new RegExp(COMMAND_PATTERN, 'gmi') @@ -16,7 +16,7 @@ module.exports = function extractSensitiveRanges (evidence) { return [{ start, end }] } } catch (e) { - iastLog.debug(e) + log.debug('[ASM] Error extracting sensitive ranges', e) } return [] } diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js index 93497465afe..cb14b2816f8 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const iastLog = require('../../../iast-log') +const log = require('../../../../../log') const LDAP_PATTERN = '\\(.*?(?:~=|=|<=|>=)(?[^)]+)\\)' const pattern = new RegExp(LDAP_PATTERN, 'gmi') @@ -22,7 +22,7 @@ module.exports = function extractSensitiveRanges (evidence) { } return tokens } catch (e) { - iastLog.debug(e) + log.debug('[ASM] Error extracting sensitive ranges', e) } return [] } diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js index 15580b11869..0a3a389fd60 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const iastLog = require('../../../iast-log') +const log = require('../../../../../log') const STRING_LITERAL = '\'(?:\'\'|[^\'])*\'' const POSTGRESQL_ESCAPED_LITERAL = '\\$([^$]*)\\$.*?\\$\\1\\$' @@ -106,7 +106,7 @@ module.exports = function extractSensitiveRanges (evidence) { } return tokens } catch (e) { - iastLog.debug(e) + log.debug('[ASM] Error extracting sensitive ranges', e) } return [] } diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/code-injection-sensitive-analyzer.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/tainted-range-based-sensitive-analyzer.js similarity index 100% rename from packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/code-injection-sensitive-analyzer.js rename to packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/tainted-range-based-sensitive-analyzer.js diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js index 6f43008d2c3..e945ed62539 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js @@ -1,6 +1,6 @@ 'use strict' -const iastLog = require('../../../iast-log') +const log = require('../../../../../log') const AUTHORITY = '^(?:[^:]+:)?//([^@]+)@' const QUERY_FRAGMENT = '[?#&]([^=&;]+)=([^?#&]+)' @@ -33,7 +33,7 @@ module.exports = function extractSensitiveRanges (evidence) { return ranges } catch (e) { - iastLog.debug(e) + log.debug('[ASM] Error extracting sensitive ranges', e) } return [] diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js index 39117dc5a34..9c6c48dbf54 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js @@ -1,17 +1,17 @@ 'use strict' -const iastLog = require('../../iast-log') +const log = require('../../../../log') const vulnerabilities = require('../../vulnerabilities') const { contains, intersects, remove } = require('./range-utils') -const codeInjectionSensitiveAnalyzer = require('./sensitive-analyzers/code-injection-sensitive-analyzer') const commandSensitiveAnalyzer = require('./sensitive-analyzers/command-sensitive-analyzer') const hardcodedPasswordAnalyzer = require('./sensitive-analyzers/hardcoded-password-analyzer') const headerSensitiveAnalyzer = require('./sensitive-analyzers/header-sensitive-analyzer') const jsonSensitiveAnalyzer = require('./sensitive-analyzers/json-sensitive-analyzer') const ldapSensitiveAnalyzer = require('./sensitive-analyzers/ldap-sensitive-analyzer') const sqlSensitiveAnalyzer = require('./sensitive-analyzers/sql-sensitive-analyzer') +const taintedRangeBasedSensitiveAnalyzer = require('./sensitive-analyzers/tainted-range-based-sensitive-analyzer') const urlSensitiveAnalyzer = require('./sensitive-analyzers/url-sensitive-analyzer') const { DEFAULT_IAST_REDACTION_NAME_PATTERN, DEFAULT_IAST_REDACTION_VALUE_PATTERN } = require('./sensitive-regex') @@ -24,7 +24,8 @@ class SensitiveHandler { this._valuePattern = new RegExp(DEFAULT_IAST_REDACTION_VALUE_PATTERN, 'gmi') this._sensitiveAnalyzers = new Map() - this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, codeInjectionSensitiveAnalyzer) + this._sensitiveAnalyzers.set(vulnerabilities.CODE_INJECTION, taintedRangeBasedSensitiveAnalyzer) + this._sensitiveAnalyzers.set(vulnerabilities.TEMPLATE_INJECTION, taintedRangeBasedSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, commandSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.NOSQL_MONGODB_INJECTION, jsonSensitiveAnalyzer) this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, ldapSensitiveAnalyzer) @@ -281,7 +282,7 @@ class SensitiveHandler { try { this._namePattern = new RegExp(redactionNamePattern, 'gmi') } catch (e) { - iastLog.warn('Redaction name pattern is not valid') + log.warn('[ASM] Redaction name pattern is not valid') } } @@ -289,7 +290,7 @@ class SensitiveHandler { try { this._valuePattern = new RegExp(redactionValuePattern, 'gmi') } catch (e) { - iastLog.warn('Redaction value pattern is not valid') + log.warn('[ASM] Redaction value pattern is not valid') } } } diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js index fe9d22f9c49..e0054b8546f 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js @@ -1,6 +1,6 @@ -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|(?:sur|last)name|user(?:name)?|address|e?mail)' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,})|[\\w\\.-]+@[a-zA-Z\\d\\.-]+\\.[a-zA-Z]{2,})' module.exports = { diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js index 959df790afd..256b47f5532 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js @@ -7,7 +7,7 @@ const STRINGIFY_RANGE_KEY = 'DD_' + crypto.randomBytes(20).toString('hex') const STRINGIFY_SENSITIVE_KEY = STRINGIFY_RANGE_KEY + 'SENSITIVE' const STRINGIFY_SENSITIVE_NOT_STRING_KEY = STRINGIFY_SENSITIVE_KEY + 'NOTSTRING' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const KEYS_REGEX_WITH_SENSITIVE_RANGES = new RegExp(`(?:"(${STRINGIFY_RANGE_KEY}_\\d+_))|(?:"(${STRINGIFY_SENSITIVE_KEY}_\\d+_(\\d+)_))|("${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_\\d+_([\\s0-9.a-zA-Z]*)")`, 'gm') const KEYS_REGEX_WITHOUT_SENSITIVE_RANGES = new RegExp(`"(${STRINGIFY_RANGE_KEY}_\\d+_)`, 'gm') diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities.js b/packages/dd-trace/src/appsec/iast/vulnerabilities.js index 790ec6c5db9..90287c27d91 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities.js @@ -13,6 +13,7 @@ module.exports = { PATH_TRAVERSAL: 'PATH_TRAVERSAL', SQL_INJECTION: 'SQL_INJECTION', SSRF: 'SSRF', + TEMPLATE_INJECTION: 'TEMPLATE_INJECTION', UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT', WEAK_CIPHER: 'WEAK_CIPHER', WEAK_HASH: 'WEAK_HASH', diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index e2d1619b118..05aea14cf02 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -17,13 +17,36 @@ let resetVulnerabilityCacheTimer let deduplicationEnabled = true function addVulnerability (iastContext, vulnerability) { - if (vulnerability && vulnerability.evidence && vulnerability.type && - vulnerability.location) { - if (iastContext && iastContext.rootSpan) { + if (vulnerability?.evidence && vulnerability?.type && vulnerability?.location) { + if (deduplicationEnabled && isDuplicatedVulnerability(vulnerability)) return + + VULNERABILITY_HASHES.set(`${vulnerability.type}${vulnerability.hash}`, true) + + let span = iastContext?.rootSpan + + if (!span && tracer) { + span = tracer.startSpan('vulnerability', { + type: 'vulnerability' + }) + + vulnerability.location.spanId = span.context().toSpanId() + + span.addTags({ + [IAST_ENABLED_TAG_KEY]: 1 + }) + } + + if (!span) return + + keepTrace(span, SAMPLING_MECHANISM_APPSEC) + standalone.sample(span) + + if (iastContext?.rootSpan) { iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || [] iastContext[VULNERABILITIES_KEY].push(vulnerability) } else { - sendVulnerabilities([vulnerability]) + sendVulnerabilities([vulnerability], span) + span.finish() } } } @@ -34,36 +57,17 @@ function isValidVulnerability (vulnerability) { vulnerability.location && vulnerability.location.spanId } -function sendVulnerabilities (vulnerabilities, rootSpan) { +function sendVulnerabilities (vulnerabilities, span) { if (vulnerabilities && vulnerabilities.length) { - let span = rootSpan - if (!span && tracer) { - span = tracer.startSpan('vulnerability', { - type: 'vulnerability' - }) - vulnerabilities.forEach((vulnerability) => { - vulnerability.location.spanId = span.context().toSpanId() - }) - span.addTags({ - [IAST_ENABLED_TAG_KEY]: 1 - }) - } - if (span && span.addTags) { - const validAndDedupVulnerabilities = deduplicateVulnerabilities(vulnerabilities).filter(isValidVulnerability) - const jsonToSend = vulnerabilitiesFormatter.toJson(validAndDedupVulnerabilities) + const validatedVulnerabilities = vulnerabilities.filter(isValidVulnerability) + const jsonToSend = vulnerabilitiesFormatter.toJson(validatedVulnerabilities) if (jsonToSend.vulnerabilities.length > 0) { const tags = {} // TODO: Store this outside of the span and set the tag in the exporter. tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend) span.addTags(tags) - - keepTrace(span, SAMPLING_MECHANISM_APPSEC) - - standalone.sample(span) - - if (!rootSpan) span.finish() } } } @@ -86,17 +90,8 @@ function stopClearCacheTimer () { } } -function deduplicateVulnerabilities (vulnerabilities) { - if (!deduplicationEnabled) return vulnerabilities - const deduplicated = vulnerabilities.filter((vulnerability) => { - const key = `${vulnerability.type}${vulnerability.hash}` - if (!VULNERABILITY_HASHES.get(key)) { - VULNERABILITY_HASHES.set(key, true) - return true - } - return false - }) - return deduplicated +function isDuplicatedVulnerability (vulnerability) { + return VULNERABILITY_HASHES.get(`${vulnerability.type}${vulnerability.hash}`) } function start (config, _tracer) { diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index f4f9a4db036..db089a61dca 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -16,7 +16,8 @@ const { expressProcessParams, responseBody, responseWriteHead, - responseSetHeader + responseSetHeader, + routerParam } = require('./channels') const waf = require('./waf') const addresses = require('./addresses') @@ -27,7 +28,7 @@ const web = require('../plugins/util/web') const { extractIp } = require('../plugins/util/ip_extractor') const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking') -const { passportTrackEvent } = require('./passport') +const UserTracking = require('./user_tracking') const { storage } = require('../../../datadog-core') const graphql = require('./graphql') const rasp = require('./rasp') @@ -58,28 +59,27 @@ function enable (_config) { apiSecuritySampler.configure(_config.appsec) + UserTracking.setCollectionMode(_config.appsec.eventTracking.mode, false) + bodyParser.subscribe(onRequestBodyParsed) multerParser.subscribe(onRequestBodyParsed) cookieParser.subscribe(onRequestCookieParser) incomingHttpRequestStart.subscribe(incomingHttpStartTranslator) incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator) + passportVerify.subscribe(onPassportVerify) // possible optimization: only subscribe if collection mode is enabled queryParser.subscribe(onRequestQueryParsed) nextBodyParsed.subscribe(onRequestBodyParsed) nextQueryParsed.subscribe(onRequestQueryParsed) expressProcessParams.subscribe(onRequestProcessParams) + routerParam.subscribe(onRequestProcessParams) responseBody.subscribe(onResponseBody) responseWriteHead.subscribe(onResponseWriteHead) responseSetHeader.subscribe(onResponseSetHeader) - if (_config.appsec.eventTracking.enabled) { - passportVerify.subscribe(onPassportVerify) - } - isEnabled = true config = _config } catch (err) { - log.error('Unable to start AppSec') - log.error(err) + log.error('[ASM] Unable to start AppSec', err) disable() } @@ -145,10 +145,6 @@ function incomingHttpStartTranslator ({ req, res, abortController }) { persistent[addresses.HTTP_CLIENT_IP] = clientIp } - if (apiSecuritySampler.sampleRequest(req)) { - persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true } - } - const actions = waf.run({ persistent }, req) handleResults(actions, req, res, rootSpan, abortController) @@ -168,8 +164,14 @@ function incomingHttpEndTranslator ({ req, res }) { persistent[addresses.HTTP_INCOMING_COOKIES] = req.cookies } - if (req.query !== null && typeof req.query === 'object') { - persistent[addresses.HTTP_INCOMING_QUERY] = req.query + // we need to keep this to support nextjs + const query = req.query + if (query !== null && typeof query === 'object') { + persistent[addresses.HTTP_INCOMING_QUERY] = query + } + + if (apiSecuritySampler.sampleRequest(req, res, true)) { + persistent[addresses.WAF_CONTEXT_PROCESSOR] = { 'extract-schema': true } } if (Object.keys(persistent).length) { @@ -181,16 +183,18 @@ function incomingHttpEndTranslator ({ req, res }) { Reporter.finishRequest(req, res) } -function onPassportVerify ({ credentials, user }) { +function onPassportVerify ({ framework, login, user, success, abortController }) { const store = storage.getStore() const rootSpan = store?.req && web.root(store.req) if (!rootSpan) { - log.warn('No rootSpan found in onPassportVerify') + log.warn('[ASM] No rootSpan found in onPassportVerify') return } - passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode) + const results = UserTracking.trackLogin(framework, login, user, success, rootSpan) + + handleResults(results, store.req, store.req.res, rootSpan, abortController) } function onRequestQueryParsed ({ req, res, query, abortController }) { @@ -228,9 +232,9 @@ function onRequestProcessParams ({ req, res, abortController, params }) { handleResults(results, req, res, rootSpan, abortController) } -function onResponseBody ({ req, body }) { +function onResponseBody ({ req, res, body }) { if (!body || typeof body !== 'object') return - if (!apiSecuritySampler.isSampled(req)) return + if (!apiSecuritySampler.sampleRequest(req, res)) return // we don't support blocking at this point, so no results needed waf.run({ @@ -310,6 +314,7 @@ function disable () { if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed) if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed) if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams) + if (routerParam.hasSubscribers) routerParam.unsubscribe(onRequestProcessParams) if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody) if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead) if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader) diff --git a/packages/dd-trace/src/appsec/passport.js b/packages/dd-trace/src/appsec/passport.js deleted file mode 100644 index 2093b7b1fdc..00000000000 --- a/packages/dd-trace/src/appsec/passport.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -const log = require('../log') -const { trackEvent } = require('./sdk/track_event') -const { setUserTags } = require('./sdk/set_user') - -const UUID_PATTERN = '^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$' -const regexUsername = new RegExp(UUID_PATTERN, 'i') - -const SDK_USER_EVENT_PATTERN = '^_dd\\.appsec\\.events\\.users\\.[\\W\\w+]+\\.sdk$' -const regexSdkEvent = new RegExp(SDK_USER_EVENT_PATTERN, 'i') - -function isSdkCalled (tags) { - let called = false - - if (tags !== null && typeof tags === 'object') { - called = Object.entries(tags).some(([key, value]) => regexSdkEvent.test(key) && value === 'true') - } - - return called -} - -// delete this function later if we know it's always credential.username -function getLogin (credentials) { - const type = credentials && credentials.type - let login - if (type === 'local' || type === 'http') { - login = credentials.username - } - - return login -} - -function parseUser (login, passportUser, mode) { - const user = { - 'usr.id': login - } - - if (!user['usr.id']) { - return user - } - - if (passportUser) { - // Guess id - if (passportUser.id) { - user['usr.id'] = passportUser.id - } else if (passportUser._id) { - user['usr.id'] = passportUser._id - } - - if (mode === 'extended') { - if (login) { - user['usr.login'] = login - } - - if (passportUser.email) { - user['usr.email'] = passportUser.email - } - - // Guess username - if (passportUser.username) { - user['usr.username'] = passportUser.username - } else if (passportUser.name) { - user['usr.username'] = passportUser.name - } - } - } - - if (mode === 'safe') { - // Remove PII in safe mode - if (!regexUsername.test(user['usr.id'])) { - user['usr.id'] = '' - } - } - - return user -} - -function passportTrackEvent (credentials, passportUser, rootSpan, mode) { - const tags = rootSpan && rootSpan.context() && rootSpan.context()._tags - - if (isSdkCalled(tags)) { - // Don't overwrite tags set by SDK callings - return - } - const user = parseUser(getLogin(credentials), passportUser, mode) - - if (user['usr.id'] === undefined) { - log.warn('No user ID found in authentication instrumentation') - return - } - - if (passportUser) { - // If a passportUser object is published then the login succeded - const userTags = {} - Object.entries(user).forEach(([k, v]) => { - const attr = k.split('.', 2)[1] - userTags[attr] = v - }) - - setUserTags(userTags, rootSpan) - trackEvent('users.login.success', null, 'passportTrackEvent', rootSpan, mode) - } else { - trackEvent('users.login.failure', user, 'passportTrackEvent', rootSpan, mode) - } -} - -module.exports = { - passportTrackEvent -} diff --git a/packages/dd-trace/src/appsec/rasp/fs-plugin.js b/packages/dd-trace/src/appsec/rasp/fs-plugin.js index a283b4f1a61..71f9cf3c6b5 100644 --- a/packages/dd-trace/src/appsec/rasp/fs-plugin.js +++ b/packages/dd-trace/src/appsec/rasp/fs-plugin.js @@ -70,7 +70,7 @@ function enable (mod) { fsPlugin.enable() } - log.info(`Enabled AppsecFsPlugin for ${mod}`) + log.info('[ASM] Enabled AppsecFsPlugin for %s', mod) } function disable (mod) { @@ -85,7 +85,7 @@ function disable (mod) { fsPlugin = undefined } - log.info(`Disabled AppsecFsPlugin for ${mod}`) + log.info('[ASM] Disabled AppsecFsPlugin for %s', mod) } module.exports = { diff --git a/packages/dd-trace/src/appsec/rasp/utils.js b/packages/dd-trace/src/appsec/rasp/utils.js index bdf3596209e..a454a71b8c6 100644 --- a/packages/dd-trace/src/appsec/rasp/utils.js +++ b/packages/dd-trace/src/appsec/rasp/utils.js @@ -8,7 +8,7 @@ const log = require('../../log') const abortOnUncaughtException = process.execArgv?.includes('--abort-on-uncaught-exception') if (abortOnUncaughtException) { - log.warn('The --abort-on-uncaught-exception flag is enabled. The RASP module will not block operations.') + log.warn('[ASM] The --abort-on-uncaught-exception flag is enabled. The RASP module will not block operations.') } const RULE_TYPES = { diff --git a/packages/dd-trace/src/appsec/recommended.json b/packages/dd-trace/src/appsec/recommended.json index 01156e6f206..35e36c9159c 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.13.2" + "rules_version": "1.13.3" }, "rules": [ { @@ -9,7 +9,8 @@ "name": "Block IP Addresses", "tags": { "type": "block_ip", - "category": "security_response" + "category": "security_response", + "module": "network-acl" }, "conditions": [ { @@ -34,7 +35,8 @@ "name": "Block User Addresses", "tags": { "type": "block_user", - "category": "security_response" + "category": "security_response", + "module": "authentication-acl" }, "conditions": [ { @@ -64,7 +66,8 @@ "tool_name": "Acunetix", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -98,7 +101,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -162,7 +166,8 @@ "category": "attack_attempt", "cwe": "176", "capec": "1000/255/153/267/71", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -191,7 +196,8 @@ "crs_id": "921110", "category": "attack_attempt", "cwe": "444", - "capec": "1000/210/272/220/33" + "capec": "1000/210/272/220/33", + "module": "waf" }, "conditions": [ { @@ -228,7 +234,8 @@ "crs_id": "921160", "category": "attack_attempt", "cwe": "113", - "capec": "1000/210/272/220/105" + "capec": "1000/210/272/220/105", + "module": "waf" }, "conditions": [ { @@ -263,7 +270,8 @@ "category": "attack_attempt", "cwe": "22", "capec": "1000/255/153/126", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -297,7 +305,8 @@ "category": "attack_attempt", "cwe": "22", "capec": "1000/255/153/126", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -1803,7 +1812,8 @@ "category": "attack_attempt", "cwe": "98", "capec": "1000/152/175/253/193", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -1831,7 +1841,8 @@ "crs_id": "931120", "category": "attack_attempt", "cwe": "98", - "capec": "1000/152/175/253/193" + "capec": "1000/152/175/253/193", + "module": "waf" }, "conditions": [ { @@ -1876,7 +1887,8 @@ "category": "attack_attempt", "cwe": "77", "capec": "1000/152/248/88", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2388,7 +2400,8 @@ "category": "attack_attempt", "cwe": "77", "capec": "1000/152/248/88", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2436,7 +2449,8 @@ "category": "attack_attempt", "cwe": "706", "capec": "1000/225/122/17/177", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2500,7 +2514,8 @@ "category": "attack_attempt", "cwe": "434", "capec": "1000/225/122/17/650", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2553,7 +2568,8 @@ "category": "attack_attempt", "cwe": "94", "capec": "1000/225/122/17/650", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2620,7 +2636,8 @@ "crs_id": "933131", "category": "attack_attempt", "cwe": "94", - "capec": "1000/225/122/17/650" + "capec": "1000/225/122/17/650", + "module": "waf" }, "conditions": [ { @@ -2665,7 +2682,8 @@ "category": "attack_attempt", "cwe": "94", "capec": "1000/225/122/17/650", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2709,7 +2727,8 @@ "category": "attack_attempt", "cwe": "94", "capec": "1000/225/122/17/650", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2799,7 +2818,8 @@ "crs_id": "933160", "category": "attack_attempt", "cwe": "94", - "capec": "1000/225/122/17/650" + "capec": "1000/225/122/17/650", + "module": "waf" }, "conditions": [ { @@ -2824,7 +2844,7 @@ "address": "graphql.server.resolver" } ], - "regex": "\\b(?:s(?:e(?:t(?:_(?:e(?:xception|rror)_handler|magic_quotes_runtime|include_path)|defaultstub)|ssion_s(?:et_save_handler|tart))|qlite_(?:(?:(?:unbuffered|single|array)_)?query|create_(?:aggregate|function)|p?open|exec)|tr(?:eam_(?:context_create|socket_client)|ipc?slashes|rev)|implexml_load_(?:string|file)|ocket_c(?:onnect|reate)|h(?:ow_sourc|a1_fil)e|pl_autoload_register|ystem)|p(?:r(?:eg_(?:replace(?:_callback(?:_array)?)?|match(?:_all)?|split)|oc_(?:(?:terminat|clos|nic)e|get_status|open)|int_r)|o(?:six_(?:get(?:(?:e[gu]|g)id|login|pwnam)|mk(?:fifo|nod)|ttyname|kill)|pen)|hp(?:_(?:strip_whitespac|unam)e|version|info)|g_(?:(?:execut|prepar)e|connect|query)|a(?:rse_(?:ini_file|str)|ssthru)|utenv)|r(?:unkit_(?:function_(?:re(?:defin|nam)e|copy|add)|method_(?:re(?:defin|nam)e|copy|add)|constant_(?:redefine|add))|e(?:(?:gister_(?:shutdown|tick)|name)_function|ad(?:(?:gz)?file|_exif_data|dir))|awurl(?:de|en)code)|i(?:mage(?:createfrom(?:(?:jpe|pn)g|x[bp]m|wbmp|gif)|(?:jpe|pn)g|g(?:d2?|if)|2?wbmp|xbm)|s_(?:(?:(?:execut|write?|read)ab|fi)le|dir)|ni_(?:get(?:_all)?|set)|terator_apply|ptcembed)|g(?:et(?:_(?:c(?:urrent_use|fg_va)r|meta_tags)|my(?:[gpu]id|inode)|(?:lastmo|cw)d|imagesize|env)|z(?:(?:(?:defla|wri)t|encod|fil)e|compress|open|read)|lob)|a(?:rray_(?:u(?:intersect(?:_u?assoc)?|diff(?:_u?assoc)?)|intersect_u(?:assoc|key)|diff_u(?:assoc|key)|filter|reduce|map)|ssert(?:_options)?|tob)|h(?:tml(?:specialchars(?:_decode)?|_entity_decode|entities)|(?:ash(?:_(?:update|hmac))?|ighlight)_file|e(?:ader_register_callback|x2bin))|f(?:i(?:le(?:(?:[acm]tim|inod)e|(?:_exist|perm)s|group)?|nfo_open)|tp_(?:nb_(?:ge|pu)|connec|ge|pu)t|(?:unction_exis|pu)ts|write|open)|o(?:b_(?:get_(?:c(?:ontents|lean)|flush)|end_(?:clean|flush)|clean|flush|start)|dbc_(?:result(?:_all)?|exec(?:ute)?|connect)|pendir)|m(?:b_(?:ereg(?:_(?:replace(?:_callback)?|match)|i(?:_replace)?)?|parse_str)|(?:ove_uploaded|d5)_file|ethod_exists|ysql_query|kdir)|e(?:x(?:if_(?:t(?:humbnail|agname)|imagetype|read_data)|ec)|scapeshell(?:arg|cmd)|rror_reporting|val)|c(?:url_(?:file_create|exec|init)|onvert_uuencode|reate_function|hr)|u(?:n(?:serialize|pack)|rl(?:de|en)code|[ak]?sort)|b(?:(?:son_(?:de|en)|ase64_en)code|zopen|toa)|(?:json_(?:de|en)cod|debug_backtrac|tmpfil)e|var_dump)(?:\\s|/\\*.*\\*/|//.*|#.*|\\\"|')*\\((?:(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:\\$\\w+|[A-Z\\d]\\w*|\\w+\\(.*\\)|\\\\?\"(?:[^\"]|\\\\\"|\"\"|\"\\+\")*\\\\?\"|\\\\?'(?:[^']|''|'\\+')*\\\\?')(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:(?:::|\\.|->)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\w+(?:\\(.*\\))?)?,)*(?:(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:\\$\\w+|[A-Z\\d]\\w*|\\w+\\(.*\\)|\\\\?\"(?:[^\"]|\\\\\"|\"\"|\"\\+\")*\\\\?\"|\\\\?'(?:[^']|''|'\\+')*\\\\?')(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:(?:::|\\.|->)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\w+(?:\\(.*\\))?)?)?\\)", + "regex": "\\b(?:s(?:e(?:t(?:_(?:e(?:xception|rror)_handler|magic_quotes_runtime|include_path)|defaultstub)|ssion_s(?:et_save_handler|tart))|qlite_(?:(?:(?:unbuffered|single|array)_)?query|create_(?:aggregate|function)|p?open|exec)|tr(?:eam_(?:context_create|socket_client)|ipc?slashes|rev)|implexml_load_(?:string|file)|ocket_c(?:onnect|reate)|h(?:ow_sourc|a1_fil)e|pl_autoload_register|ystem)|p(?:r(?:eg_(?:replace(?:_callback(?:_array)?)?|match(?:_all)?|split)|oc_(?:(?:terminat|clos|nic)e|get_status|open)|int_r)|o(?:six_(?:get(?:(?:e[gu]|g)id|login|pwnam)|mk(?:fifo|nod)|ttyname|kill)|pen)|hp(?:_(?:strip_whitespac|unam)e|version|info)|g_(?:(?:execut|prepar)e|connect|query)|a(?:rse_(?:ini_file|str)|ssthru)|utenv)|r(?:unkit_(?:function_(?:re(?:defin|nam)e|copy|add)|method_(?:re(?:defin|nam)e|copy|add)|constant_(?:redefine|add))|e(?:(?:gister_(?:shutdown|tick)|name)_function|ad(?:(?:gz)?file|_exif_data|dir))|awurl(?:de|en)code)|i(?:mage(?:createfrom(?:(?:jpe|pn)g|x[bp]m|wbmp|gif)|(?:jpe|pn)g|g(?:d2?|if)|2?wbmp|xbm)|s_(?:(?:(?:execut|write?|read)ab|fi)le|dir)|ni_(?:get(?:_all)?|set)|terator_apply|ptcembed)|g(?:et(?:_(?:c(?:urrent_use|fg_va)r|meta_tags)|my(?:[gpu]id|inode)|(?:lastmo|cw)d|imagesize|env)|z(?:(?:(?:defla|wri)t|encod|fil)e|compress|open|read)|lob)|a(?:rray_(?:u(?:intersect(?:_u?assoc)?|diff(?:_u?assoc)?)|intersect_u(?:assoc|key)|diff_u(?:assoc|key)|filter|reduce|map)|ssert(?:_options)?|tob)|h(?:tml(?:specialchars(?:_decode)?|_entity_decode|entities)|(?:ash(?:_(?:update|hmac))?|ighlight)_file|e(?:ader_register_callback|x2bin))|f(?:i(?:le(?:(?:[acm]tim|inod)e|(?:_exist|perm)s|group)?|nfo_open)|tp_(?:nb_(?:ge|pu)|connec|ge|pu)t|(?:unction_exis|pu)ts|write|open)|o(?:b_(?:get_(?:c(?:ontents|lean)|flush)|end_(?:clean|flush)|clean|flush|start)|dbc_(?:result(?:_all)?|exec(?:ute)?|connect)|pendir)|m(?:b_(?:ereg(?:_(?:replace(?:_callback)?|match)|i(?:_replace)?)?|parse_str)|(?:ove_uploaded|d5)_file|ethod_exists|ysql_query|kdir)|e(?:x(?:if_(?:t(?:humbnail|agname)|imagetype|read_data)|ec)|scapeshell(?:arg|cmd)|rror_reporting|val)|c(?:url_(?:file_create|exec|init)|onvert_uuencode|reate_function|hr)|u(?:n(?:serialize|pack)|rl(?:de|en)code|[ak]?sort)|b(?:(?:son_(?:de|en)|ase64_en)code|zopen|toa)|(?:json_(?:de|en)cod|debug_backtrac|tmpfil)e|var_dump)(?:\\s|/\\*.*\\*/|//.*|#.*|\\\"|')*\\((?:(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:\\$\\w+|[A-Z\\d]\\w*|\\w+\\(.*\\)|\\\\?\"(?:[^\"]|\\\\\"|\"\"|\"\\+\")*\\\\?\"|\\\\?'(?:[^']|''|'\\+')*\\\\?')(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:(?:::|\\.|->)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\w+(?:\\(.*\\))?)?,)*(?:(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:\\$\\w+|[A-Z\\d]\\w*|\\w+\\(.*\\)|\\\\?\"(?:[^\"]|\\\\\"|\"\"|\"\\+\")*\\\\?\"|\\\\?'(?:[^']|''|'\\+')*\\\\?')(?:\\s|/\\*.*\\*/|//.*|#.*)*(?:(?:::|\\.|->)(?:\\s|/\\*.*\\*/|//.*|#.*)*\\w+(?:\\(.*\\))?)?)?\\)\\s*(?:[;\\.)}\\]|\\\\]|\\?>|%>|$)", "options": { "case_sensitive": true, "min_length": 5 @@ -2844,7 +2864,8 @@ "category": "attack_attempt", "cwe": "502", "capec": "1000/152/586", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -2891,7 +2912,8 @@ "crs_id": "933200", "category": "attack_attempt", "cwe": "502", - "capec": "1000/152/586" + "capec": "1000/152/586", + "module": "waf" }, "conditions": [ { @@ -2937,7 +2959,8 @@ "crs_id": "934100", "category": "attack_attempt", "cwe": "94", - "capec": "1000/152/242" + "capec": "1000/152/242", + "module": "waf" }, "conditions": [ { @@ -2982,7 +3005,8 @@ "category": "attack_attempt", "confidence": "1", "cwe": "94", - "capec": "1000/152/242" + "capec": "1000/152/242", + "module": "waf" }, "conditions": [ { @@ -3024,7 +3048,8 @@ "category": "attack_attempt", "cwe": "80", "capec": "1000/152/242/63/591", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3081,7 +3106,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3140,7 +3166,8 @@ "category": "attack_attempt", "cwe": "84", "capec": "1000/152/242/63/591/244", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3199,7 +3226,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3257,7 +3285,8 @@ "crs_id": "941180", "category": "attack_attempt", "cwe": "79", - "capec": "1000/152/242/63/591" + "capec": "1000/152/242/63/591", + "module": "waf" }, "conditions": [ { @@ -3311,7 +3340,8 @@ "category": "attack_attempt", "cwe": "80", "capec": "1000/152/242/63/591", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3358,7 +3388,8 @@ "category": "attack_attempt", "cwe": "80", "capec": "1000/152/242/63/591", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3405,7 +3436,8 @@ "category": "attack_attempt", "cwe": "80", "capec": "1000/152/242/63/591", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3452,7 +3484,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3498,7 +3531,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3545,7 +3579,8 @@ "crs_id": "941270", "category": "attack_attempt", "cwe": "83", - "capec": "1000/152/242/63/591/243" + "capec": "1000/152/242/63/591/243", + "module": "waf" }, "conditions": [ { @@ -3588,7 +3623,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3634,7 +3670,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3680,7 +3717,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3726,7 +3764,8 @@ "category": "attack_attempt", "cwe": "87", "capec": "1000/152/242/63/591/199", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3770,7 +3809,8 @@ "crs_id": "941360", "category": "attack_attempt", "cwe": "87", - "capec": "1000/152/242/63/591/199" + "capec": "1000/152/242/63/591/199", + "module": "waf" }, "conditions": [ { @@ -3815,7 +3855,8 @@ "category": "attack_attempt", "confidence": "1", "cwe": "79", - "capec": "1000/152/242/63/591" + "capec": "1000/152/242/63/591", + "module": "waf" }, "conditions": [ { @@ -3859,7 +3900,8 @@ "crs_id": "942100", "category": "attack_attempt", "cwe": "89", - "capec": "1000/152/248/66" + "capec": "1000/152/248/66", + "module": "waf" }, "conditions": [ { @@ -3898,7 +3940,8 @@ "category": "attack_attempt", "cwe": "89", "capec": "1000/152/248/66/7", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3943,7 +3986,8 @@ "category": "attack_attempt", "cwe": "89", "capec": "1000/152/248/66/7", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -3986,7 +4030,8 @@ "crs_id": "942250", "category": "attack_attempt", "cwe": "89", - "capec": "1000/152/248/66" + "capec": "1000/152/248/66", + "module": "waf" }, "conditions": [ { @@ -4030,7 +4075,8 @@ "crs_id": "942270", "category": "attack_attempt", "cwe": "89", - "capec": "1000/152/248/66" + "capec": "1000/152/248/66", + "module": "waf" }, "conditions": [ { @@ -4074,7 +4120,8 @@ "category": "attack_attempt", "cwe": "89", "capec": "1000/152/248/66/7", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4117,7 +4164,8 @@ "crs_id": "942290", "category": "attack_attempt", "cwe": "943", - "capec": "1000/152/248/676" + "capec": "1000/152/248/676", + "module": "waf" }, "conditions": [ { @@ -4163,7 +4211,8 @@ "crs_id": "942360", "category": "attack_attempt", "cwe": "89", - "capec": "1000/152/248/66/470" + "capec": "1000/152/248/66/470", + "module": "waf" }, "conditions": [ { @@ -4206,7 +4255,8 @@ "crs_id": "942500", "category": "attack_attempt", "cwe": "89", - "capec": "1000/152/248/66" + "capec": "1000/152/248/66", + "module": "waf" }, "conditions": [ { @@ -4251,7 +4301,8 @@ "category": "attack_attempt", "cwe": "384", "capec": "1000/225/21/593/61", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4296,7 +4347,8 @@ "category": "attack_attempt", "cwe": "94", "capec": "1000/152/242", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4344,7 +4396,8 @@ "type": "java_code_injection", "category": "attack_attempt", "cwe": "94", - "capec": "1000/152/242" + "capec": "1000/152/242", + "module": "waf" }, "conditions": [ { @@ -4391,7 +4444,8 @@ "crs_id": "944130", "category": "attack_attempt", "cwe": "94", - "capec": "1000/152/242" + "capec": "1000/152/242", + "module": "waf" }, "conditions": [ { @@ -4529,7 +4583,8 @@ "type": "nosql_injection", "category": "attack_attempt", "cwe": "943", - "capec": "1000/152/248/676" + "capec": "1000/152/248/676", + "module": "waf" }, "conditions": [ { @@ -4573,7 +4628,8 @@ "type": "java_code_injection", "category": "attack_attempt", "cwe": "94", - "capec": "1000/152/242" + "capec": "1000/152/242", + "module": "waf" }, "conditions": [ { @@ -4619,7 +4675,8 @@ "category": "attack_attempt", "cwe": "94", "capec": "1000/152/242", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4695,7 +4752,8 @@ "category": "attack_attempt", "cwe": "1321", "capec": "1000/152/242", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4725,7 +4783,8 @@ "category": "attack_attempt", "cwe": "1321", "capec": "1000/152/242", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4769,7 +4828,8 @@ "category": "attack_attempt", "cwe": "1336", "capec": "1000/152/242/19", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4813,7 +4873,8 @@ "tool_name": "BurpCollaborator", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4857,7 +4918,8 @@ "tool_name": "Qualys", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -4901,7 +4963,8 @@ "tool_name": "Probely", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -4944,7 +5007,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -4987,7 +5051,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5031,7 +5096,8 @@ "tool_name": "Rapid7", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5075,7 +5141,8 @@ "tool_name": "interact.sh", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5119,7 +5186,8 @@ "tool_name": "Netsparker", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5167,7 +5235,8 @@ "tool_name": "WhiteHatSecurity", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5215,7 +5284,8 @@ "tool_name": "Nessus", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5263,7 +5333,8 @@ "tool_name": "Watchtowr", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5311,7 +5382,8 @@ "tool_name": "AppCheckNG", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5358,7 +5430,8 @@ "category": "attack_attempt", "cwe": "287", "capec": "1000/225/115", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5392,7 +5465,8 @@ "category": "attack_attempt", "cwe": "98", "capec": "1000/152/175/253/193", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5436,7 +5510,8 @@ "category": "attack_attempt", "cwe": "77", "capec": "1000/152/248/88", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -5483,7 +5558,8 @@ "category": "attack_attempt", "cwe": "91", "capec": "1000/152/248/250", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5521,7 +5597,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5579,7 +5656,8 @@ "category": "attack_attempt", "cwe": "83", "capec": "1000/152/242/63/591/243", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5866,7 +5944,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5908,7 +5987,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5950,7 +6030,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -5992,7 +6073,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6034,7 +6116,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6059,7 +6142,7 @@ "address": "server.request.uri.raw" } ], - "regex": "\\.(cgi|bat|dll|exe|key|cert|crt|pem|der|pkcs|pkcs|pkcs[0-9]*|nsf|jsa|war|java|class|vb|vba|so|git|svn|hg|cvs)([^a-zA-Z0-9_]|$)", + "regex": "\\.(cgi|bat|dll|exe|key|cert|crt|pem|der|pkcs|pkcs|pkcs[0-9]*|nsf|jsa|war|java|class|vb|vba|so|git|svn|hg|cvs)([?#&/]|$)", "options": { "case_sensitive": false } @@ -6076,7 +6159,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6118,7 +6202,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6160,7 +6245,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6202,7 +6288,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -6276,7 +6363,7 @@ } ] }, - "operator": "lfi_detector" + "operator": "lfi_detector@v2" } ], "transformers": [], @@ -6286,7 +6373,7 @@ }, { "id": "rasp-932-100", - "name": "Command injection exploit", + "name": "Shell command injection exploit", "tags": { "type": "command_injection", "category": "vulnerability_trigger", @@ -6332,6 +6419,54 @@ "stack_trace" ] }, + { + "id": "rasp-932-110", + "name": "OS command injection exploit", + "tags": { + "type": "command_injection", + "category": "vulnerability_trigger", + "cwe": "77", + "capec": "1000/152/248/88", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.sys.exec.cmd" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "cmdi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace" + ] + }, { "id": "rasp-934-100", "name": "Server-side request forgery exploit", @@ -6438,7 +6573,8 @@ "category": "attack_attempt", "cwe": "918", "capec": "1000/225/115/664", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6482,7 +6618,8 @@ "type": "js_code_injection", "category": "attack_attempt", "cwe": "94", - "capec": "1000/152/242" + "capec": "1000/152/242", + "module": "waf" }, "conditions": [ { @@ -6527,7 +6664,8 @@ "category": "attack_attempt", "cwe": "78", "capec": "1000/152/248/88", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6570,7 +6708,8 @@ "category": "attack_attempt", "cwe": "78", "capec": "1000/152/248/88", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6615,7 +6754,8 @@ "category": "attack_attempt", "cwe": "78", "capec": "1000/152/248/88", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6658,7 +6798,8 @@ "category": "attack_attempt", "cwe": "918", "capec": "1000/225/115/664", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6701,7 +6842,8 @@ "category": "attack_attempt", "cwe": "918", "capec": "1000/225/115/664", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -6743,7 +6885,8 @@ "category": "attack_attempt", "cwe": "918", "capec": "1000/225/115/664", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -6785,7 +6928,8 @@ "category": "attack_attempt", "cwe": "918", "capec": "1000/225/115/664", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6828,7 +6972,8 @@ "category": "attack_attempt", "cwe": "918", "capec": "1000/225/115/664", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -6870,7 +7015,8 @@ "category": "attack_attempt", "cwe": "94", "capec": "1000/152/242", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6916,7 +7062,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Joomla exploitation tool", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6945,7 +7092,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nessus", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -6974,7 +7122,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Arachni", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7003,7 +7152,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Jorgee", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7032,7 +7182,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Probely", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -7061,7 +7212,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Metis", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7090,7 +7242,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "SQLPowerInjector", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7119,7 +7272,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "N-Stealth", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7148,7 +7302,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Brutus", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7176,7 +7331,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7205,7 +7361,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Netsparker", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -7234,7 +7391,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "JAASCois", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7263,7 +7421,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nsauditor", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7292,7 +7451,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Paros", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7321,7 +7481,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "DirBuster", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7350,7 +7511,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Pangolin", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7379,7 +7541,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Qualys", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -7408,7 +7571,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "SQLNinja", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7437,7 +7601,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nikto", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7466,7 +7631,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "BlackWidow", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7495,7 +7661,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Grendel-Scan", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7524,7 +7691,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Havij", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7553,7 +7721,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "w3af", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7582,7 +7751,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nmap", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7611,7 +7781,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nessus", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7640,7 +7811,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "EvilScanner", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7669,7 +7841,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "WebFuck", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7698,7 +7871,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "OpenVAS", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7727,7 +7901,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Spider-Pig", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7756,7 +7931,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Zgrab", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7785,7 +7961,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Zmeu", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7814,7 +7991,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "GoogleSecurityScanner", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -7843,7 +8021,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Commix", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7872,7 +8051,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Gobuster", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7901,7 +8081,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "CGIchk", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7930,7 +8111,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "FFUF", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7959,7 +8141,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nuclei", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -7988,7 +8171,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Tsunami", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8017,7 +8201,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Nimbostratus", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8046,7 +8231,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Datadog Canary Test", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8081,7 +8267,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Datadog Canary Test", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8119,7 +8306,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "AlertLogic", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -8148,7 +8336,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "wfuzz", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8177,7 +8366,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Detectify", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -8206,7 +8396,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "BSQLBF", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8235,7 +8426,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "masscan", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8264,7 +8456,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "WPScan", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8293,7 +8486,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Aon", - "confidence": "0" + "confidence": "0", + "module": "waf" }, "conditions": [ { @@ -8322,7 +8516,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "feroxbuster", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8350,7 +8545,8 @@ "category": "attack_attempt", "cwe": "200", "capec": "1000/118/169", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8382,7 +8578,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "SQLmap", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { @@ -8411,7 +8608,8 @@ "cwe": "200", "capec": "1000/118/169", "tool_name": "Skipfish", - "confidence": "1" + "confidence": "1", + "module": "waf" }, "conditions": [ { diff --git a/packages/dd-trace/src/appsec/remote_config/capabilities.js b/packages/dd-trace/src/appsec/remote_config/capabilities.js index 18c11a92104..16034f5f9ee 100644 --- a/packages/dd-trace/src/appsec/remote_config/capabilities.js +++ b/packages/dd-trace/src/appsec/remote_config/capabilities.js @@ -11,7 +11,7 @@ module.exports = { ASM_CUSTOM_RULES: 1n << 8n, ASM_CUSTOM_BLOCKING_RESPONSE: 1n << 9n, ASM_TRUSTED_IPS: 1n << 10n, - ASM_API_SECURITY_SAMPLE_RATE: 1n << 11n, + ASM_API_SECURITY_SAMPLE_RATE: 1n << 11n, // deprecated APM_TRACING_SAMPLE_RATE: 1n << 12n, APM_TRACING_LOGS_INJECTION: 1n << 13n, APM_TRACING_HTTP_HEADER_TAGS: 1n << 14n, @@ -22,6 +22,7 @@ module.exports = { ASM_RASP_SSRF: 1n << 23n, ASM_RASP_SHI: 1n << 24n, APM_TRACING_SAMPLE_RULES: 1n << 29n, + ASM_AUTO_USER_INSTRUM_MODE: 1n << 31n, ASM_ENDPOINT_FINGERPRINT: 1n << 32n, ASM_NETWORK_FINGERPRINT: 1n << 34n, ASM_HEADER_FINGERPRINT: 1n << 35n diff --git a/packages/dd-trace/src/appsec/remote_config/index.js b/packages/dd-trace/src/appsec/remote_config/index.js index 9f0869351af..7884175abb0 100644 --- a/packages/dd-trace/src/appsec/remote_config/index.js +++ b/packages/dd-trace/src/appsec/remote_config/index.js @@ -4,7 +4,8 @@ const Activation = require('../activation') const RemoteConfigManager = require('./manager') const RemoteConfigCapabilities = require('./capabilities') -const apiSecuritySampler = require('../api_security_sampler') +const { setCollectionMode } = require('../user_tracking') +const log = require('../../log') let rc @@ -24,18 +25,34 @@ function enable (config, appsec) { rc.updateCapabilities(RemoteConfigCapabilities.ASM_ACTIVATION, true) } - if (config.appsec.apiSecurity?.enabled) { - rc.updateCapabilities(RemoteConfigCapabilities.ASM_API_SECURITY_SAMPLE_RATE, true) - } + rc.updateCapabilities(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + + let autoUserInstrumModeId - rc.setProductHandler('ASM_FEATURES', (action, rcConfig) => { + 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) } - - apiSecuritySampler.setRequestSampling(rcConfig.api_security?.request_sample_rate) }) } diff --git a/packages/dd-trace/src/appsec/remote_config/manager.js b/packages/dd-trace/src/appsec/remote_config/manager.js index 8f2aa44cea2..75c72690503 100644 --- a/packages/dd-trace/src/appsec/remote_config/manager.js +++ b/packages/dd-trace/src/appsec/remote_config/manager.js @@ -134,7 +134,7 @@ class RemoteConfigManager extends EventEmitter { if (statusCode === 404) return cb() if (err) { - log.error(err) + log.error('[RC] Error in request', err) return cb() } @@ -148,7 +148,7 @@ class RemoteConfigManager extends EventEmitter { try { this.parseConfig(JSON.parse(data)) } catch (err) { - log.error(`Could not parse remote config response: ${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() diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index be038279dc8..57519e5bc79 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -148,7 +148,9 @@ function reportAttack (attackData) { newTags['_dd.appsec.json'] = '{"triggers":' + attackData + '}' } - newTags['network.client.ip'] = req.socket.remoteAddress + if (req.socket) { + newTags['network.client.ip'] = req.socket.remoteAddress + } rootSpan.addTags(newTags) } diff --git a/packages/dd-trace/src/appsec/sdk/set_user.js b/packages/dd-trace/src/appsec/sdk/set_user.js index 81b0e3ec7ad..6efe44ebd41 100644 --- a/packages/dd-trace/src/appsec/sdk/set_user.js +++ b/packages/dd-trace/src/appsec/sdk/set_user.js @@ -11,13 +11,13 @@ function setUserTags (user, rootSpan) { function setUser (tracer, user) { if (!user || !user.id) { - log.warn('Invalid user provided to setUser') + log.warn('[ASM] Invalid user provided to setUser') return } const rootSpan = getRootSpan(tracer) if (!rootSpan) { - log.warn('Root span not available in setUser') + log.warn('[ASM] Root span not available in setUser') return } diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index e95081314de..a04f596bbc3 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -7,67 +7,68 @@ const standalone = require('../standalone') const waf = require('../waf') const { SAMPLING_MECHANISM_APPSEC } = require('../../constants') const { keepTrace } = require('../../priority_sampler') +const addresses = require('../addresses') function trackUserLoginSuccessEvent (tracer, user, metadata) { // TODO: better user check here and in _setUser() ? if (!user || !user.id) { - log.warn('Invalid user provided to trackUserLoginSuccessEvent') + log.warn('[ASM] Invalid user provided to trackUserLoginSuccessEvent') return } const rootSpan = getRootSpan(tracer) if (!rootSpan) { - log.warn('Root span not available in trackUserLoginSuccessEvent') + log.warn('[ASM] Root span not available in trackUserLoginSuccessEvent') return } setUserTags(user, rootSpan) - trackEvent('users.login.success', metadata, 'trackUserLoginSuccessEvent', rootSpan, 'sdk') + const login = user.login ?? user.id + + metadata = { 'usr.login': login, ...metadata } + + trackEvent('users.login.success', metadata, 'trackUserLoginSuccessEvent', rootSpan) + + runWaf('users.login.success', { id: user.id, login }) } function trackUserLoginFailureEvent (tracer, userId, exists, metadata) { if (!userId || typeof userId !== 'string') { - log.warn('Invalid userId provided to trackUserLoginFailureEvent') + log.warn('[ASM] Invalid userId provided to trackUserLoginFailureEvent') return } const fields = { 'usr.id': userId, + 'usr.login': userId, 'usr.exists': exists ? 'true' : 'false', ...metadata } - trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer), 'sdk') + trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer)) + + runWaf('users.login.failure', { login: userId }) } function trackCustomEvent (tracer, eventName, metadata) { if (!eventName || typeof eventName !== 'string') { - log.warn('Invalid eventName provided to trackCustomEvent') + log.warn('[ASM] Invalid eventName provided to trackCustomEvent') return } - trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer), 'sdk') + trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer)) } -function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) { +function trackEvent (eventName, fields, sdkMethodName, rootSpan) { if (!rootSpan) { - log.warn(`Root span not available in ${sdkMethodName}`) + log.warn('[ASM] Root span not available in %s', sdkMethodName) return } - keepTrace(rootSpan, SAMPLING_MECHANISM_APPSEC) - const tags = { - [`appsec.events.${eventName}.track`]: 'true' - } - - if (mode === 'sdk') { - tags[`_dd.appsec.events.${eventName}.sdk`] = 'true' - } - - if (mode === 'safe' || mode === 'extended') { - tags[`_dd.appsec.events.${eventName}.auto.mode`] = mode + [`appsec.events.${eventName}.track`]: 'true', + [`_dd.appsec.events.${eventName}.sdk`]: 'true' } if (fields) { @@ -78,16 +79,28 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) { rootSpan.addTags(tags) + keepTrace(rootSpan, SAMPLING_MECHANISM_APPSEC) standalone.sample(rootSpan) +} + +function runWaf (eventName, user) { + const persistent = { + [`server.business_logic.${eventName}`]: null + } + + if (user.id) { + persistent[addresses.USER_ID] = '' + user.id + } - if (['users.login.success', 'users.login.failure'].includes(eventName)) { - waf.run({ persistent: { [`server.business_logic.${eventName}`]: null } }) + if (user.login) { + persistent[addresses.USER_LOGIN] = '' + user.login } + + waf.run({ persistent }) } module.exports = { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, - trackCustomEvent, - trackEvent + trackCustomEvent } diff --git a/packages/dd-trace/src/appsec/sdk/user_blocking.js b/packages/dd-trace/src/appsec/sdk/user_blocking.js index 19997d3ff9c..8af54ccbec1 100644 --- a/packages/dd-trace/src/appsec/sdk/user_blocking.js +++ b/packages/dd-trace/src/appsec/sdk/user_blocking.js @@ -15,7 +15,7 @@ function isUserBlocked (user) { function checkUserAndSetUser (tracer, user) { if (!user || !user.id) { - log.warn('Invalid user provided to isUserBlocked') + log.warn('[ASM] Invalid user provided to isUserBlocked') return false } @@ -25,7 +25,7 @@ function checkUserAndSetUser (tracer, user) { setUserTags(user, rootSpan) } } else { - log.warn('Root span not available in isUserBlocked') + log.warn('[ASM] Root span not available in isUserBlocked') } return isUserBlocked(user) @@ -41,13 +41,13 @@ function blockRequest (tracer, req, res) { } if (!req || !res) { - log.warn('Requests or response object not available in blockRequest') + log.warn('[ASM] Requests or response object not available in blockRequest') return false } const rootSpan = getRootSpan(tracer) if (!rootSpan) { - log.warn('Root span not available in blockRequest') + log.warn('[ASM] Root span not available in blockRequest') return false } diff --git a/packages/dd-trace/src/appsec/sdk/utils.js b/packages/dd-trace/src/appsec/sdk/utils.js index 542a6311f5b..b59bf08d22a 100644 --- a/packages/dd-trace/src/appsec/sdk/utils.js +++ b/packages/dd-trace/src/appsec/sdk/utils.js @@ -1,8 +1,27 @@ 'use strict' function getRootSpan (tracer) { - const span = tracer.scope().active() - return span && span.context()._trace.started[0] + let span = tracer.scope().active() + if (!span) return + + const context = span.context() + const started = context._trace.started + + let parentId = context._parentId + while (parentId) { + const parent = started.find(s => s.context()._spanId === parentId) + const pContext = parent?.context() + + if (!pContext) break + + parentId = pContext._parentId + + if (!pContext._tags?._inferred_span) { + span = parent + } + } + + return span } module.exports = { diff --git a/packages/dd-trace/src/appsec/telemetry.js b/packages/dd-trace/src/appsec/telemetry.js index d96ca77601f..8e9a2518f80 100644 --- a/packages/dd-trace/src/appsec/telemetry.js +++ b/packages/dd-trace/src/appsec/telemetry.js @@ -172,6 +172,15 @@ function addRaspRequestMetrics (store, { duration, durationExt }) { store[DD_TELEMETRY_REQUEST_METRICS].raspEvalCount++ } +function incrementMissingUserLoginMetric (framework, eventType) { + if (!enabled) return + + appsecMetrics.count('instrum.user_auth.missing_user_login', { + framework, + event_type: eventType + }).inc() +} + function getRequestMetrics (req) { if (req) { const store = getStore(req) @@ -188,6 +197,7 @@ module.exports = { incrementWafInitMetric, incrementWafUpdatesMetric, incrementWafRequestsMetric, + incrementMissingUserLoginMetric, getRequestMetrics } diff --git a/packages/dd-trace/src/appsec/user_tracking.js b/packages/dd-trace/src/appsec/user_tracking.js new file mode 100644 index 00000000000..5b92f80d642 --- /dev/null +++ b/packages/dd-trace/src/appsec/user_tracking.js @@ -0,0 +1,168 @@ +'use strict' + +const crypto = require('crypto') +const log = require('../log') +const telemetry = require('./telemetry') +const addresses = require('./addresses') +const { keepTrace } = require('../priority_sampler') +const { SAMPLING_MECHANISM_APPSEC } = require('../constants') +const standalone = require('./standalone') +const waf = require('./waf') + +// the RFC doesn't include '_id', but it's common in MongoDB +const USER_ID_FIELDS = ['id', '_id', 'email', 'username', 'login', 'user'] + +let collectionMode + +function setCollectionMode (mode, overwrite = true) { + // don't overwrite if already set, only used in appsec/index.js to not overwrite RC values + if (!overwrite && collectionMode) return + + /* eslint-disable no-fallthrough */ + switch (mode) { + case 'safe': + log.warn('[ASM] Using deprecated value "safe" in config.appsec.eventTracking.mode') + case 'anon': + case 'anonymization': + collectionMode = 'anonymization' + break + + case 'extended': + log.warn('[ASM] Using deprecated value "extended" in config.appsec.eventTracking.mode') + case 'ident': + case 'identification': + collectionMode = 'identification' + break + + default: + collectionMode = 'disabled' + } + /* eslint-enable no-fallthrough */ +} + +function obfuscateIfNeeded (str) { + if (collectionMode === 'anonymization') { + // get first 16 bytes of sha256 hash in lowercase hex + return 'anon_' + crypto.createHash('sha256').update(str).digest().toString('hex', 0, 16).toLowerCase() + } else { + return str + } +} + +// TODO: should we find other ways to get the user ID ? +function getUserId (user) { + if (!user) return + + for (const field of USER_ID_FIELDS) { + let id = user[field] + + // try to find a field that can be stringified + if (id && typeof id.toString === 'function') { + id = id.toString() + + if (typeof id !== 'string' || id.startsWith('[object ')) { + // probably not a usable ID ? + continue + } + + return obfuscateIfNeeded(id) + } + } +} + +function trackLogin (framework, login, user, success, rootSpan) { + if (!collectionMode || collectionMode === 'disabled') return + + if (!rootSpan) { + log.error('[ASM] No rootSpan found in AppSec trackLogin') + return + } + + if (typeof login !== 'string') { + log.error('[ASM] Invalid login provided to AppSec trackLogin') + + telemetry.incrementMissingUserLoginMetric(framework, success ? 'login_success' : 'login_failure') + // note: + // if we start supporting using userId if login is missing, we need to only give up if both are missing, and + // implement 'appsec.instrum.user_auth.missing_user_id' telemetry too + return + } + + login = obfuscateIfNeeded(login) + const userId = getUserId(user) + + let newTags + + const persistent = { + [addresses.USER_LOGIN]: login + } + + const currentTags = rootSpan.context()._tags + const isSdkCalled = currentTags[`_dd.appsec.events.users.login.${success ? 'success' : 'failure'}.sdk`] === 'true' + + // used to not overwrite tags set by SDK + function shouldSetTag (tag) { + return !(isSdkCalled && currentTags[tag]) + } + + if (success) { + newTags = { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': collectionMode, + '_dd.appsec.usr.login': login + } + + if (shouldSetTag('appsec.events.users.login.success.usr.login')) { + newTags['appsec.events.users.login.success.usr.login'] = login + } + + if (userId) { + newTags['_dd.appsec.usr.id'] = userId + + if (shouldSetTag('usr.id')) { + newTags['usr.id'] = userId + persistent[addresses.USER_ID] = userId + } + } + + persistent[addresses.LOGIN_SUCCESS] = null + } else { + newTags = { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': collectionMode, + '_dd.appsec.usr.login': login + } + + if (shouldSetTag('appsec.events.users.login.failure.usr.login')) { + newTags['appsec.events.users.login.failure.usr.login'] = login + } + + if (userId) { + newTags['_dd.appsec.usr.id'] = userId + + if (shouldSetTag('appsec.events.users.login.failure.usr.id')) { + newTags['appsec.events.users.login.failure.usr.id'] = userId + } + } + + /* TODO: if one day we have this info + if (exists != null && shouldSetTag('appsec.events.users.login.failure.usr.exists')) { + newTags['appsec.events.users.login.failure.usr.exists'] = exists + } + */ + + persistent[addresses.LOGIN_FAILURE] = null + } + + keepTrace(rootSpan, SAMPLING_MECHANISM_APPSEC) + standalone.sample(rootSpan) + + rootSpan.addTags(newTags) + + return waf.run({ persistent }) +} + +module.exports = { + setCollectionMode, + trackLogin +} diff --git a/packages/dd-trace/src/appsec/waf/index.js b/packages/dd-trace/src/appsec/waf/index.js index 8aa30fabbb4..3b2bc9e2a13 100644 --- a/packages/dd-trace/src/appsec/waf/index.js +++ b/packages/dd-trace/src/appsec/waf/index.js @@ -41,7 +41,7 @@ function update (newRules) { try { waf.wafManager.update(newRules) } catch (err) { - log.error('Could not apply rules from remote config') + log.error('[ASM] Could not apply rules from remote config') throw err } } @@ -50,7 +50,7 @@ function run (data, req, raspRuleType) { if (!req) { const store = storage.getStore() if (!store || !store.req) { - log.warn('Request object not available in waf.run') + log.warn('[ASM] Request object not available in waf.run') return } diff --git a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js index a2dae737a86..6a90b8f89bb 100644 --- a/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +++ b/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js @@ -23,7 +23,7 @@ class WAFContextWrapper { run ({ persistent, ephemeral }, raspRuleType) { if (this.ddwafContext.disposed) { - log.warn('Calling run on a disposed context') + log.warn('[ASM] Calling run on a disposed context') return } @@ -101,8 +101,7 @@ class WAFContextWrapper { return result.actions } catch (err) { - log.error('Error while running the AppSec WAF') - log.error(err) + log.error('[ASM] Error while running the AppSec WAF', err) } } diff --git a/packages/dd-trace/src/appsec/waf/waf_manager.js b/packages/dd-trace/src/appsec/waf/waf_manager.js index b3cc91e6104..520438d8a20 100644 --- a/packages/dd-trace/src/appsec/waf/waf_manager.js +++ b/packages/dd-trace/src/appsec/waf/waf_manager.js @@ -25,7 +25,7 @@ class WAFManager { const { obfuscatorKeyRegex, obfuscatorValueRegex } = this.config return new DDWAF(rules, { obfuscatorKeyRegex, obfuscatorValueRegex }) } catch (err) { - log.error('AppSec could not load native package. In-app WAF features will not be available.') + log.error('[ASM] AppSec could not load native package. In-app WAF features will not be available.') throw err } diff --git a/packages/dd-trace/src/azure_metadata.js b/packages/dd-trace/src/azure_metadata.js index 94c29c9dd16..6895f28b479 100644 --- a/packages/dd-trace/src/azure_metadata.js +++ b/packages/dd-trace/src/azure_metadata.js @@ -1,6 +1,6 @@ 'use strict' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len // Modeled after https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/ddcommon/src/azure_app_services.rs const os = require('os') @@ -79,7 +79,7 @@ function buildMetadata () { function getAzureAppMetadata () { // DD_AZURE_APP_SERVICES is an environment variable introduced by the .NET APM team and is set automatically for // anyone using the Datadog APM Extensions (.NET, Java, or Node) for Windows Azure App Services - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len // See: https://github.com/DataDog/datadog-aas-extension/blob/01f94b5c28b7fa7a9ab264ca28bd4e03be603900/node/src/applicationHost.xdt#L20-L21 return process.env.DD_AZURE_APP_SERVICES !== undefined ? buildMetadata() : undefined } @@ -88,9 +88,9 @@ function getAzureFunctionMetadata () { return getIsAzureFunction() ? buildMetadata() : undefined } -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len // Modeled after https://github.com/DataDog/libdatadog/blob/92272e90a7919f07178f3246ef8f82295513cfed/profiling/src/exporter/mod.rs#L187 -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len // and https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/trace-utils/src/trace_utils.rs#L533 function getAzureTagsFromMetadata (metadata) { if (metadata === undefined) { diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index 97323d02407..ec6e2a1fd75 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -73,8 +73,7 @@ class TestVisDynamicInstrumentation { // Allow the parent to exit even if the worker is still running this.worker.unref() - this.breakpointSetChannel.port2.on('message', (message) => { - const { probeId } = message + this.breakpointSetChannel.port2.on('message', ({ probeId }) => { const resolve = probeIdToResolveBreakpointSet.get(probeId) if (resolve) { resolve() @@ -82,8 +81,7 @@ class TestVisDynamicInstrumentation { } }).unref() - this.breakpointHitChannel.port2.on('message', (message) => { - const { snapshot } = message + this.breakpointHitChannel.port2.on('message', ({ snapshot }) => { const { probe: { id: probeId } } = snapshot const resolve = probeIdToResolveBreakpointHit.get(probeId) if (resolve) { @@ -91,6 +89,9 @@ class TestVisDynamicInstrumentation { probeIdToResolveBreakpointHit.delete(probeId) } }).unref() + + this.worker.on('error', (err) => log.error('ci-visibility DI worker error', err)) + this.worker.on('messageerror', (err) => log.error('ci-visibility DI worker messageerror', err)) } } 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 4bef76e6343..952ba1a7cf7 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 @@ -1,6 +1,8 @@ 'use strict' - +const sourceMap = require('source-map') +const path = require('path') const { workerData: { breakpointSetChannel, breakpointHitChannel } } = require('worker_threads') + // TODO: move debugger/devtools_client/session to common place const session = require('../../../debugger/devtools_client/session') // TODO: move debugger/devtools_client/snapshot to common place @@ -69,14 +71,24 @@ async function addBreakpoint (snapshotId, probe) { const script = findScriptFromPartialPath(file) if (!script) throw new Error(`No loaded script found for ${file}`) - const [path, scriptId] = script + const [path, scriptId, sourceMapURL] = script log.debug(`Adding breakpoint at ${path}:${line}`) + let lineNumber = line + + if (sourceMapURL && sourceMapURL.startsWith('data:')) { + try { + lineNumber = await processScriptWithInlineSourceMap({ file, line, sourceMapURL }) + } catch (err) { + log.error(err) + } + } + const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { scriptId, - lineNumber: line - 1 + lineNumber: lineNumber - 1 } }) @@ -88,3 +100,27 @@ function start () { sessionStarted = true return session.post('Debugger.enable') // return instead of await to reduce number of promises created } + +async function processScriptWithInlineSourceMap (params) { + const { file, line, sourceMapURL } = params + + // Extract the base64-encoded source map + const base64SourceMap = sourceMapURL.split('base64,')[1] + + // Decode the base64 source map + const decodedSourceMap = Buffer.from(base64SourceMap, 'base64').toString('utf8') + + // Parse the source map + const consumer = await new sourceMap.SourceMapConsumer(decodedSourceMap) + + // Map to the generated position + const generatedPosition = consumer.generatedPositionFor({ + source: path.basename(file), // this needs to be the file, not the filepath + line, + column: 0 + }) + + consumer.destroy() + + return generatedPosition.line +} diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js index 98eff61a6fd..a36b07201e1 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js @@ -63,7 +63,7 @@ class Writer extends BaseWriter { TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, { endpoint: 'code_coverage' } ) - log.error(err) + log.error('Error sending CI coverage payload', err) done() return } diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js index eebc3c5e6a9..7d8c5ba47a0 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js @@ -40,7 +40,7 @@ class DynamicInstrumentationLogsWriter extends BaseWriter { request(data, options, (err, res) => { if (err) { - log.error(err) + log.error('Error sending DI logs payload', err) done() return } 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 5895bb573cd..a5b677ef98b 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js @@ -38,7 +38,7 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter { apiUrl = new URL(apiUrl) this._apiUrl = apiUrl } catch (e) { - log.error(e) + log.error('Error setting CI exporter api url', e) } } diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js index 466c5230b22..34cad3862bc 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js @@ -64,7 +64,7 @@ class Writer extends BaseWriter { TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, { endpoint: 'test_cycle' } ) - log.error(err) + log.error('Error sending CI agentless payload', err) done() return } 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 f555603e0cb..3ad1a11e027 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 @@ -73,6 +73,9 @@ class CiVisibilityExporter extends AgentInfoExporter { if (this._coverageWriter) { this._coverageWriter.flush() } + if (this._logsWriter) { + this._logsWriter.flush() + } }) } @@ -193,7 +196,8 @@ class CiVisibilityExporter extends AgentInfoExporter { isEarlyFlakeDetectionEnabled, earlyFlakeDetectionNumRetries, earlyFlakeDetectionFaultyThreshold, - isFlakyTestRetriesEnabled + isFlakyTestRetriesEnabled, + isDiEnabled } = remoteConfiguration return { isCodeCoverageEnabled, @@ -204,7 +208,8 @@ class CiVisibilityExporter extends AgentInfoExporter { earlyFlakeDetectionNumRetries, earlyFlakeDetectionFaultyThreshold, isFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled && this._config.isFlakyTestRetriesEnabled, - flakyTestRetriesCount: this._config.flakyTestRetriesCount + flakyTestRetriesCount: this._config.flakyTestRetriesCount, + isDiEnabled: isDiEnabled && this._config.isTestDynamicInstrumentationEnabled } } @@ -222,7 +227,7 @@ class CiVisibilityExporter extends AgentInfoExporter { repositoryUrl, (err) => { if (err) { - log.error(`Error uploading git metadata: ${err.message}`) + log.error('Error uploading git metadata: %s', err.message) } else { log.debug('Successfully uploaded git metadata') } @@ -302,13 +307,28 @@ class CiVisibilityExporter extends AgentInfoExporter { if (!this._isInitialized) { return done() } - this._writer.flush(() => { - if (this._coverageWriter) { - this._coverageWriter.flush(done) - } else { + + // TODO: safe to do them at once? Or do we want to do them one by one? + const writers = [ + this._writer, + this._coverageWriter, + this._logsWriter + ].filter(writer => writer) + + let remaining = writers.length + + if (remaining === 0) { + return done() + } + + const onFlushComplete = () => { + remaining -= 1 + if (remaining === 0) { done() } - }) + } + + writers.forEach(writer => writer.flush(onFlushComplete)) } exportUncodedCoverages () { @@ -327,7 +347,7 @@ class CiVisibilityExporter extends AgentInfoExporter { this._writer.setUrl(url) this._coverageWriter.setUrl(coverageUrl) } catch (e) { - log.error(e) + log.error('Error setting CI exporter url', e) } } diff --git a/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js b/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js index 9a32efad05e..e39770dea82 100644 --- a/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +++ b/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js @@ -92,7 +92,8 @@ function getLibraryConfiguration ({ itr_enabled: isItrEnabled, require_git: requireGit, early_flake_detection: earlyFlakeDetectionConfig, - flaky_test_retries_enabled: isFlakyTestRetriesEnabled + flaky_test_retries_enabled: isFlakyTestRetriesEnabled, + di_enabled: isDiEnabled } } } = JSON.parse(res) @@ -107,7 +108,8 @@ function getLibraryConfiguration ({ earlyFlakeDetectionConfig?.slow_test_retries?.['5s'] || DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES, earlyFlakeDetectionFaultyThreshold: earlyFlakeDetectionConfig?.faulty_session_threshold ?? DEFAULT_EARLY_FLAKE_DETECTION_ERROR_THRESHOLD, - isFlakyTestRetriesEnabled + isFlakyTestRetriesEnabled, + isDiEnabled: isDiEnabled && isFlakyTestRetriesEnabled } log.debug(() => `Remote settings: ${JSON.stringify(settings)}`) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index c50c05f794a..a46cc3153fc 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -132,11 +132,11 @@ function checkIfBothOtelAndDdEnvVarSet () { const fromEntries = Object.fromEntries || (entries => entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {})) -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const defaultWafObfuscatorKeyRegex = '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\\.net[_-]sessionid|sid|jwt' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const defaultWafObfuscatorValueRegex = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\\.net(?:[_-]|-)sessionid|sid|jwt)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}' const runtimeId = uuid() @@ -145,7 +145,7 @@ function maybeFile (filepath) { try { return fs.readFileSync(filepath, 'utf8') } catch (e) { - log.error(e) + log.error('Error reading file %s', filepath, e) return undefined } } @@ -378,7 +378,7 @@ class Config { } catch (e) { // Only log error if the user has set a git.properties path if (process.env.DD_GIT_PROPERTIES_FILE) { - log.error(e) + log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', DD_GIT_PROPERTIES_FILE, e) } } if (gitPropertiesString) { @@ -444,13 +444,12 @@ class Config { const defaults = setHiddenProperty(this, '_defaults', {}) this._setValue(defaults, 'appsec.apiSecurity.enabled', true) - this._setValue(defaults, 'appsec.apiSecurity.requestSampling', 0.1) + this._setValue(defaults, 'appsec.apiSecurity.sampleDelay', 30) this._setValue(defaults, 'appsec.blockedTemplateGraphql', undefined) this._setValue(defaults, 'appsec.blockedTemplateHtml', undefined) this._setValue(defaults, 'appsec.blockedTemplateJson', undefined) this._setValue(defaults, 'appsec.enabled', undefined) - this._setValue(defaults, 'appsec.eventTracking.enabled', true) - this._setValue(defaults, 'appsec.eventTracking.mode', 'safe') + this._setValue(defaults, 'appsec.eventTracking.mode', 'identification') this._setValue(defaults, 'appsec.obfuscatorKeyRegex', defaultWafObfuscatorKeyRegex) this._setValue(defaults, 'appsec.obfuscatorValueRegex', defaultWafObfuscatorValueRegex) this._setValue(defaults, 'appsec.rasp.enabled', true) @@ -467,7 +466,7 @@ class Config { this._setValue(defaults, 'ciVisibilityTestSessionName', '') this._setValue(defaults, 'clientIpEnabled', false) this._setValue(defaults, 'clientIpHeader', null) - this._setValue(defaults, 'crashtracking.enabled', false) + this._setValue(defaults, 'crashtracking.enabled', true) this._setValue(defaults, 'codeOriginForSpans.enabled', false) this._setValue(defaults, 'dbmPropagationMode', 'disabled') this._setValue(defaults, 'dogstatsd.hostname', '127.0.0.1') @@ -486,6 +485,7 @@ class Config { this._setValue(defaults, 'headerTags', []) this._setValue(defaults, 'hostname', '127.0.0.1') this._setValue(defaults, 'iast.cookieFilterPattern', '.{32,}') + this._setValue(defaults, 'iast.dbRowsToTaint', 1) this._setValue(defaults, 'iast.deduplicationEnabled', true) this._setValue(defaults, 'iast.enabled', false) this._setValue(defaults, 'iast.maxConcurrentRequests', 2) @@ -505,6 +505,8 @@ class Config { this._setValue(defaults, 'isGitUploadEnabled', false) this._setValue(defaults, 'isIntelligentTestRunnerEnabled', false) this._setValue(defaults, 'isManualApiEnabled', false) + this._setValue(defaults, 'langchain.spanCharLimit', 128) + this._setValue(defaults, 'langchain.spanPromptCompletionSampleRate', 1.0) this._setValue(defaults, 'llmobs.agentlessEnabled', false) this._setValue(defaults, 'llmobs.enabled', false) this._setValue(defaults, 'llmobs.mlApp', undefined) @@ -551,7 +553,7 @@ class Config { this._setValue(defaults, 'telemetry.dependencyCollection', true) this._setValue(defaults, 'telemetry.enabled', true) this._setValue(defaults, 'telemetry.heartbeatInterval', 60000) - this._setValue(defaults, 'telemetry.logCollection', false) + this._setValue(defaults, 'telemetry.logCollection', true) this._setValue(defaults, 'telemetry.metrics', true) this._setValue(defaults, 'traceEnabled', true) this._setValue(defaults, 'traceId128BitGenerationEnabled', true) @@ -571,7 +573,8 @@ class Config { AWS_LAMBDA_FUNCTION_NAME, DD_AGENT_HOST, DD_API_SECURITY_ENABLED, - DD_API_SECURITY_REQUEST_SAMPLE_RATE, + DD_API_SECURITY_SAMPLE_DELAY, + DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE, DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING, DD_APPSEC_ENABLED, DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON, @@ -592,6 +595,7 @@ class Config { DD_DATA_STREAMS_ENABLED, DD_DBM_PROPAGATION_MODE, DD_DOGSTATSD_HOSTNAME, + DD_DOGSTATSD_HOST, DD_DOGSTATSD_PORT, DD_DYNAMIC_INSTRUMENTATION_ENABLED, DD_ENV, @@ -602,6 +606,7 @@ class Config { DD_GRPC_SERVER_ERROR_STATUSES, JEST_WORKER_ID, DD_IAST_COOKIE_FILTER_PATTERN, + DD_IAST_DB_ROWS_TO_TAINT, DD_IAST_DEDUPLICATION_ENABLED, DD_IAST_ENABLED, DD_IAST_MAX_CONCURRENT_REQUESTS, @@ -615,6 +620,8 @@ class Config { DD_INSTRUMENTATION_TELEMETRY_ENABLED, DD_INSTRUMENTATION_CONFIG_ID, DD_LOGS_INJECTION, + DD_LANGCHAIN_SPAN_CHAR_LIMIT, + DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE, DD_LLMOBS_AGENTLESS_ENABLED, DD_LLMOBS_ENABLED, DD_LLMOBS_ML_APP, @@ -700,18 +707,17 @@ class Config { DD_API_SECURITY_ENABLED && isTrue(DD_API_SECURITY_ENABLED), DD_EXPERIMENTAL_API_SECURITY_ENABLED && isTrue(DD_EXPERIMENTAL_API_SECURITY_ENABLED) )) - this._setUnit(env, 'appsec.apiSecurity.requestSampling', DD_API_SECURITY_REQUEST_SAMPLE_RATE) + this._setValue(env, 'appsec.apiSecurity.sampleDelay', maybeFloat(DD_API_SECURITY_SAMPLE_DELAY)) this._setValue(env, 'appsec.blockedTemplateGraphql', maybeFile(DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON)) this._setValue(env, 'appsec.blockedTemplateHtml', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML)) this._envUnprocessed['appsec.blockedTemplateHtml'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML this._setValue(env, 'appsec.blockedTemplateJson', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON)) this._envUnprocessed['appsec.blockedTemplateJson'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED) - if (DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING) { - this._setValue(env, 'appsec.eventTracking.enabled', - ['extended', 'safe'].includes(DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING.toLowerCase())) - this._setValue(env, 'appsec.eventTracking.mode', DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING.toLowerCase()) - } + this._setString(env, 'appsec.eventTracking.mode', coalesce( + DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE, + DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING // TODO: remove in next major + )) this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP) this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP) this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED) @@ -735,7 +741,7 @@ class Config { this._setBoolean(env, 'crashtracking.enabled', DD_CRASHTRACKING_ENABLED) this._setBoolean(env, 'codeOriginForSpans.enabled', DD_CODE_ORIGIN_FOR_SPANS_ENABLED) this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE) - this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME) + this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOST || DD_DOGSTATSD_HOSTNAME) this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT) this._setBoolean(env, 'dsmEnabled', DD_DATA_STREAMS_ENABLED) this._setBoolean(env, 'dynamicInstrumentationEnabled', DD_DYNAMIC_INSTRUMENTATION_ENABLED) @@ -753,6 +759,7 @@ class Config { this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS) this._setString(env, 'hostname', coalesce(DD_AGENT_HOST, DD_TRACE_AGENT_HOSTNAME)) this._setString(env, 'iast.cookieFilterPattern', DD_IAST_COOKIE_FILTER_PATTERN) + this._setValue(env, 'iast.dbRowsToTaint', maybeInt(DD_IAST_DB_ROWS_TO_TAINT)) this._setBoolean(env, 'iast.deduplicationEnabled', DD_IAST_DEDUPLICATION_ENABLED) this._setBoolean(env, 'iast.enabled', DD_IAST_ENABLED) this._setValue(env, 'iast.maxConcurrentRequests', maybeInt(DD_IAST_MAX_CONCURRENT_REQUESTS)) @@ -771,6 +778,10 @@ class Config { this._setArray(env, 'injectionEnabled', DD_INJECTION_ENABLED) this._setBoolean(env, 'isAzureFunction', getIsAzureFunction()) this._setBoolean(env, 'isGCPFunction', getIsGCPFunction()) + this._setValue(env, 'langchain.spanCharLimit', maybeInt(DD_LANGCHAIN_SPAN_CHAR_LIMIT)) + this._setValue( + env, 'langchain.spanPromptCompletionSampleRate', maybeFloat(DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE) + ) this._setBoolean(env, 'legacyBaggageEnabled', DD_TRACE_LEGACY_BAGGAGE_ENABLED) this._setBoolean(env, 'llmobs.agentlessEnabled', DD_LLMOBS_AGENTLESS_ENABLED) this._setBoolean(env, 'llmobs.enabled', DD_LLMOBS_ENABLED) @@ -880,19 +891,13 @@ class Config { tagger.add(tags, options.tags) this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec.apiSecurity?.enabled) - this._setUnit(opts, 'appsec.apiSecurity.requestSampling', options.appsec.apiSecurity?.requestSampling) this._setValue(opts, 'appsec.blockedTemplateGraphql', maybeFile(options.appsec.blockedTemplateGraphql)) this._setValue(opts, 'appsec.blockedTemplateHtml', maybeFile(options.appsec.blockedTemplateHtml)) this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec.blockedTemplateHtml this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec.blockedTemplateJson)) this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec.blockedTemplateJson this._setBoolean(opts, 'appsec.enabled', options.appsec.enabled) - let eventTracking = options.appsec.eventTracking?.mode - if (eventTracking) { - eventTracking = eventTracking.toLowerCase() - this._setValue(opts, 'appsec.eventTracking.enabled', ['extended', 'safe'].includes(eventTracking)) - this._setValue(opts, 'appsec.eventTracking.mode', eventTracking) - } + this._setString(opts, 'appsec.eventTracking.mode', options.appsec.eventTracking?.mode) this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex) this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex) this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec.rasp?.enabled) @@ -930,6 +935,7 @@ class Config { this._setArray(opts, 'headerTags', options.headerTags) this._setString(opts, 'hostname', options.hostname) this._setString(opts, 'iast.cookieFilterPattern', options.iast?.cookieFilterPattern) + this._setValue(opts, 'iast.dbRowsToTaint', maybeInt(options.iast?.dbRowsToTaint)) this._setBoolean(opts, 'iast.deduplicationEnabled', options.iast && options.iast.deduplicationEnabled) this._setBoolean(opts, 'iast.enabled', options.iast && (options.iast === true || options.iast.enabled === true)) @@ -1134,16 +1140,6 @@ class Config { calc['tracePropagationStyle.inject'] = calc['tracePropagationStyle.inject'] || defaultPropagationStyle calc['tracePropagationStyle.extract'] = calc['tracePropagationStyle.extract'] || defaultPropagationStyle } - - const iastEnabled = coalesce(this._options['iast.enabled'], this._env['iast.enabled']) - const profilingEnabled = coalesce(this._options['profiling.enabled'], this._env['profiling.enabled']) - const injectionIncludesProfiler = (this._env.injectionEnabled || []).includes('profiler') - if (iastEnabled || ['auto', 'true'].includes(profilingEnabled) || injectionIncludesProfiler) { - this._setBoolean(calc, 'telemetry.logCollection', true) - } - if (this._env.injectionEnabled?.length > 0) { - this._setBoolean(calc, 'crashtracking.enabled', true) - } } _applyRemote (options) { @@ -1281,7 +1277,7 @@ class Config { // TODO: Deeply merge configurations. // TODO: Move change tracking to telemetry. // for telemetry reporting, `name`s in `containers` need to be keys from: - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-payload/static/config_norm_rules.json _merge () { const containers = [this._remote, this._options, this._env, this._calculated, this._defaults] diff --git a/packages/dd-trace/src/constants.js b/packages/dd-trace/src/constants.js index a242f717a37..4e7faf669d4 100644 --- a/packages/dd-trace/src/constants.js +++ b/packages/dd-trace/src/constants.js @@ -46,5 +46,10 @@ module.exports = { SCHEMA_OPERATION: 'schema.operation', SCHEMA_NAME: 'schema.name', GRPC_CLIENT_ERROR_STATUSES: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], - GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + S3_PTR_KIND: 'aws.s3.object', + SPAN_POINTER_DIRECTION: Object.freeze({ + UPSTREAM: 'u', + DOWNSTREAM: 'd' + }) } diff --git a/packages/dd-trace/src/crashtracking/crashtracker.js b/packages/dd-trace/src/crashtracking/crashtracker.js index 0a35f0e0580..a2d3ec2eb52 100644 --- a/packages/dd-trace/src/crashtracking/crashtracker.js +++ b/packages/dd-trace/src/crashtracking/crashtracker.js @@ -20,7 +20,7 @@ class Crashtracker { binding.updateConfig(this._getConfig(config)) binding.updateMetadata(this._getMetadata(config)) } catch (e) { - log.error(e) + log.error('Error configuring crashtracker', e) } } @@ -36,7 +36,7 @@ class Crashtracker { this._getMetadata(config) ) } catch (e) { - log.error(e) + log.error('Error initialising crashtracker', e) } } @@ -53,14 +53,14 @@ class Crashtracker { // TODO: Use the string directly when deserialization is fixed. url: { scheme: url.protocol.slice(0, -1), - authority: url.protocol === 'unix' + authority: url.protocol === 'unix:' ? Buffer.from(url.pathname).toString('hex') : url.host, path_and_query: '' }, timeout_ms: 3000 }, - timeout_ms: 0, + timeout_ms: 5000, // TODO: Use `EnabledWithSymbolsInReceiver` instead for Linux when fixed. resolve_frames: 'EnabledWithInprocessSymbols' } @@ -79,6 +79,7 @@ class Crashtracker { 'language:javascript', `library_version:${pkg.version}`, 'runtime:nodejs', + `runtime_version:${process.versions.node}`, 'severity:crash' ] } diff --git a/packages/dd-trace/src/datastreams/processor.js b/packages/dd-trace/src/datastreams/processor.js index d036af805a7..d997ba098ae 100644 --- a/packages/dd-trace/src/datastreams/processor.js +++ b/packages/dd-trace/src/datastreams/processor.js @@ -1,7 +1,5 @@ const os = require('os') const pkg = require('../../../../package.json') -// Message pack int encoding is done in big endian, but data streams uses little endian -const Uint64 = require('int64-buffer').Uint64BE const { LogCollapsingLowestDenseDDSketch } = require('@datadog/sketches-js') const { DsmPathwayCodec } = require('./pathway') @@ -19,8 +17,8 @@ const HIGH_ACCURACY_DISTRIBUTION = 0.0075 class StatsPoint { constructor (hash, parentHash, edgeTags) { - this.hash = new Uint64(hash) - this.parentHash = new Uint64(parentHash) + this.hash = hash.readBigUInt64BE() + this.parentHash = parentHash.readBigUInt64BE() this.edgeTags = edgeTags this.edgeLatency = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION) this.pathwayLatency = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION) @@ -344,8 +342,8 @@ class DataStreamsProcessor { backlogs.push(backlog.encode()) } serializedBuckets.push({ - Start: new Uint64(timeNs), - Duration: new Uint64(this.bucketSizeNs), + Start: BigInt(timeNs), + Duration: BigInt(this.bucketSizeNs), Stats: points, Backlogs: backlogs }) diff --git a/packages/dd-trace/src/datastreams/writer.js b/packages/dd-trace/src/datastreams/writer.js index f8c9e021ecc..220b3dfecf7 100644 --- a/packages/dd-trace/src/datastreams/writer.js +++ b/packages/dd-trace/src/datastreams/writer.js @@ -2,9 +2,10 @@ const pkg = require('../../../../package.json') const log = require('../log') const request = require('../exporters/common/request') const { URL, format } = require('url') -const msgpack = require('msgpack-lite') +const { MsgpackEncoder } = require('../msgpack') const zlib = require('zlib') -const codec = msgpack.createCodec({ int64: true }) + +const msgpack = new MsgpackEncoder() function makeRequest (data, url, cb) { const options = { @@ -41,17 +42,17 @@ class DataStreamsWriter { log.debug(() => `Maximum number of active requests reached. Payload discarded: ${JSON.stringify(payload)}`) return } - const encodedPayload = msgpack.encode(payload, { codec }) + const encodedPayload = msgpack.encode(payload) zlib.gzip(encodedPayload, { level: 1 }, (err, compressedData) => { if (err) { - log.error(err) + log.error('Error zipping datastream', err) return } makeRequest(compressedData, this._url, (err, res) => { log.debug(`Response from the agent: ${res}`) if (err) { - log.error(err) + log.error('Error sending datastream', err) } }) }) diff --git a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js new file mode 100644 index 00000000000..dd44e9bfde0 --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js @@ -0,0 +1,80 @@ +'use strict' + +const session = require('./session') +const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults') +const { findScriptFromPartialPath, probes, breakpoints } = require('./state') +const log = require('../../log') + +let sessionStarted = false + +module.exports = { + addBreakpoint, + removeBreakpoint +} + +async function addBreakpoint (probe) { + if (!sessionStarted) await start() + + const file = probe.where.sourceFile + const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints + + // Optimize for sending data to /debugger/v1/input endpoint + probe.location = { file, lines: [String(line)] } + delete probe.where + + // Optimize for fast calculations when probe is hit + const snapshotsPerSecond = probe.sampling.snapshotsPerSecond ?? (probe.captureSnapshot + ? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE + : MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE) + probe.sampling.nsBetweenSampling = BigInt(1 / snapshotsPerSecond * 1e9) + probe.lastCaptureNs = 0n + + // TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached. + // Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will + // not continue untill all scripts have been parsed? + const script = findScriptFromPartialPath(file) + if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`) + const [path, scriptId] = script + + log.debug( + '[debugger:devtools_client] Adding breakpoint at %s:%d (probe: %s, version: %d)', + path, line, probe.id, probe.version + ) + + const { breakpointId } = await session.post('Debugger.setBreakpoint', { + location: { + scriptId, + lineNumber: line - 1 // Beware! lineNumber is zero-indexed + } + }) + + probes.set(probe.id, breakpointId) + breakpoints.set(breakpointId, probe) +} + +async function removeBreakpoint ({ id }) { + if (!sessionStarted) { + // We should not get in this state, but abort if we do, so the code doesn't fail unexpected + throw Error(`Cannot remove probe ${id}: Debugger not started`) + } + if (!probes.has(id)) { + throw Error(`Unknown probe id: ${id}`) + } + + const breakpointId = probes.get(id) + await session.post('Debugger.removeBreakpoint', { breakpointId }) + probes.delete(id) + breakpoints.delete(breakpointId) + + if (breakpoints.size === 0) await stop() +} + +async function start () { + sessionStarted = true + return session.post('Debugger.enable') // return instead of await to reduce number of promises created +} + +async function stop () { + sessionStarted = false + return session.post('Debugger.disable') // return instead of await to reduce number of promises created +} diff --git a/packages/dd-trace/src/debugger/devtools_client/config.js b/packages/dd-trace/src/debugger/devtools_client/config.js index 838a1a76cca..7783bc84d75 100644 --- a/packages/dd-trace/src/debugger/devtools_client/config.js +++ b/packages/dd-trace/src/debugger/devtools_client/config.js @@ -15,7 +15,9 @@ const config = module.exports = { updateUrl(parentConfig) configPort.on('message', updateUrl) -configPort.on('messageerror', (err) => log.error(err)) +configPort.on('messageerror', (err) => + log.error('[debugger:devtools_client] received "messageerror" on config port', err) +) function updateUrl (updates) { config.url = updates.url || format({ diff --git a/packages/dd-trace/src/debugger/devtools_client/defaults.js b/packages/dd-trace/src/debugger/devtools_client/defaults.js new file mode 100644 index 00000000000..6acb813ab26 --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/defaults.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = { + MAX_SNAPSHOTS_PER_SECOND_PER_PROBE: 1, + MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE: 5_000 +} diff --git a/packages/dd-trace/src/debugger/devtools_client/index.js b/packages/dd-trace/src/debugger/devtools_client/index.js index db71e7028e7..9634003bf61 100644 --- a/packages/dd-trace/src/debugger/devtools_client/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/index.js @@ -13,28 +13,59 @@ const { version } = require('../../../../../package.json') require('./remote_config') +// Expression to run on a call frame of the paused thread to get its active trace and span id. +const expression = ` + const context = global.require('dd-trace').scope().active()?.context(); + ({ trace_id: context?.toTraceId(), span_id: context?.toSpanId() }) +` + // There doesn't seem to be an official standard for the content of these fields, so we're just populating them with // something that should be useful to a Node.js developer. const threadId = parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${parentThreadId}` const threadName = parentThreadId === 0 ? 'MainThread' : `WorkerThread:${parentThreadId}` +// WARNING: The code above the line `await session.post('Debugger.resume')` is highly optimized. Please edit with care! session.on('Debugger.paused', async ({ params }) => { const start = process.hrtime.bigint() - const timestamp = Date.now() let captureSnapshotForProbe = null let maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength - const probes = params.hitBreakpoints.map((id) => { + + // V8 doesn't allow seting more than one breakpoint at a specific location, however, it's possible to set two + // breakpoints just next to eachother that will "snap" to the same logical location, which in turn will be hit at the + // same time. E.g. index.js:1:1 and index.js:1:2. + // TODO: Investigate if it will improve performance to create a fast-path for when there's only a single breakpoint + let sampled = false + const length = params.hitBreakpoints.length + let probes = new Array(length) + for (let i = 0; i < length; i++) { + const id = params.hitBreakpoints[i] const probe = breakpoints.get(id) - if (probe.captureSnapshot) { + + if (start - probe.lastCaptureNs < probe.sampling.nsBetweenSampling) { + continue + } + + sampled = true + probe.lastCaptureNs = start + + if (probe.captureSnapshot === true) { captureSnapshotForProbe = probe maxReferenceDepth = highestOrUndefined(probe.capture.maxReferenceDepth, maxReferenceDepth) maxCollectionSize = highestOrUndefined(probe.capture.maxCollectionSize, maxCollectionSize) maxFieldCount = highestOrUndefined(probe.capture.maxFieldCount, maxFieldCount) maxLength = highestOrUndefined(probe.capture.maxLength, maxLength) } - return probe - }) + + probes[i] = probe + } + + if (sampled === false) { + return session.post('Debugger.resume') + } + + const timestamp = Date.now() + const dd = await getDD(params.callFrames[0].callFrameId) let processLocalState if (captureSnapshotForProbe !== null) { @@ -54,7 +85,13 @@ session.on('Debugger.paused', async ({ params }) => { await session.post('Debugger.resume') const diff = process.hrtime.bigint() - start // TODO: Recored as telemetry (DEBUG-2858) - log.debug(`Finished processing breakpoints - main thread paused for: ${Number(diff) / 1000000} ms`) + log.debug( + '[debugger:devtools_client] Finished processing breakpoints - main thread paused for: %d ms', + Number(diff) / 1000000 + ) + + // Due to the highly optimized algorithm above, the `probes` array might have gaps + probes = probes.filter((probe) => !!probe) const logger = { // We can safely use `location.file` from the first probe in the array, since all probes hit by `hitBreakpoints` @@ -92,8 +129,8 @@ session.on('Debugger.paused', async ({ params }) => { } // TODO: Process template (DEBUG-2628) - send(probe.template, logger, snapshot, (err) => { - if (err) log.error(err) + send(probe.template, logger, dd, snapshot, (err) => { + if (err) log.error('Debugger error', err) else ackEmitting(probe) }) } @@ -102,3 +139,21 @@ session.on('Debugger.paused', async ({ params }) => { function highestOrUndefined (num, max) { return num === undefined ? max : Math.max(num, max ?? 0) } + +async function getDD (callFrameId) { + const { result } = await session.post('Debugger.evaluateOnCallFrame', { + callFrameId, + expression, + returnByValue: true, + includeCommandLineAPI: true + }) + + if (result?.value?.trace_id === undefined) { + if (result?.subtype === 'error') { + log.error('[debugger:devtools_client] Error getting trace/span id:', result.description) + } + return + } + + return result.value +} diff --git a/packages/dd-trace/src/debugger/devtools_client/remote_config.js b/packages/dd-trace/src/debugger/devtools_client/remote_config.js index 8a7d7386e33..8e56fdd7aa0 100644 --- a/packages/dd-trace/src/debugger/devtools_client/remote_config.js +++ b/packages/dd-trace/src/debugger/devtools_client/remote_config.js @@ -1,13 +1,10 @@ 'use strict' const { workerData: { rcPort } } = require('node:worker_threads') -const { findScriptFromPartialPath, probes, breakpoints } = require('./state') -const session = require('./session') +const { addBreakpoint, removeBreakpoint } = require('./breakpoints') const { ackReceived, ackInstalled, ackError } = require('./status') const log = require('../../log') -let sessionStarted = false - // Example log line probe (simplified): // { // id: '100c9a5c-45ad-49dc-818b-c570d31e11d1', @@ -44,20 +41,13 @@ rcPort.on('message', async ({ action, conf: probe, ackId }) => { ackError(err, probe) } }) -rcPort.on('messageerror', (err) => log.error(err)) - -async function start () { - sessionStarted = true - return session.post('Debugger.enable') // return instead of await to reduce number of promises created -} - -async function stop () { - sessionStarted = false - return session.post('Debugger.disable') // return instead of await to reduce number of promises created -} +rcPort.on('messageerror', (err) => log.error('[debugger:devtools_client] received "messageerror" on RC port', err)) async function processMsg (action, probe) { - log.debug(`Received request to ${action} ${probe.type} probe (id: ${probe.id}, version: ${probe.version})`) + log.debug( + '[debugger:devtools_client] Received request to %s %s probe (id: %s, version: %d)', + action, probe.type, probe.id, probe.version + ) if (action !== 'unapply') ackReceived(probe) @@ -66,7 +56,7 @@ async function processMsg (action, probe) { } if (!probe.where.sourceFile && !probe.where.lines) { throw new Error( - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len `Unsupported probe insertion point! Only line-based probes are supported (id: ${probe.id}, version: ${probe.version})` ) } @@ -90,15 +80,17 @@ async function processMsg (action, probe) { break case 'apply': await addBreakpoint(probe) + ackInstalled(probe) break case 'modify': // TODO: Modify existing probe instead of removing it (DEBUG-2817) await removeBreakpoint(probe) await addBreakpoint(probe) + ackInstalled(probe) // TODO: Should we also send ackInstalled when modifying a probe? break default: throw new Error( - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len `Cannot process probe ${probe.id} (version: ${probe.version}) - unknown remote configuration action: ${action}` ) } @@ -107,55 +99,6 @@ async function processMsg (action, probe) { } } -async function addBreakpoint (probe) { - if (!sessionStarted) await start() - - const file = probe.where.sourceFile - const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints - - // Optimize for sending data to /debugger/v1/input endpoint - probe.location = { file, lines: [String(line)] } - delete probe.where - - // TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached. - // Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will - // not continue untill all scripts have been parsed? - const script = findScriptFromPartialPath(file) - if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`) - const [path, scriptId] = script - - log.debug(`Adding breakpoint at ${path}:${line} (probe: ${probe.id}, version: ${probe.version})`) - - const { breakpointId } = await session.post('Debugger.setBreakpoint', { - location: { - scriptId, - lineNumber: line - 1 // Beware! lineNumber is zero-indexed - } - }) - - probes.set(probe.id, breakpointId) - breakpoints.set(breakpointId, probe) - - ackInstalled(probe) -} - -async function removeBreakpoint ({ id }) { - if (!sessionStarted) { - // We should not get in this state, but abort if we do, so the code doesn't fail unexpected - throw Error(`Cannot remove probe ${id}: Debugger not started`) - } - if (!probes.has(id)) { - throw Error(`Unknown probe id: ${id}`) - } - - const breakpointId = probes.get(id) - await session.post('Debugger.removeBreakpoint', { breakpointId }) - probes.delete(id) - breakpoints.delete(breakpointId) - - if (breakpoints.size === 0) await stop() -} - async function lock () { if (lock.p) await lock.p let resolve diff --git a/packages/dd-trace/src/debugger/devtools_client/send.js b/packages/dd-trace/src/debugger/devtools_client/send.js index f2ba5befd46..9d607b1ad1c 100644 --- a/packages/dd-trace/src/debugger/devtools_client/send.js +++ b/packages/dd-trace/src/debugger/devtools_client/send.js @@ -22,7 +22,7 @@ const ddtags = [ const path = `/debugger/v1/input?${stringify({ ddtags })}` -function send (message, logger, snapshot, cb) { +function send (message, logger, dd, snapshot, cb) { const opts = { method: 'POST', url: config.url, @@ -36,6 +36,7 @@ function send (message, logger, snapshot, cb) { service, message, logger, + dd, 'debugger.snapshot': snapshot } diff --git a/packages/dd-trace/src/debugger/devtools_client/state.js b/packages/dd-trace/src/debugger/devtools_client/state.js index c409a69f6b7..a69a37067f4 100644 --- a/packages/dd-trace/src/debugger/devtools_client/state.js +++ b/packages/dd-trace/src/debugger/devtools_client/state.js @@ -57,6 +57,6 @@ module.exports = { session.on('Debugger.scriptParsed', ({ params }) => { scriptUrls.set(params.scriptId, params.url) if (params.url.startsWith('file:')) { - scriptIds.push([params.url, params.scriptId]) + scriptIds.push([params.url, params.scriptId, params.sourceMapURL]) } }) diff --git a/packages/dd-trace/src/debugger/devtools_client/status.js b/packages/dd-trace/src/debugger/devtools_client/status.js index e4ba10d8c55..b228d7e50b7 100644 --- a/packages/dd-trace/src/debugger/devtools_client/status.js +++ b/packages/dd-trace/src/debugger/devtools_client/status.js @@ -55,7 +55,7 @@ function ackEmitting ({ id: probeId, version }) { } function ackError (err, { id: probeId, version }) { - log.error(err) + log.error('[debugger:devtools_client] ackError', err) onlyUniqueUpdates(STATUSES.ERROR, probeId, version, () => { const payload = statusPayload(probeId, version, STATUSES.ERROR) @@ -87,16 +87,16 @@ function send (payload) { } request(form, options, (err) => { - if (err) log.error(err) + if (err) log.error('[debugger:devtools_client] Error sending debugger payload', err) }) } -function statusPayload (probeId, version, status) { +function statusPayload (probeId, probeVersion, status) { return { ddsource, service, debugger: { - diagnostics: { probeId, runtimeId, version, status } + diagnostics: { probeId, runtimeId, probeVersion, status } } } } diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index 3638119c6f1..fee514f32f1 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -18,7 +18,7 @@ module.exports = { function start (config, rc) { if (worker !== null) return - log.debug('Starting Dynamic Instrumentation client...') + log.debug('[debugger] Starting Dynamic Instrumentation client...') const rcAckCallbacks = new Map() const rcChannel = new MessageChannel() @@ -33,14 +33,14 @@ function start (config, rc) { const ack = rcAckCallbacks.get(ackId) if (ack === undefined) { // This should never happen, but just in case something changes in the future, we should guard against it - log.error(`Received an unknown ackId: ${ackId}`) - if (error) log.error(error) + log.error('[debugger] Received an unknown ackId: %s', ackId) + if (error) log.error('[debugger] Error starting Dynamic Instrumentation client', error) return } ack(error) rcAckCallbacks.delete(ackId) }) - rcChannel.port2.on('messageerror', (err) => log.error(err)) + rcChannel.port2.on('messageerror', (err) => log.error('[debugger] received "messageerror" on RC port', err)) worker = new Worker( join(__dirname, 'devtools_client', 'index.js'), @@ -57,19 +57,17 @@ function start (config, rc) { } ) - worker.unref() - worker.on('online', () => { - log.debug(`Dynamic Instrumentation worker thread started successfully (thread id: ${worker.threadId})`) + log.debug('[debugger] Dynamic Instrumentation worker thread started successfully (thread id: %d)', worker.threadId) }) - worker.on('error', (err) => log.error(err)) - worker.on('messageerror', (err) => log.error(err)) + worker.on('error', (err) => log.error('[debugger] worker thread error', err)) + worker.on('messageerror', (err) => log.error('[debugger] received "messageerror" from worker', err)) worker.on('exit', (code) => { const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`) - log.error(error) + log.error('[debugger] worker thread exited unexpectedly', error) // Be nice, clean up now that the worker thread encounted an issue and we can't continue rc.removeProductHandler('LIVE_DEBUGGING') @@ -80,6 +78,12 @@ function start (config, rc) { rcAckCallbacks.delete(ackId) } }) + + worker.unref() + rcChannel.port1.unref() + rcChannel.port2.unref() + configChannel.port1.unref() + configChannel.port2.unref() } function configure (config) { diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index ba84de71341..a396c9e98a4 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -71,7 +71,7 @@ class DogStatsDClient { const buffer = Buffer.concat(queue) request(buffer, this._httpOptions, (err) => { if (err) { - log.error('HTTP error from agent: ' + err.stack) + log.error('DogStatsDClient: HTTP error from agent: %s', err.message, err) if (err.status === 404) { // Inside this if-block, we have connectivity to the agent, but // we're not getting a 200 from the proxy endpoint. If it's a 404, @@ -89,7 +89,7 @@ class DogStatsDClient { this._sendUdpFromQueue(queue, this._host, this._family) } else { lookup(this._host, (err, address, family) => { - if (err) return log.error(err) + if (err) return log.error('DogStatsDClient: Host not found', err) this._sendUdpFromQueue(queue, address, family) }) } diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index 02d96cb8a26..d5c72bdb575 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -1,26 +1,20 @@ 'use strict' const { truncateSpan, normalizeSpan } = require('./tags-processors') -const Chunk = require('./chunk') +const { Chunk, MsgpackEncoder } = require('../msgpack') const log = require('../log') const { isTrue } = require('../util') const coalesce = require('koalas') const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB -const float64Array = new Float64Array(1) -const uInt8Float64Array = new Uint8Array(float64Array.buffer) - -float64Array[0] = -1 - -const bigEndian = uInt8Float64Array[7] === 0 - function formatSpan (span) { return normalizeSpan(truncateSpan(span, false)) } class AgentEncoder { constructor (writer, limit = SOFT_LIMIT) { + this._msgpack = new MsgpackEncoder() this._limit = limit this._traceBytes = new Chunk() this._stringBytes = new Chunk() @@ -84,11 +78,11 @@ class AgentEncoder { bytes.reserve(1) if (span.type && span.meta_struct) { - bytes.buffer[bytes.length++] = 0x8d + bytes.buffer[bytes.length - 1] = 0x8d } else if (span.type || span.meta_struct) { - bytes.buffer[bytes.length++] = 0x8c + bytes.buffer[bytes.length - 1] = 0x8c } else { - bytes.buffer[bytes.length++] = 0x8b + bytes.buffer[bytes.length - 1] = 0x8b } if (span.type) { @@ -135,43 +129,31 @@ class AgentEncoder { this._cacheString('') } - _encodeArrayPrefix (bytes, value) { - const length = value.length - const offset = bytes.length + _encodeBuffer (bytes, buffer) { + this._msgpack.encodeBin(bytes, buffer) + } - bytes.reserve(5) - bytes.length += 5 + _encodeBool (bytes, value) { + this._msgpack.encodeBoolean(bytes, value) + } - bytes.buffer[offset] = 0xdd - bytes.buffer[offset + 1] = length >> 24 - bytes.buffer[offset + 2] = length >> 16 - bytes.buffer[offset + 3] = length >> 8 - bytes.buffer[offset + 4] = length + _encodeArrayPrefix (bytes, value) { + this._msgpack.encodeArrayPrefix(bytes, value) } _encodeMapPrefix (bytes, keysLength) { - const offset = bytes.length - - bytes.reserve(5) - bytes.length += 5 - bytes.buffer[offset] = 0xdf - bytes.buffer[offset + 1] = keysLength >> 24 - bytes.buffer[offset + 2] = keysLength >> 16 - bytes.buffer[offset + 3] = keysLength >> 8 - bytes.buffer[offset + 4] = keysLength + this._msgpack.encodeMapPrefix(bytes, keysLength) } _encodeByte (bytes, value) { - bytes.reserve(1) - - bytes.buffer[bytes.length++] = value + this._msgpack.encodeByte(bytes, value) } + // TODO: Use BigInt instead. _encodeId (bytes, id) { const offset = bytes.length bytes.reserve(9) - bytes.length += 9 id = id.toArray() @@ -186,36 +168,16 @@ class AgentEncoder { bytes.buffer[offset + 8] = id[7] } - _encodeInteger (bytes, value) { - const offset = bytes.length - - bytes.reserve(5) - bytes.length += 5 + _encodeNumber (bytes, value) { + this._msgpack.encodeNumber(bytes, value) + } - bytes.buffer[offset] = 0xce - bytes.buffer[offset + 1] = value >> 24 - bytes.buffer[offset + 2] = value >> 16 - bytes.buffer[offset + 3] = value >> 8 - bytes.buffer[offset + 4] = value + _encodeInteger (bytes, value) { + this._msgpack.encodeInteger(bytes, value) } _encodeLong (bytes, value) { - const offset = bytes.length - const hi = (value / Math.pow(2, 32)) >> 0 - const lo = value >>> 0 - - bytes.reserve(9) - bytes.length += 9 - - bytes.buffer[offset] = 0xcf - bytes.buffer[offset + 1] = hi >> 24 - bytes.buffer[offset + 2] = hi >> 16 - bytes.buffer[offset + 3] = hi >> 8 - bytes.buffer[offset + 4] = hi - bytes.buffer[offset + 5] = lo >> 24 - bytes.buffer[offset + 6] = lo >> 16 - bytes.buffer[offset + 7] = lo >> 8 - bytes.buffer[offset + 8] = lo + this._msgpack.encodeLong(bytes, value) } _encodeMap (bytes, value) { @@ -252,23 +214,7 @@ class AgentEncoder { } _encodeFloat (bytes, value) { - float64Array[0] = value - - const offset = bytes.length - bytes.reserve(9) - bytes.length += 9 - - bytes.buffer[offset] = 0xcb - - if (bigEndian) { - for (let i = 0; i <= 7; i++) { - bytes.buffer[offset + i + 1] = uInt8Float64Array[i] - } - } else { - for (let i = 7; i >= 0; i--) { - bytes.buffer[bytes.length - i - 1] = uInt8Float64Array[i] - } - } + this._msgpack.encodeFloat(bytes, value) } _encodeMetaStruct (bytes, value) { @@ -294,7 +240,6 @@ class AgentEncoder { const offset = bytes.length bytes.reserve(prefixLength) - bytes.length += prefixLength this._encodeObject(bytes, value) diff --git a/packages/dd-trace/src/encode/agentless-ci-visibility.js b/packages/dd-trace/src/encode/agentless-ci-visibility.js index dea15182323..bc5d9fc42b6 100644 --- a/packages/dd-trace/src/encode/agentless-ci-visibility.js +++ b/packages/dd-trace/src/encode/agentless-ci-visibility.js @@ -251,37 +251,6 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder { } } - _encodeNumber (bytes, value) { - if (Math.floor(value) !== value) { // float 64 - return this._encodeFloat(bytes, value) - } - return this._encodeLong(bytes, value) - } - - _encodeLong (bytes, value) { - const isPositive = value >= 0 - - const hi = isPositive ? (value / Math.pow(2, 32)) >> 0 : Math.floor(value / Math.pow(2, 32)) - const lo = value >>> 0 - const flag = isPositive ? 0xcf : 0xd3 - - const offset = bytes.length - - // int 64 - bytes.reserve(9) - bytes.length += 9 - - bytes.buffer[offset] = flag - bytes.buffer[offset + 1] = hi >> 24 - bytes.buffer[offset + 2] = hi >> 16 - bytes.buffer[offset + 3] = hi >> 8 - bytes.buffer[offset + 4] = hi - bytes.buffer[offset + 5] = lo >> 24 - bytes.buffer[offset + 6] = lo >> 16 - bytes.buffer[offset + 7] = lo >> 8 - bytes.buffer[offset + 8] = lo - } - _encode (bytes, trace) { if (this._isReset) { this._encodePayloadStart(bytes) @@ -380,7 +349,6 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder { // Get offset of the events list to update the length of the array when calling `makePayload` this._eventsOffset = bytes.length bytes.reserve(5) - bytes.length += 5 } reset () { diff --git a/packages/dd-trace/src/encode/coverage-ci-visibility.js b/packages/dd-trace/src/encode/coverage-ci-visibility.js index bdf4b17a3cc..5b31d83cb12 100644 --- a/packages/dd-trace/src/encode/coverage-ci-visibility.js +++ b/packages/dd-trace/src/encode/coverage-ci-visibility.js @@ -1,6 +1,6 @@ 'use strict' const { AgentEncoder } = require('./0.4') -const Chunk = require('./chunk') +const { Chunk } = require('../msgpack') const { distributionMetric, @@ -82,7 +82,6 @@ class CoverageCIVisibilityEncoder extends AgentEncoder { // Get offset of the coverages list to update the length of the array when calling `makePayload` this._coveragesOffset = bytes.length bytes.reserve(5) - bytes.length += 5 } makePayload () { diff --git a/packages/dd-trace/src/encode/span-stats.js b/packages/dd-trace/src/encode/span-stats.js index 15410cec203..43215756c7c 100644 --- a/packages/dd-trace/src/encode/span-stats.js +++ b/packages/dd-trace/src/encode/span-stats.js @@ -22,10 +22,6 @@ function truncate (value, maxLength, suffix = '') { } class SpanStatsEncoder extends AgentEncoder { - _encodeBool (bytes, value) { - this._encodeByte(bytes, value ? 0xc3 : 0xc2) - } - makePayload () { const traceSize = this._traceBytes.length const buffer = Buffer.allocUnsafe(traceSize) @@ -34,32 +30,6 @@ class SpanStatsEncoder extends AgentEncoder { return buffer } - _encodeMapPrefix (bytes, length) { - const offset = bytes.length - - bytes.reserve(1) - bytes.length += 1 - - bytes.buffer[offset] = 0x80 + length - } - - _encodeBuffer (bytes, buffer) { - const length = buffer.length - const offset = bytes.length - - bytes.reserve(5) - bytes.length += 5 - - bytes.buffer[offset] = 0xc6 - bytes.buffer[offset + 1] = length >> 24 - bytes.buffer[offset + 2] = length >> 16 - bytes.buffer[offset + 3] = length >> 8 - bytes.buffer[offset + 4] = length - - buffer.copy(bytes.buffer, offset + 5) - bytes.length += length - } - _encodeStat (bytes, stat) { this._encodeMapPrefix(bytes, 12) diff --git a/packages/dd-trace/src/exporters/agent/writer.js b/packages/dd-trace/src/exporters/agent/writer.js index 82a28647778..8fac323e614 100644 --- a/packages/dd-trace/src/exporters/agent/writer.js +++ b/packages/dd-trace/src/exporters/agent/writer.js @@ -41,17 +41,17 @@ class Writer extends BaseWriter { startupLog({ agentError: err }) if (err) { - log.error(err) + log.error('Error sending payload to the agent (status code: %s)', err.status, err) done() return } - log.debug(`Response from the agent: ${res}`) + log.debug('Response from the agent: %s', res) try { this._prioritySampler.update(JSON.parse(res).rate_by_service) } catch (e) { - log.error(e) + log.error('Error updating prioritySampler rates', e) runtimeMetrics.increment(`${METRIC_PREFIX}.errors`, true) runtimeMetrics.increment(`${METRIC_PREFIX}.errors.by.name`, `name:${e.name}`, true) diff --git a/packages/dd-trace/src/exporters/common/request.js b/packages/dd-trace/src/exporters/common/request.js index ab8b697eef6..2ff90236ee8 100644 --- a/packages/dd-trace/src/exporters/common/request.js +++ b/packages/dd-trace/src/exporters/common/request.js @@ -86,7 +86,7 @@ function request (data, options, callback) { if (isGzip) { zlib.gunzip(buffer, (err, result) => { if (err) { - log.error(`Could not gunzip response: ${err.message}`) + log.error('Could not gunzip response: %s', err.message) callback(null, '', res.statusCode) } else { callback(null, result.toString(), res.statusCode) diff --git a/packages/dd-trace/src/exporters/span-stats/writer.js b/packages/dd-trace/src/exporters/span-stats/writer.js index 3ece6d221b4..37cd6c77d5e 100644 --- a/packages/dd-trace/src/exporters/span-stats/writer.js +++ b/packages/dd-trace/src/exporters/span-stats/writer.js @@ -16,7 +16,7 @@ class Writer extends BaseWriter { _sendPayload (data, _, done) { makeRequest(data, this._url, (err, res) => { if (err) { - log.error(err) + log.error('Error sending span stats', err) done() return } diff --git a/packages/dd-trace/src/flare/index.js b/packages/dd-trace/src/flare/index.js index 70ec4ccd75e..4a5166d45e1 100644 --- a/packages/dd-trace/src/flare/index.js +++ b/packages/dd-trace/src/flare/index.js @@ -83,7 +83,7 @@ const flare = { headers: form.getHeaders() }, (err) => { if (err) { - log.error(err) + log.error('Error sending flare payload', err) } }) } diff --git a/packages/dd-trace/src/guardrails/index.js b/packages/dd-trace/src/guardrails/index.js new file mode 100644 index 00000000000..179262f154e --- /dev/null +++ b/packages/dd-trace/src/guardrails/index.js @@ -0,0 +1,64 @@ +'use strict' + +/* eslint-disable no-var */ + +var path = require('path') +var Module = require('module') +var isTrue = require('./util').isTrue +var log = require('./log') +var telemetry = require('./telemetry') +var nodeVersion = require('../../../../version') + +var NODE_MAJOR = nodeVersion.NODE_MAJOR + +function guard (fn) { + var initBailout = false + 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 version = process.versions.node + + if (process.env.DD_INJECTION_ENABLED) { + // If we're running via single-step install, and we're in the app's + // node_modules, then we should not initialize the tracer. This prevents + // single-step-installed tracer from clobbering the manually-installed tracer. + var resolvedInApp + var entrypoint = process.argv[1] + try { + resolvedInApp = Module.createRequire(entrypoint).resolve('dd-trace') + } catch (e) { + // Ignore. If we can't resolve the module, we assume it's not in the app. + } + if (resolvedInApp) { + var ourselves = path.normalize(path.join(__dirname, '..', '..', '..', '..', 'index.js')) + if (ourselves !== resolvedInApp) { + clobberBailout = true + } + } + } + + // If the runtime doesn't match the engines field in package.json, then we + // should not initialize the tracer. + if (!clobberBailout && NODE_MAJOR < minMajor) { + initBailout = true + telemetry([ + { name: 'abort', tags: ['reason:incompatible_runtime'] }, + { name: 'abort.runtime', tags: [] } + ]) + log.info('Aborting application instrumentation due to incompatible_runtime.') + log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') + if (forced) { + log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') + } + } + + if (!clobberBailout && (!initBailout || forced)) { + var result = fn() + telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) + log.info('Application instrumentation bootstrapping complete') + return result + } +} + +module.exports = guard diff --git a/packages/dd-trace/src/guardrails/log.js b/packages/dd-trace/src/guardrails/log.js new file mode 100644 index 00000000000..dd74e5bdbf0 --- /dev/null +++ b/packages/dd-trace/src/guardrails/log.js @@ -0,0 +1,32 @@ +'use strict' + +/* eslint-disable no-var */ +/* eslint-disable no-console */ + +var isTrue = require('./util').isTrue + +var DD_TRACE_DEBUG = process.env.DD_TRACE_DEBUG +var DD_TRACE_LOG_LEVEL = process.env.DD_TRACE_LOG_LEVEL + +var logLevels = { + trace: 20, + debug: 20, + info: 30, + warn: 40, + error: 50, + critical: 50, + off: 100 +} + +var logLevel = isTrue(DD_TRACE_DEBUG) + ? Number(DD_TRACE_LOG_LEVEL) || logLevels.debug + : logLevels.off + +var log = { + debug: logLevel <= 20 ? console.debug.bind(console) : function () {}, + info: logLevel <= 30 ? console.info.bind(console) : function () {}, + warn: logLevel <= 40 ? console.warn.bind(console) : function () {}, + error: logLevel <= 50 ? console.error.bind(console) : function () {} +} + +module.exports = log diff --git a/packages/dd-trace/src/guardrails/telemetry.js b/packages/dd-trace/src/guardrails/telemetry.js new file mode 100644 index 00000000000..0c73e1f0bce --- /dev/null +++ b/packages/dd-trace/src/guardrails/telemetry.js @@ -0,0 +1,78 @@ +'use strict' + +/* eslint-disable no-var */ +/* eslint-disable object-shorthand */ + +var fs = require('fs') +var spawn = require('child_process').spawn +var tracerVersion = require('../../../../package.json').version +var log = require('./log') + +module.exports = sendTelemetry + +if (!process.env.DD_INJECTION_ENABLED) { + module.exports = function () {} +} + +if (!process.env.DD_TELEMETRY_FORWARDER_PATH) { + module.exports = function () {} +} + +if (!fs.existsSync(process.env.DD_TELEMETRY_FORWARDER_PATH)) { + module.exports = function () {} +} + +var metadata = { + language_name: 'nodejs', + language_version: process.versions.node, + runtime_name: 'nodejs', + runtime_version: process.versions.node, + tracer_version: tracerVersion, + pid: process.pid +} + +var seen = [] +function hasSeen (point) { + if (point.name === 'abort') { + // This one can only be sent once, regardless of tags + return seen.includes('abort') + } + if (point.name === 'abort.integration') { + // For now, this is the only other one we want to dedupe + var compiledPoint = point.name + point.tags.join('') + return seen.includes(compiledPoint) + } + return false +} + +function sendTelemetry (name, tags) { + var points = name + if (typeof name === 'string') { + points = [{ name: name, tags: tags || [] }] + } + if (['1', 'true', 'True'].indexOf(process.env.DD_INJECT_FORCE) !== -1) { + points = points.filter(function (p) { return ['error', 'complete'].includes(p.name) }) + } + points = points.filter(function (p) { return !hasSeen(p) }) + for (var i = 0; i < points.length; i++) { + points[i].name = 'library_entrypoint.' + points[i].name + } + if (points.length === 0) { + return + } + var proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], { + stdio: 'pipe' + }) + proc.on('error', function () { + log.error('Failed to spawn telemetry forwarder') + }) + proc.on('exit', function (code) { + if (code !== 0) { + log.error('Telemetry forwarder exited with code ' + code) + } + }) + proc.stdin.on('error', function () { + log.error('Failed to write telemetry data to telemetry forwarder') + }) + proc.stdin.end(JSON.stringify({ metadata: metadata, points: points })) +} diff --git a/packages/dd-trace/src/guardrails/util.js b/packages/dd-trace/src/guardrails/util.js new file mode 100644 index 00000000000..9aa60713573 --- /dev/null +++ b/packages/dd-trace/src/guardrails/util.js @@ -0,0 +1,10 @@ +'use strict' + +/* eslint-disable object-shorthand */ + +function isTrue (str) { + str = String(str).toLowerCase() + return str === 'true' || str === '1' +} + +module.exports = { isTrue: isTrue } diff --git a/packages/dd-trace/src/lambda/runtime/ritm.js b/packages/dd-trace/src/lambda/runtime/ritm.js index 4dd27713a0b..ec50a4a80be 100644 --- a/packages/dd-trace/src/lambda/runtime/ritm.js +++ b/packages/dd-trace/src/lambda/runtime/ritm.js @@ -101,7 +101,7 @@ const registerLambdaHook = () => { try { moduleExports = hook(moduleExports) } catch (e) { - log.error(e) + log.error('Error executing lambda hook', e) } } @@ -120,7 +120,7 @@ const registerLambdaHook = () => { try { moduleExports = hook(moduleExports) } catch (e) { - log.error(e) + log.error('Error executing lambda hook for datadog-lambda-js', e) } } } diff --git a/packages/dd-trace/src/llmobs/storage.js b/packages/dd-trace/src/llmobs/storage.js index 1362aaf966e..82202c18174 100644 --- a/packages/dd-trace/src/llmobs/storage.js +++ b/packages/dd-trace/src/llmobs/storage.js @@ -1,7 +1,6 @@ 'use strict' -// TODO: remove this and use namespaced storage once available -const { AsyncLocalStorage } = require('async_hooks') -const storage = new AsyncLocalStorage() +const { storage: createStorage } = require('../../../datadog-core') +const storage = createStorage('llmobs') module.exports = { storage } diff --git a/packages/dd-trace/src/llmobs/writers/base.js b/packages/dd-trace/src/llmobs/writers/base.js index 8a6cdae9c2f..1d33bc653ad 100644 --- a/packages/dd-trace/src/llmobs/writers/base.js +++ b/packages/dd-trace/src/llmobs/writers/base.js @@ -74,11 +74,11 @@ class BaseLLMObsWriter { request(payload, options, (err, resp, code) => { if (err) { logger.error( - `Error sending ${events.length} LLMObs ${this._eventType} events to ${this._url}: ${err.message}` + 'Error sending %d LLMObs %s events to %s: %s', events.length, this._eventType, this._url, err.message, err ) } else if (code >= 300) { logger.error( - `Error sending ${events.length} LLMObs ${this._eventType} events to ${this._url}: ${code}` + 'Error sending %d LLMObs %s events to %s: %s', events.length, this._eventType, this._url, code ) } else { logger.debug(`Sent ${events.length} LLMObs ${this._eventType} events to ${this._url}`) diff --git a/packages/dd-trace/src/log/index.js b/packages/dd-trace/src/log/index.js index 726d7d1e5e7..3a5392340df 100644 --- a/packages/dd-trace/src/log/index.js +++ b/packages/dd-trace/src/log/index.js @@ -4,6 +4,7 @@ const coalesce = require('koalas') const { isTrue } = require('../util') const { debugChannel, infoChannel, warnChannel, errorChannel } = require('./channels') const logWriter = require('./writer') +const { Log } = require('./log') const memoize = func => { const cache = {} @@ -18,10 +19,6 @@ const memoize = func => { return memoized } -function processMsg (msg) { - return typeof msg === 'function' ? msg() : msg -} - const config = { enabled: false, logger: undefined, @@ -52,37 +49,37 @@ const log = { reset () { logWriter.reset() this._deprecate = memoize((code, message) => { - errorChannel.publish(message) + errorChannel.publish(Log.parse(message)) return true }) return this }, - debug (message) { + debug (...args) { if (debugChannel.hasSubscribers) { - debugChannel.publish(processMsg(message)) + debugChannel.publish(Log.parse(...args)) } return this }, - info (message) { + info (...args) { if (infoChannel.hasSubscribers) { - infoChannel.publish(processMsg(message)) + infoChannel.publish(Log.parse(...args)) } return this }, - warn (message) { + warn (...args) { if (warnChannel.hasSubscribers) { - warnChannel.publish(processMsg(message)) + warnChannel.publish(Log.parse(...args)) } return this }, - error (err) { + error (...args) { if (errorChannel.hasSubscribers) { - errorChannel.publish(processMsg(err)) + errorChannel.publish(Log.parse(...args)) } return this }, diff --git a/packages/dd-trace/src/log/log.js b/packages/dd-trace/src/log/log.js new file mode 100644 index 00000000000..a9ec407291a --- /dev/null +++ b/packages/dd-trace/src/log/log.js @@ -0,0 +1,52 @@ +'use strict' + +const { format } = require('util') + +class Log { + constructor (message, args, cause, delegate) { + this.message = message + this.args = args + this.cause = cause + this.delegate = delegate + } + + get formatted () { + const { message, args } = this + + let formatted = message + if (message && args && args.length) { + formatted = format(message, ...args) + } + return formatted + } + + static parse (...args) { + let message, cause, delegate + + const lastArg = args[args.length - 1] + if (lastArg && typeof lastArg === 'object' && lastArg.stack) { // lastArg instanceof Error? + cause = args.pop() + } + + const firstArg = args.shift() + if (firstArg) { + if (typeof firstArg === 'string') { + message = firstArg + } else if (typeof firstArg === 'object') { + message = String(firstArg.message || firstArg) + } else if (typeof firstArg === 'function') { + delegate = firstArg + } else { + message = String(firstArg) + } + } else if (!cause) { + message = String(firstArg) + } + + return new Log(message, args, cause, delegate) + } +} + +module.exports = { + Log +} diff --git a/packages/dd-trace/src/log/writer.js b/packages/dd-trace/src/log/writer.js index bc4a5b20621..4724253244b 100644 --- a/packages/dd-trace/src/log/writer.js +++ b/packages/dd-trace/src/log/writer.js @@ -2,6 +2,7 @@ const { storage } = require('../../../datadog-core') const { LogChannel } = require('./channels') +const { Log } = require('./log') const defaultLogger = { debug: msg => console.debug(msg), /* eslint-disable-line no-console */ info: msg => console.info(msg), /* eslint-disable-line no-console */ @@ -22,7 +23,7 @@ function withNoop (fn) { } function unsubscribeAll () { - logChannel.unsubscribe({ debug, info, warn, error }) + logChannel.unsubscribe({ debug: onDebug, info: onInfo, warn: onWarn, error: onError }) } function toggleSubscription (enable, level) { @@ -30,7 +31,7 @@ function toggleSubscription (enable, level) { if (enable) { logChannel = new LogChannel(level) - logChannel.subscribe({ debug, info, warn, error }) + logChannel.subscribe({ debug: onDebug, info: onInfo, warn: onWarn, error: onError }) } } @@ -51,32 +52,62 @@ function reset () { toggleSubscription(false) } -function error (err) { - if (typeof err !== 'object' || !err) { - err = String(err) - } else if (!err.stack) { - err = String(err.message || err) +function getErrorLog (err) { + if (err && typeof err.delegate === 'function') { + const result = err.delegate() + return Array.isArray(result) ? Log.parse(...result) : Log.parse(result) + } else { + return err } +} - if (typeof err === 'string') { - err = new Error(err) - } +function onError (err) { + const { formatted, cause } = getErrorLog(err) + + // calling twice logger.error() because Error cause is only available in nodejs v16.9.0 + // TODO: replace it with Error(message, { cause }) when cause has broad support + if (formatted) withNoop(() => logger.error(new Error(formatted))) + if (cause) withNoop(() => logger.error(cause)) +} + +function onWarn (log) { + const { formatted, cause } = getErrorLog(log) + if (formatted) withNoop(() => logger.warn(formatted)) + if (cause) withNoop(() => logger.warn(cause)) +} - withNoop(() => logger.error(err)) +function onInfo (log) { + const { formatted, cause } = getErrorLog(log) + if (formatted) withNoop(() => logger.info(formatted)) + if (cause) withNoop(() => logger.info(cause)) } -function warn (message) { - if (!logger.warn) return debug(message) - withNoop(() => logger.warn(message)) +function onDebug (log) { + const { formatted, cause } = getErrorLog(log) + if (formatted) withNoop(() => logger.debug(formatted)) + if (cause) withNoop(() => logger.debug(cause)) } -function info (message) { - if (!logger.info) return debug(message) - withNoop(() => logger.info(message)) +function error (...args) { + onError(Log.parse(...args)) +} + +function warn (...args) { + const log = Log.parse(...args) + if (!logger.warn) return onDebug(log) + + onWarn(log) +} + +function info (...args) { + const log = Log.parse(...args) + if (!logger.info) return onDebug(log) + + onInfo(log) } -function debug (message) { - withNoop(() => logger.debug(message)) +function debug (...args) { + onDebug(Log.parse(...args)) } module.exports = { use, toggle, reset, error, warn, info, debug } diff --git a/packages/dd-trace/src/encode/chunk.js b/packages/dd-trace/src/msgpack/chunk.js similarity index 85% rename from packages/dd-trace/src/encode/chunk.js rename to packages/dd-trace/src/msgpack/chunk.js index 8a17b45f430..02999086c55 100644 --- a/packages/dd-trace/src/encode/chunk.js +++ b/packages/dd-trace/src/msgpack/chunk.js @@ -10,6 +10,7 @@ const DEFAULT_MIN_SIZE = 2 * 1024 * 1024 // 2MB class Chunk { constructor (minSize = DEFAULT_MIN_SIZE) { this.buffer = Buffer.allocUnsafe(minSize) + this.view = new DataView(this.buffer.buffer) this.length = 0 this._minSize = minSize } @@ -20,11 +21,9 @@ class Chunk { if (length < 0x20) { // fixstr this.reserve(length + 1) - this.length += 1 this.buffer[offset] = length | 0xa0 } else if (length < 0x100000000) { // str 32 this.reserve(length + 5) - this.length += 5 this.buffer[offset] = 0xdb this.buffer[offset + 1] = length >> 24 this.buffer[offset + 2] = length >> 16 @@ -32,7 +31,7 @@ class Chunk { this.buffer[offset + 4] = length } - this.length += this.buffer.utf8Write(value, this.length, length) + this.buffer.utf8Write(value, this.length - length, length) return this.length - offset } @@ -42,22 +41,26 @@ class Chunk { } set (array) { + const length = this.length + this.reserve(array.length) - this.buffer.set(array, this.length) - this.length += array.length + this.buffer.set(array, length) } reserve (size) { if (this.length + size > this.buffer.length) { this._resize(this._minSize * Math.ceil((this.length + size) / this._minSize)) } + + this.length += size } _resize (size) { const oldBuffer = this.buffer this.buffer = Buffer.allocUnsafe(size) + this.view = new DataView(this.buffer.buffer) oldBuffer.copy(this.buffer, 0, 0, this.length) } diff --git a/packages/dd-trace/src/msgpack/encoder.js b/packages/dd-trace/src/msgpack/encoder.js new file mode 100644 index 00000000000..6fa39d82148 --- /dev/null +++ b/packages/dd-trace/src/msgpack/encoder.js @@ -0,0 +1,309 @@ +'use strict' + +const Chunk = require('./chunk') + +class MsgpackEncoder { + encode (value) { + const bytes = new Chunk() + + this.encodeValue(bytes, value) + + return bytes.buffer.subarray(0, bytes.length) + } + + encodeValue (bytes, value) { + switch (typeof value) { + case 'bigint': + this.encodeBigInt(bytes, value) + break + case 'boolean': + this.encodeBoolean(bytes, value) + break + case 'number': + this.encodeNumber(bytes, value) + break + case 'object': + if (value === null) { + this.encodeNull(bytes, value) + } else if (Array.isArray(value)) { + this.encodeArray(bytes, value) + } else if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) { + this.encodeBin(bytes, value) + } else { + this.encodeMap(bytes, value) + } + break + case 'string': + this.encodeString(bytes, value) + break + case 'symbol': + this.encodeString(bytes, value.toString()) + break + default: // function, symbol, undefined + this.encodeNull(bytes, value) + break + } + } + + encodeNull (bytes) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = 0xc0 + } + + encodeBoolean (bytes, value) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = value ? 0xc3 : 0xc2 + } + + encodeString (bytes, value) { + bytes.write(value) + } + + encodeFixArray (bytes, size = 0) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = 0x90 + size + } + + encodeArrayPrefix (bytes, value) { + const length = value.length + const offset = bytes.length + + bytes.reserve(5) + bytes.buffer[offset] = 0xdd + bytes.buffer[offset + 1] = length >> 24 + bytes.buffer[offset + 2] = length >> 16 + bytes.buffer[offset + 3] = length >> 8 + bytes.buffer[offset + 4] = length + } + + encodeArray (bytes, value) { + if (value.length < 16) { + this.encodeFixArray(bytes, value.length) + } else { + this.encodeArrayPrefix(bytes, value) + } + + for (const item of value) { + this.encodeValue(bytes, item) + } + } + + encodeFixMap (bytes, size = 0) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = 0x80 + size + } + + encodeMapPrefix (bytes, keysLength) { + const offset = bytes.length + + bytes.reserve(5) + bytes.buffer[offset] = 0xdf + bytes.buffer[offset + 1] = keysLength >> 24 + bytes.buffer[offset + 2] = keysLength >> 16 + bytes.buffer[offset + 3] = keysLength >> 8 + bytes.buffer[offset + 4] = keysLength + } + + encodeByte (bytes, value) { + bytes.reserve(1) + bytes.buffer[bytes.length - 1] = value + } + + encodeBin (bytes, value) { + const offset = bytes.length + + if (value.byteLength < 256) { + bytes.reserve(2) + bytes.buffer[offset] = 0xc4 + bytes.buffer[offset + 1] = value.byteLength + } else if (value.byteLength < 65536) { + bytes.reserve(3) + bytes.buffer[offset] = 0xc5 + bytes.buffer[offset + 1] = value.byteLength >> 8 + bytes.buffer[offset + 2] = value.byteLength + } else { + bytes.reserve(5) + bytes.buffer[offset] = 0xc6 + bytes.buffer[offset + 1] = value.byteLength >> 24 + bytes.buffer[offset + 2] = value.byteLength >> 16 + bytes.buffer[offset + 3] = value.byteLength >> 8 + bytes.buffer[offset + 4] = value.byteLength + } + + bytes.set(value) + } + + encodeInteger (bytes, value) { + const offset = bytes.length + + bytes.reserve(5) + bytes.buffer[offset] = 0xce + bytes.buffer[offset + 1] = value >> 24 + bytes.buffer[offset + 2] = value >> 16 + bytes.buffer[offset + 3] = value >> 8 + bytes.buffer[offset + 4] = value + } + + encodeShort (bytes, value) { + const offset = bytes.length + + bytes.reserve(3) + bytes.buffer[offset] = 0xcd + bytes.buffer[offset + 1] = value >> 8 + bytes.buffer[offset + 2] = value + } + + encodeLong (bytes, value) { + const offset = bytes.length + const hi = (value / Math.pow(2, 32)) >> 0 + const lo = value >>> 0 + + bytes.reserve(9) + bytes.buffer[offset] = 0xcf + bytes.buffer[offset + 1] = hi >> 24 + bytes.buffer[offset + 2] = hi >> 16 + bytes.buffer[offset + 3] = hi >> 8 + bytes.buffer[offset + 4] = hi + bytes.buffer[offset + 5] = lo >> 24 + bytes.buffer[offset + 6] = lo >> 16 + bytes.buffer[offset + 7] = lo >> 8 + bytes.buffer[offset + 8] = lo + } + + encodeNumber (bytes, value) { + if (Number.isNaN(value)) { + value = 0 + } + if (Number.isInteger(value)) { + if (value >= 0) { + this.encodeUnsigned(bytes, value) + } else { + this.encodeSigned(bytes, value) + } + } else { + this.encodeFloat(bytes, value) + } + } + + encodeSigned (bytes, value) { + const offset = bytes.length + + if (value >= -0x20) { + bytes.reserve(1) + bytes.buffer[offset] = value + } else if (value >= -0x80) { + bytes.reserve(2) + bytes.buffer[offset] = 0xd0 + bytes.buffer[offset + 1] = value + } else if (value >= -0x8000) { + bytes.reserve(3) + bytes.buffer[offset] = 0xd1 + bytes.buffer[offset + 1] = value >> 8 + bytes.buffer[offset + 2] = value + } else if (value >= -0x80000000) { + bytes.reserve(5) + bytes.buffer[offset] = 0xd2 + bytes.buffer[offset + 1] = value >> 24 + bytes.buffer[offset + 2] = value >> 16 + bytes.buffer[offset + 3] = value >> 8 + bytes.buffer[offset + 4] = value + } else { + const hi = Math.floor(value / Math.pow(2, 32)) + const lo = value >>> 0 + + bytes.reserve(9) + bytes.buffer[offset] = 0xd3 + bytes.buffer[offset + 1] = hi >> 24 + bytes.buffer[offset + 2] = hi >> 16 + bytes.buffer[offset + 3] = hi >> 8 + bytes.buffer[offset + 4] = hi + bytes.buffer[offset + 5] = lo >> 24 + bytes.buffer[offset + 6] = lo >> 16 + bytes.buffer[offset + 7] = lo >> 8 + bytes.buffer[offset + 8] = lo + } + } + + encodeUnsigned (bytes, value) { + const offset = bytes.length + + if (value <= 0x7f) { + bytes.reserve(1) + bytes.buffer[offset] = value + } else if (value <= 0xff) { + bytes.reserve(2) + bytes.buffer[offset] = 0xcc + bytes.buffer[offset + 1] = value + } else if (value <= 0xffff) { + bytes.reserve(3) + bytes.buffer[offset] = 0xcd + bytes.buffer[offset + 1] = value >> 8 + bytes.buffer[offset + 2] = value + } else if (value <= 0xffffffff) { + bytes.reserve(5) + bytes.buffer[offset] = 0xce + bytes.buffer[offset + 1] = value >> 24 + bytes.buffer[offset + 2] = value >> 16 + bytes.buffer[offset + 3] = value >> 8 + bytes.buffer[offset + 4] = value + } else { + const hi = (value / Math.pow(2, 32)) >> 0 + const lo = value >>> 0 + + bytes.reserve(9) + bytes.buffer[offset] = 0xcf + bytes.buffer[offset + 1] = hi >> 24 + bytes.buffer[offset + 2] = hi >> 16 + bytes.buffer[offset + 3] = hi >> 8 + bytes.buffer[offset + 4] = hi + bytes.buffer[offset + 5] = lo >> 24 + bytes.buffer[offset + 6] = lo >> 16 + bytes.buffer[offset + 7] = lo >> 8 + bytes.buffer[offset + 8] = lo + } + } + + // TODO: Support BigInt larger than 64bit. + encodeBigInt (bytes, value) { + const offset = bytes.length + + bytes.reserve(9) + + if (value >= 0n) { + bytes.buffer[offset] = 0xcf + bytes.view.setBigUint64(offset + 1, value) + } else { + bytes.buffer[offset] = 0xd3 + bytes.view.setBigInt64(offset + 1, value) + } + } + + encodeMap (bytes, value) { + const keys = Object.keys(value) + + this.encodeMapPrefix(bytes, keys.length) + + for (const key of keys) { + this.encodeValue(bytes, key) + this.encodeValue(bytes, value[key]) + } + } + + encodeFloat (bytes, value) { + const offset = bytes.length + + bytes.reserve(9) + bytes.buffer[offset] = 0xcb + bytes.view.setFloat64(offset + 1, value) + } +} + +module.exports = { MsgpackEncoder } diff --git a/packages/dd-trace/src/msgpack/index.js b/packages/dd-trace/src/msgpack/index.js new file mode 100644 index 00000000000..03228d27044 --- /dev/null +++ b/packages/dd-trace/src/msgpack/index.js @@ -0,0 +1,6 @@ +'use strict' + +const Chunk = require('./chunk') +const { MsgpackEncoder } = require('./encoder') + +module.exports = { Chunk, MsgpackEncoder } diff --git a/packages/dd-trace/src/noop/span.js b/packages/dd-trace/src/noop/span.js index 0bdbf96ef66..1a431d090ea 100644 --- a/packages/dd-trace/src/noop/span.js +++ b/packages/dd-trace/src/noop/span.js @@ -22,6 +22,7 @@ class NoopSpan { setTag (key, value) { return this } addTags (keyValueMap) { return this } addLink (link) { return this } + addSpanPointer (ptrKind, ptrDir, ptrHash) { return this } log () { return this } logEvent () {} finish (finishTime) {} diff --git a/packages/dd-trace/src/opentelemetry/context_manager.js b/packages/dd-trace/src/opentelemetry/context_manager.js index fba84eef9f4..430626bbd7e 100644 --- a/packages/dd-trace/src/opentelemetry/context_manager.js +++ b/packages/dd-trace/src/opentelemetry/context_manager.js @@ -1,6 +1,6 @@ 'use strict' -const { AsyncLocalStorage } = require('async_hooks') +const { storage } = require('../../../datadog-core') const { trace, ROOT_CONTEXT } = require('@opentelemetry/api') const DataDogSpanContext = require('../opentracing/span_context') @@ -9,7 +9,7 @@ const tracer = require('../../') class ContextManager { constructor () { - this._store = new AsyncLocalStorage() + this._store = storage('opentelemetry') } active () { diff --git a/packages/dd-trace/src/opentelemetry/span.js b/packages/dd-trace/src/opentelemetry/span.js index d2c216c138e..68355ad9970 100644 --- a/packages/dd-trace/src/opentelemetry/span.js +++ b/packages/dd-trace/src/opentelemetry/span.js @@ -14,6 +14,7 @@ const { SERVICE_NAME, RESOURCE_NAME } = require('../../../../ext/tags') const kinds = require('../../../../ext/kinds') const SpanContext = require('./span_context') +const id = require('../id') // The one built into OTel rounds so we lose sub-millisecond precision. function hrTimeToMilliseconds (time) { @@ -217,6 +218,20 @@ class Span { return this } + addSpanPointer (ptrKind, ptrDir, ptrHash) { + const zeroContext = new SpanContext({ + traceId: id('0'), + spanId: id('0') + }) + const attributes = { + 'ptr.kind': ptrKind, + 'ptr.dir': ptrDir, + 'ptr.hash': ptrHash, + 'link.kind': 'span-pointer' + } + return this.addLink(zeroContext, attributes) + } + setStatus ({ code, message }) { if (!this.ended && !this._hasStatus && code) { this._hasStatus = true diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index e9a6c2f28a9..82bc9f2b30f 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -290,50 +290,66 @@ class TextMapPropagator { } _extractSpanContext (carrier) { - let spanContext = null + let context = null for (const extractor of this._config.tracePropagationStyle.extract) { - // add logic to ensure tracecontext headers takes precedence over other extracted headers - if (spanContext !== null) { - if (this._config.tracePropagationExtractFirst) { - return spanContext - } - if (extractor !== 'tracecontext') { - continue - } - spanContext = this._resolveTraceContextConflicts( - this._extractTraceparentContext(carrier), spanContext, carrier) - break - } - + let extractedContext = null switch (extractor) { case 'datadog': - spanContext = this._extractDatadogContext(carrier) + extractedContext = this._extractDatadogContext(carrier) break case 'tracecontext': - spanContext = this._extractTraceparentContext(carrier) + extractedContext = this._extractTraceparentContext(carrier) break - case 'b3' && this - ._config - .tracePropagationStyle - .otelPropagators: // TODO: should match "b3 single header" in next major case 'b3 single header': // TODO: delete in major after singular "b3" - spanContext = this._extractB3SingleContext(carrier) + extractedContext = this._extractB3SingleContext(carrier) break case 'b3': + if (this._config.tracePropagationStyle.otelPropagators) { + // TODO: should match "b3 single header" in next major + extractedContext = this._extractB3SingleContext(carrier) + } else { + extractedContext = this._extractB3MultiContext(carrier) + } + break case 'b3multi': - spanContext = this._extractB3MultiContext(carrier) + extractedContext = this._extractB3MultiContext(carrier) break default: if (extractor !== 'baggage') log.warn(`Unknown propagation style: ${extractor}`) } - if (this._config.tracePropagationStyle.extract.includes('baggage') && carrier.baggage) { - spanContext = spanContext || new DatadogSpanContext() - this._extractBaggageItems(carrier, spanContext) + if (extractedContext === null) { // If the current extractor was invalid, continue to the next extractor + continue } + + if (context === null) { + context = extractedContext + if (this._config.tracePropagationExtractFirst) { + return context + } + } else { + // If extractor is tracecontext, add tracecontext specific information to the context + if (extractor === 'tracecontext') { + context = this._resolveTraceContextConflicts( + this._extractTraceparentContext(carrier), context, carrier) + } + if (extractedContext._traceId && extractedContext._spanId && + extractedContext.toTraceId(true) !== context.toTraceId(true)) { + const link = { + context: extractedContext, + attributes: { reason: 'terminated_context', context_headers: extractor } + } + context._links.push(link) + } + } + } + + if (this._hasPropagationStyle('extract', 'baggage') && carrier.baggage) { + context = context || new DatadogSpanContext() + this._extractBaggageItems(carrier, context) } - return spanContext || this._extractSqsdContext(carrier) + return context || this._extractSqsdContext(carrier) } _extractDatadogContext (carrier) { @@ -483,7 +499,7 @@ class TextMapPropagator { } _extractGenericContext (carrier, traceKey, spanKey, radix) { - if (carrier[traceKey] && carrier[spanKey]) { + if (carrier && carrier[traceKey] && carrier[spanKey]) { if (invalidSegment.test(carrier[traceKey])) return null return new DatadogSpanContext({ diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index 5a50166aa49..00fd51da027 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -180,6 +180,20 @@ class DatadogSpan { }) } + addSpanPointer (ptrKind, ptrDir, ptrHash) { + const zeroContext = new SpanContext({ + traceId: id('0'), + spanId: id('0') + }) + const attributes = { + 'ptr.kind': ptrKind, + 'ptr.dir': ptrDir, + 'ptr.hash': ptrHash, + 'link.kind': 'span-pointer' + } + this.addLink(zeroContext, attributes) + } + addEvent (name, attributesOrStartTime, startTime) { const event = { name } if (attributesOrStartTime) { @@ -200,7 +214,7 @@ class DatadogSpan { if (DD_TRACE_EXPERIMENTAL_STATE_TRACKING === 'true') { if (!this._spanContext._tags['service.name']) { - log.error(`Finishing invalid span: ${this}`) + log.error('Finishing invalid span: %s', this) } } diff --git a/packages/dd-trace/src/opentracing/span_context.js b/packages/dd-trace/src/opentracing/span_context.js index 207c97080bb..223348bfd55 100644 --- a/packages/dd-trace/src/opentracing/span_context.js +++ b/packages/dd-trace/src/opentracing/span_context.js @@ -18,6 +18,7 @@ class DatadogSpanContext { this._tags = props.tags || {} this._sampling = props.sampling || {} this._spanSampling = undefined + this._links = props.links || [] this._baggageItems = props.baggageItems || {} this._traceparent = props.traceparent this._tracestate = props.tracestate diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index 2d854442cc3..4ae30ca93ac 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -91,7 +91,7 @@ class DatadogTracer { } this._propagators[format].inject(context, carrier) } catch (e) { - log.error(e) + log.error('Error injecting trace', e) runtimeMetrics.increment('datadog.tracer.node.inject.errors', true) } } @@ -100,7 +100,7 @@ class DatadogTracer { try { return this._propagators[format].extract(carrier) } catch (e) { - log.error(e) + log.error('Error extracting trace', e) runtimeMetrics.increment('datadog.tracer.node.extract.errors', true) return null } diff --git a/packages/dd-trace/src/plugin_manager.js b/packages/dd-trace/src/plugin_manager.js index e9daea9b60b..74cc656048b 100644 --- a/packages/dd-trace/src/plugin_manager.js +++ b/packages/dd-trace/src/plugin_manager.js @@ -138,7 +138,8 @@ module.exports = class PluginManager { clientIpEnabled, memcachedCommandEnabled, ciVisibilityTestSessionName, - ciVisAgentlessLogSubmissionEnabled + ciVisAgentlessLogSubmissionEnabled, + isTestDynamicInstrumentationEnabled } = this._tracerConfig const sharedConfig = { @@ -149,7 +150,8 @@ module.exports = class PluginManager { url, headers: headerTags || [], ciVisibilityTestSessionName, - ciVisAgentlessLogSubmissionEnabled + ciVisAgentlessLogSubmissionEnabled, + isTestDynamicInstrumentationEnabled } if (logInjection !== undefined) { diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index d4c9f32bc68..a2f8948bf49 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -21,7 +21,9 @@ const { ITR_CORRELATION_ID, TEST_SOURCE_FILE, TEST_LEVEL_EVENT_TYPES, - TEST_SUITE + TEST_SUITE, + getFileAndLineNumberFromError, + getTestSuitePath } = require('./util/test') const Plugin = require('./plugin') const { COMPONENT } = require('../constants') @@ -47,7 +49,7 @@ module.exports = class CiPlugin extends Plugin { } this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => { if (err) { - log.error(`Library configuration could not be fetched. ${err.message}`) + log.error('Library configuration could not be fetched. %s', err.message) } else { this.libraryConfig = libraryConfig } @@ -61,7 +63,7 @@ module.exports = class CiPlugin extends Plugin { } this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => { if (err) { - log.error(`Skippable suites could not be fetched. ${err.message}`) + log.error('Skippable suites could not be fetched. %s', err.message) } else { this.itrCorrelationId = itrCorrelationId } @@ -150,7 +152,7 @@ module.exports = class CiPlugin extends Plugin { } this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => { if (err) { - log.error(`Known tests could not be fetched. ${err.message}`) + log.error('Known tests could not be fetched. %s', err.message) this.libraryConfig.isEarlyFlakeDetectionEnabled = false } onDone({ err, knownTests }) @@ -180,6 +182,12 @@ module.exports = class CiPlugin extends Plugin { configure (config) { super.configure(config) + + if (config.isTestDynamicInstrumentationEnabled) { + const testVisibilityDynamicInstrumentation = require('../ci-visibility/dynamic-instrumentation') + this.di = testVisibilityDynamicInstrumentation + } + this.testEnvironmentMetadata = getTestEnvironmentMetadata(this.constructor.id, this.config) const { @@ -283,4 +291,39 @@ module.exports = class CiPlugin extends Plugin { return testSpan } + + // TODO: If the test finishes and the probe is not hit, we should remove the breakpoint + addDiProbe (err, probe) { + const [file, line] = getFileAndLineNumberFromError(err) + + const relativePath = getTestSuitePath(file, this.repositoryRoot) + + const [ + snapshotId, + setProbePromise, + hitProbePromise + ] = this.di.addLineProbe({ file: relativePath, line }) + + if (probe) { // not all frameworks may sync with the set probe promise + probe.setProbePromise = setProbePromise + } + + hitProbePromise.then(({ snapshot }) => { + // TODO: handle race conditions for this.retriedTestIds + const { traceId, spanId } = this.retriedTestIds + this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, { + debugger: { snapshot }, + dd: { + trace_id: traceId, + span_id: spanId + } + }) + }) + + return { + snapshotId, + file: relativePath, + line + } + } } diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 80c32401536..3e77226a119 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -15,6 +15,8 @@ module.exports = { get '@jest/test-sequencer' () { return require('../../../datadog-plugin-jest/src') }, get '@jest/transform' () { return require('../../../datadog-plugin-jest/src') }, get '@koa/router' () { return require('../../../datadog-plugin-koa/src') }, + get '@langchain/core' () { return require('../../../datadog-plugin-langchain/src') }, + get '@langchain/openai' () { return require('../../../datadog-plugin-langchain/src') }, get '@node-redis/client' () { return require('../../../datadog-plugin-redis/src') }, get '@opensearch-project/opensearch' () { return require('../../../datadog-plugin-opensearch/src') }, get '@redis/client' () { return require('../../../datadog-plugin-redis/src') }, @@ -52,6 +54,7 @@ module.exports = { get koa () { return require('../../../datadog-plugin-koa/src') }, get 'koa-router' () { return require('../../../datadog-plugin-koa/src') }, get kafkajs () { return require('../../../datadog-plugin-kafkajs/src') }, + get langchain () { return require('../../../datadog-plugin-langchain/src') }, get mariadb () { return require('../../../datadog-plugin-mariadb/src') }, get memcached () { return require('../../../datadog-plugin-memcached/src') }, get 'microgateway-core' () { return require('../../../datadog-plugin-microgateway-core/src') }, diff --git a/packages/dd-trace/src/plugins/plugin.js b/packages/dd-trace/src/plugins/plugin.js index 78a49b62b14..e8d9c911a69 100644 --- a/packages/dd-trace/src/plugins/plugin.js +++ b/packages/dd-trace/src/plugins/plugin.js @@ -79,7 +79,7 @@ module.exports = class Plugin { return handler.apply(this, arguments) } catch (e) { logger.error('Error in plugin handler:', e) - logger.info('Disabling plugin:', plugin.id) + logger.info('Disabling plugin: %s', plugin.id) plugin.configure(false) } } diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index d2d487a4a6f..e384b8cb7a7 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -94,7 +94,7 @@ class TracingPlugin extends Plugin { } addError (error, span = this.activeSpan) { - if (!span._spanContext._tags.error) { + if (span && !span._spanContext._tags.error) { // Errors may be wrapped in a context. error = (error && error.error) || error span.setTag('error', error || 1) @@ -103,7 +103,6 @@ class TracingPlugin extends Plugin { startSpan (name, { childOf, kind, meta, metrics, service, resource, type } = {}, enter = true) { const store = storage.getStore() - if (store && childOf === undefined) { childOf = store.span } @@ -119,7 +118,8 @@ class TracingPlugin extends Plugin { ...meta, ...metrics }, - integrationName: type + integrationName: type, + links: childOf?._links }) analyticsSampler.sample(span, this.config.measured) diff --git a/packages/dd-trace/src/plugins/util/git.js b/packages/dd-trace/src/plugins/util/git.js index 06b9521817f..47707a48679 100644 --- a/packages/dd-trace/src/plugins/util/git.js +++ b/packages/dd-trace/src/plugins/util/git.js @@ -61,7 +61,7 @@ function sanitizedExec ( exitCode: err.status || err.errno }) } - log.error(err) + log.error('Git plugin error executing command', err) return '' } finally { storage.enterWith(store) @@ -144,7 +144,7 @@ function unshallowRepository () { ], { stdio: 'pipe' }) } catch (err) { // If the local HEAD is a commit that has not been pushed to the remote, the above command will fail. - log.error(err) + log.error('Git plugin error executing git command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno } @@ -157,7 +157,7 @@ function unshallowRepository () { ], { stdio: 'pipe' }) } catch (err) { // If the CI is working on a detached HEAD or branch tracking hasn’t been set up, the above command will fail. - log.error(err) + log.error('Git plugin error executing fallback git command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno } @@ -196,7 +196,7 @@ function getLatestCommits () { distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_local_commits' }, Date.now() - startTime) return result } catch (err) { - log.error(`Get latest commits failed: ${err.message}`) + log.error('Get latest commits failed: %s', err.message) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_local_commits', errorType: err.status } @@ -229,7 +229,7 @@ function getCommitsRevList (commitsToExclude, commitsToInclude) { .split('\n') .filter(commit => commit) } catch (err) { - log.error(`Get commits to upload failed: ${err.message}`) + log.error('Get commits to upload failed: %s', err.message) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_objects', errorType: err.code, exitCode: err.status || err.errno } // err.status might be null @@ -272,7 +272,7 @@ function generatePackFilesForCommits (commitsToUpload) { try { result = execGitPackObjects(temporaryPath) } catch (err) { - log.error(err) + log.error('Git plugin error executing git pack-objects command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code } @@ -292,7 +292,7 @@ function generatePackFilesForCommits (commitsToUpload) { try { result = execGitPackObjects(cwdPath) } catch (err) { - log.error(err) + log.error('Git plugin error executing fallback git pack-objects command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code } diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 6c0dde70cfb..633b1f14361 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -106,6 +106,13 @@ const TEST_LEVEL_EVENT_TYPES = [ 'test_session_end' ] +// Dynamic instrumentation - Test optimization integration tags +const DI_ERROR_DEBUG_INFO_CAPTURED = 'error.debug_info_captured' +// TODO: for the moment we'll only use a single snapshot id, so `0` is hardcoded +const DI_DEBUG_ERROR_SNAPSHOT_ID = '_dd.debug.error.0.snapshot_id' +const DI_DEBUG_ERROR_FILE = '_dd.debug.error.0.file' +const DI_DEBUG_ERROR_LINE = '_dd.debug.error.0.line' + module.exports = { TEST_CODE_OWNERS, TEST_SESSION_NAME, @@ -181,7 +188,12 @@ module.exports = { TEST_BROWSER_VERSION, getTestSessionName, TEST_LEVEL_EVENT_TYPES, - getNumFromKnownTests + getNumFromKnownTests, + getFileAndLineNumberFromError, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19 @@ -206,13 +218,13 @@ function removeInvalidMetadata (metadata) { return Object.keys(metadata).reduce((filteredTags, tag) => { if (tag === GIT_REPOSITORY_URL) { if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) { - log.error(`Repository URL is not a valid repository URL: ${metadata[GIT_REPOSITORY_URL]}.`) + log.error('Repository URL is not a valid repository URL: %s.', metadata[GIT_REPOSITORY_URL]) return filteredTags } } if (tag === GIT_COMMIT_SHA) { if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) { - log.error(`Git commit SHA must be a full-length git SHA: ${metadata[GIT_COMMIT_SHA]}.`) + log.error('Git commit SHA must be a full-length git SHA: %s.', metadata[GIT_COMMIT_SHA]) return filteredTags } } @@ -637,3 +649,24 @@ function getNumFromKnownTests (knownTests) { return totalNumTests } + +function getFileAndLineNumberFromError (error) { + // Split the stack trace into individual lines + const stackLines = error.stack.split('\n') + + // The top frame is usually the second line + const topFrame = stackLines[1] + + // Regular expression to match the file path, line number, and column number + const regex = /\s*at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/ + const match = topFrame.match(regex) + + if (match) { + const filePath = match[1] + const lineNumber = Number(match[2]) + const columnNumber = Number(match[3]) + + return [filePath, lineNumber, columnNumber] + } + return [] +} diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index 374490c3bf0..2d92c74ea91 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -267,7 +267,7 @@ const web = { } } - const span = tracer.startSpan(name, { childOf }) + const span = tracer.startSpan(name, { childOf, links: childOf?._links }) return span }, @@ -546,7 +546,7 @@ function getHeadersToRecord (config) { .map(h => h.split(':')) .map(([key, tag]) => [key.toLowerCase(), tag]) } catch (err) { - log.error(err) + log.error('Web plugin error getting headers', err) } } else if (config.hasOwnProperty('headers')) { log.error('Expected `headers` to be an array of strings.') @@ -595,7 +595,7 @@ function getQsObfuscator (config) { try { return new RegExp(obfuscator, 'gi') } catch (err) { - log.error(err) + log.error('Web plugin error getting qs obfuscator', err) } } diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 3c360d65f7a..4e7863dce3a 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -21,6 +21,7 @@ class Config { const { DD_AGENT_HOST, DD_ENV, + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing DD_PROFILING_CODEHOTSPOTS_ENABLED, DD_PROFILING_CPU_ENABLED, DD_PROFILING_DEBUG_SOURCE_MAPS, @@ -175,6 +176,8 @@ class Config { DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, samplingContextsAvailable)) logExperimentalVarDeprecation('TIMELINE_ENABLED') checkOptionWithSamplingContextAllowed(this.timelineEnabled, 'Timeline view') + this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled, + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true)) this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled, DD_PROFILING_CODEHOTSPOTS_ENABLED, diff --git a/packages/dd-trace/src/profiling/exporters/agent.js b/packages/dd-trace/src/profiling/exporters/agent.js index 01363d6d2c5..6ad63486a87 100644 --- a/packages/dd-trace/src/profiling/exporters/agent.js +++ b/packages/dd-trace/src/profiling/exporters/agent.js @@ -3,25 +3,52 @@ const retry = require('retry') const { request: httpRequest } = require('http') const { request: httpsRequest } = require('https') +const { EventSerializer } = require('./event_serializer') // 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 os = require('os') const { urlToHttpOptions } = require('url') const perf = require('perf_hooks').performance +const telemetryMetrics = require('../../telemetry/metrics') +const profilersNamespace = telemetryMetrics.manager.namespace('profilers') + const containerId = docker.id() +const statusCodeCounters = [] +const requestCounter = profilersNamespace.count('profile_api.requests', []) +const sizeDistribution = profilersNamespace.distribution('profile_api.bytes', []) +const durationDistribution = profilersNamespace.distribution('profile_api.ms', []) +const statusCodeErrorCounter = profilersNamespace.count('profile_api.errors', ['type:status_code']) +const networkErrorCounter = profilersNamespace.count('profile_api.errors', ['type:network']) +// TODO: implement timeout error counter when we have a way to track timeouts +// const timeoutErrorCounter = profilersNamespace.count('profile_api.errors', ['type:timeout']) + +function countStatusCode (statusCode) { + let counter = statusCodeCounters[statusCode] + if (counter === undefined) { + counter = statusCodeCounters[statusCode] = profilersNamespace.count( + 'profile_api.responses', [`status_code:${statusCode}`] + ) + } + counter.inc() +} + function sendRequest (options, form, callback) { const request = options.protocol === 'https:' ? httpsRequest : httpRequest const store = storage.getStore() storage.enterWith({ noop: true }) + requestCounter.inc() + const start = perf.now() const req = request(options, res => { + durationDistribution.track(perf.now() - start) + countStatusCode(res.statusCode) if (res.statusCode >= 400) { + statusCodeErrorCounter.inc() const error = new Error(`HTTP Error ${res.statusCode}`) error.status = res.statusCode callback(error) @@ -29,14 +56,24 @@ function sendRequest (options, form, callback) { callback(null, res) } }) - req.on('error', callback) - if (form) form.pipe(req) + + req.on('error', (err) => { + networkErrorCounter.inc() + callback(err) + }) + if (form) { + sizeDistribution.track(form.size()) + form.pipe(req) + } storage.enterWith(store) } function getBody (stream, callback) { const chunks = [] - stream.on('error', callback) + stream.on('error', (err) => { + networkErrorCounter.inc() + callback(err) + }) stream.on('data', chunk => chunks.push(chunk)) stream.on('end', () => { callback(null, Buffer.concat(chunks)) @@ -52,8 +89,10 @@ function computeRetries (uploadTimeout) { return [tries, Math.floor(uploadTimeout)] } -class AgentExporter { - constructor ({ url, logger, uploadTimeout, env, host, service, version, libraryInjected, activation } = {}) { +class AgentExporter extends EventSerializer { + constructor (config = {}) { + super(config) + const { url, logger, uploadTimeout } = config this._url = url this._logger = logger @@ -61,74 +100,13 @@ class AgentExporter { this._backoffTime = backoffTime this._backoffTries = backoffTries - this._env = env - this._host = host - this._service = service - this._appVersion = version - this._libraryInjected = !!libraryInjected - this._activation = activation || 'unknown' } - export ({ profiles, start, end, tags }) { + export (exportSpec) { + const { profiles } = exportSpec const fields = [] - function typeToFile (type) { - return `${type}.pprof` - } - - const event = JSON.stringify({ - attachments: Object.keys(profiles).map(typeToFile), - start: start.toISOString(), - end: end.toISOString(), - family: 'node', - version: '4', - tags_profiler: [ - 'language:javascript', - 'runtime:nodejs', - `runtime_arch:${process.arch}`, - `runtime_os:${process.platform}`, - `runtime_version:${process.version}`, - `process_id:${process.pid}`, - `profiler_version:${version}`, - 'format:pprof', - ...Object.entries(tags).map(([key, value]) => `${key}:${value}`) - ].join(','), - info: { - application: { - env: this._env, - service: this._service, - start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(), - version: this._appVersion - }, - platform: { - hostname: this._host, - kernel_name: os.type(), - kernel_release: os.release(), - kernel_version: os.version() - }, - profiler: { - activation: this._activation, - ssi: { - mechanism: this._libraryInjected ? 'injected_agent' : 'none' - }, - version - }, - runtime: { - // Using `nodejs` for consistency with the existing `runtime` tag. - // Note that the event `family` property uses `node`, as that's what's - // proscribed by the Intake API, but that's an internal enum and is - // not customer visible. - engine: 'nodejs', - // strip off leading 'v'. This makes the format consistent with other - // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag. - // We'll keep it like this as we want cross-engine consistency. We - // also aren't changing the format of the existing tag as we don't want - // to break it. - version: process.version.substring(1) - } - } - }) - + const event = this.getEventJSON(exportSpec) fields.push(['event', event, { filename: 'event.json', contentType: 'application/json' @@ -144,7 +122,7 @@ class AgentExporter { return `Adding ${type} profile to agent export: ` + bytes }) - const filename = typeToFile(type) + const filename = this.typeToFile(type) fields.push([filename, buffer, { filename, contentType: 'application/octet-stream' @@ -198,7 +176,7 @@ class AgentExporter { if (err) { const { status } = err if ((typeof status !== 'number' || status >= 500 || status === 429) && operation.retry(err)) { - this._logger.error(`Error from the agent: ${err.message}`) + this._logger.warn(`Error from the agent: ${err.message}`) } else { reject(err) } @@ -207,7 +185,7 @@ class AgentExporter { getBody(response, (err, body) => { if (err) { - this._logger.error(`Error reading agent response: ${err.message}`) + this._logger.warn(`Error reading agent response: ${err.message}`) } else { this._logger.debug(() => { const bytes = (body.toString('hex').match(/../g) || []).join(' ') diff --git a/packages/dd-trace/src/profiling/exporters/event_serializer.js b/packages/dd-trace/src/profiling/exporters/event_serializer.js new file mode 100644 index 00000000000..1bd16ea21bc --- /dev/null +++ b/packages/dd-trace/src/profiling/exporters/event_serializer.js @@ -0,0 +1,76 @@ +const os = require('os') +const perf = require('perf_hooks').performance +const version = require('../../../../../package.json').version + +class EventSerializer { + constructor ({ env, host, service, version, libraryInjected, activation } = {}) { + this._env = env + this._host = host + this._service = service + this._appVersion = version + this._libraryInjected = !!libraryInjected + this._activation = activation || 'unknown' + } + + typeToFile (type) { + return `${type}.pprof` + } + + getEventJSON ({ profiles, start, end, tags = {}, endpointCounts }) { + return JSON.stringify({ + attachments: Object.keys(profiles).map(t => this.typeToFile(t)), + start: start.toISOString(), + end: end.toISOString(), + family: 'node', + version: '4', + tags_profiler: [ + 'language:javascript', + 'runtime:nodejs', + `runtime_arch:${process.arch}`, + `runtime_os:${process.platform}`, + `runtime_version:${process.version}`, + `process_id:${process.pid}`, + `profiler_version:${version}`, + 'format:pprof', + ...Object.entries(tags).map(([key, value]) => `${key}:${value}`) + ].join(','), + endpoint_counts: endpointCounts, + info: { + application: { + env: this._env, + service: this._service, + start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(), + version: this._appVersion + }, + platform: { + hostname: this._host, + kernel_name: os.type(), + kernel_release: os.release(), + kernel_version: os.version() + }, + profiler: { + activation: this._activation, + ssi: { + mechanism: this._libraryInjected ? 'injected_agent' : 'none' + }, + version + }, + runtime: { + // Using `nodejs` for consistency with the existing `runtime` tag. + // Note that the event `family` property uses `node`, as that's what's + // proscribed by the Intake API, but that's an internal enum and is + // not customer visible. + engine: 'nodejs', + // strip off leading 'v'. This makes the format consistent with other + // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag. + // We'll keep it like this as we want cross-engine consistency. We + // also aren't changing the format of the existing tag as we don't want + // to break it. + version: process.version.substring(1) + } + } + }) + } +} + +module.exports = { EventSerializer } diff --git a/packages/dd-trace/src/profiling/exporters/file.js b/packages/dd-trace/src/profiling/exporters/file.js index 724eac4656b..a7b87b2025d 100644 --- a/packages/dd-trace/src/profiling/exporters/file.js +++ b/packages/dd-trace/src/profiling/exporters/file.js @@ -4,6 +4,7 @@ const fs = require('fs') const { promisify } = require('util') const { threadId } = require('worker_threads') const writeFile = promisify(fs.writeFile) +const { EventSerializer } = require('./event_serializer') function formatDateTime (t) { const pad = (n) => String(n).padStart(2, '0') @@ -11,18 +12,21 @@ function formatDateTime (t) { `T${pad(t.getUTCHours())}${pad(t.getUTCMinutes())}${pad(t.getUTCSeconds())}Z` } -class FileExporter { - constructor ({ pprofPrefix } = {}) { +class FileExporter extends EventSerializer { + constructor (config = {}) { + super(config) + const { pprofPrefix } = config this._pprofPrefix = pprofPrefix || '' } - export ({ profiles, end }) { + export (exportSpec) { + const { profiles, end } = exportSpec const types = Object.keys(profiles) const dateStr = formatDateTime(end) const tasks = types.map(type => { return writeFile(`${this._pprofPrefix}${type}_worker_${threadId}_${dateStr}.pprof`, profiles[type]) }) - + tasks.push(writeFile(`event_worker_${threadId}_${dateStr}.json`, this.getEventJSON(exportSpec))) return Promise.all(tasks) } } diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 3e6c5d7f618..d02912dde42 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -4,9 +4,11 @@ const { EventEmitter } = require('events') 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 profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted') +const spanFinishedChannel = dc.channel('dd-trace:span:finish') function maybeSourceMap (sourceMap, SourceMapper, debug) { if (!sourceMap) return @@ -21,6 +23,20 @@ function logError (logger, err) { } } +function findWebSpan (startedSpans, spanId) { + for (let i = startedSpans.length; --i >= 0;) { + const ispan = startedSpans[i] + const context = ispan.context() + if (context._spanId === spanId) { + if (isWebServerSpan(context._tags)) { + return true + } + spanId = context._parentId + } + } + return false +} + class Profiler extends EventEmitter { constructor () { super() @@ -30,6 +46,7 @@ class Profiler extends EventEmitter { this._timer = undefined this._lastStart = undefined this._timeoutInterval = undefined + this.endpointCounts = new Map() } start (options) { @@ -82,6 +99,11 @@ class Profiler extends EventEmitter { this._logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`) } + if (config.endpointCollectionEnabled) { + this._spanFinishListener = this._onSpanFinish.bind(this) + spanFinishedChannel.subscribe(this._spanFinishListener) + } + this._capture(this._timeoutInterval, start) return true } catch (e) { @@ -117,6 +139,11 @@ class Profiler extends EventEmitter { this._enabled = false + if (this._spanFinishListener !== undefined) { + spanFinishedChannel.unsubscribe(this._spanFinishListener) + this._spanFinishListener = undefined + } + for (const profiler of this._config.profilers) { profiler.stop() this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`) @@ -137,6 +164,26 @@ class Profiler extends EventEmitter { } } + _onSpanFinish (span) { + const context = span.context() + const tags = context._tags + if (!isWebServerSpan(tags)) return + + const endpointName = endpointNameFromTags(tags) + if (!endpointName) return + + // Make sure this is the outermost web span, just in case so we don't overcount + if (findWebSpan(getStartedSpans(context), context._parentId)) return + + let counter = this.endpointCounts.get(endpointName) + if (counter === undefined) { + counter = { count: 1 } + this.endpointCounts.set(endpointName, counter) + } else { + counter.count++ + } + } + async _collect (snapshotKind, restart = true) { if (!this._enabled) return @@ -194,15 +241,23 @@ class Profiler extends EventEmitter { _submit (profiles, start, end, snapshotKind) { const { tags } = this._config - const tasks = [] - tags.snapshot = snapshotKind - for (const exporter of this._config.exporters) { - const task = exporter.export({ profiles, start, end, tags }) - .catch(err => this._logError(err)) - - tasks.push(task) + // Flatten endpoint counts + const endpointCounts = {} + for (const [endpoint, { count }] of this.endpointCounts) { + endpointCounts[endpoint] = count } + this.endpointCounts.clear() + + tags.snapshot = snapshotKind + const exportSpec = { profiles, start, end, tags, endpointCounts } + const tasks = this._config.exporters.map(exporter => + exporter.export(exportSpec).catch(err => { + if (this._logger) { + this._logger.warn(err) + } + }) + ) return Promise.all(tasks) } 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 f47a3468f78..48e430ba607 100644 --- a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js @@ -1,14 +1,15 @@ -const { AsyncLocalStorage } = require('async_hooks') +const { storage } = require('../../../../../datadog-core') const TracingPlugin = require('../../../plugins/tracing') const { performance } = require('perf_hooks') // We are leveraging the TracingPlugin class for its functionality to bind // start/error/finish methods to the appropriate diagnostic channels. class EventPlugin extends TracingPlugin { - constructor (eventHandler) { + constructor (eventHandler, eventFilter) { super() this.eventHandler = eventHandler - this.store = new AsyncLocalStorage() + this.eventFilter = eventFilter + this.store = storage('profiling') this.entryType = this.constructor.entryType } @@ -20,27 +21,36 @@ class EventPlugin extends TracingPlugin { } error () { - this.store.getStore().error = true + const store = this.store.getStore() + if (store) { + store.error = true + } } finish () { - const { startEvent, startTime, error } = this.store.getStore() + const store = this.store.getStore() + if (!store) return + + const { startEvent, startTime, error } = store if (error) { return // don't emit perf events for failed operations } const duration = performance.now() - startTime - const context = this.activeSpan?.context() - const _ddSpanId = context?.toSpanId() - const _ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || _ddSpanId - const event = { entryType: this.entryType, startTime, - duration, - _ddSpanId, - _ddRootSpanId + duration } + + if (!this.eventFilter(event)) { + return + } + + const context = this.activeSpan?.context() + event._ddSpanId = context?.toSpanId() + event._ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || event._ddSpanId + this.eventHandler(this.extendEvent(event, startEvent)) } } diff --git a/packages/dd-trace/src/profiling/profilers/events.js b/packages/dd-trace/src/profiling/profilers/events.js index f8f43b06a9a..2200eaadd2e 100644 --- a/packages/dd-trace/src/profiling/profilers/events.js +++ b/packages/dd-trace/src/profiling/profilers/events.js @@ -254,10 +254,10 @@ class NodeApiEventSource { } class DatadogInstrumentationEventSource { - constructor (eventHandler) { + constructor (eventHandler, eventFilter) { this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => { const Plugin = require(`./event_plugins/${m}`) - return new Plugin(eventHandler) + return new Plugin(eventHandler, eventFilter) }) this.started = false @@ -292,29 +292,68 @@ class CompositeEventSource { } } +function createPossionProcessSamplingFilter (samplingIntervalMillis) { + let nextSamplingInstant = performance.now() + let currentSamplingInstant = 0 + setNextSamplingInstant() + + return event => { + const endTime = event.startTime + event.duration + while (endTime >= nextSamplingInstant) { + setNextSamplingInstant() + } + // An event is sampled if it started before, and ended on or after a sampling instant. The above + // while loop will ensure that the ending invariant is always true for the current sampling + // instant so we don't have to test for it below. Across calls, the invariant also holds as long + // as the events arrive in endTime order. This is true for events coming from + // DatadogInstrumentationEventSource; they will be ordered by endTime by virtue of this method + // being invoked synchronously with the plugins' finish() handler which evaluates + // performance.now(). OTOH, events coming from NodeAPIEventSource (GC in typical setup) might be + // somewhat delayed as they are queued by Node, so they can arrive out of order with regard to + // events coming from the non-queued source. By omitting the endTime check, we will pass through + // some short events that started and ended before the current sampling instant. OTOH, if we + // were to check for this.currentSamplingInstant <= endTime, we would discard some long events + // that also ended before the current sampling instant. We'd rather err on the side of including + // some short events than excluding some long events. + return event.startTime < currentSamplingInstant + } + + function setNextSamplingInstant () { + currentSamplingInstant = nextSamplingInstant + nextSamplingInstant -= Math.log(1 - Math.random()) * samplingIntervalMillis + } +} + /** * This class generates pprof files with timeline events. It combines an event - * source with an event serializer. + * source with a sampling event filter and an event serializer. */ class EventsProfiler { constructor (options = {}) { this.type = 'events' this.eventSerializer = new EventSerializer() - const eventHandler = event => { - this.eventSerializer.addEvent(event) + const eventHandler = event => this.eventSerializer.addEvent(event) + const eventFilter = options.timelineSamplingEnabled + // options.samplingInterval comes in microseconds, we need millis + ? createPossionProcessSamplingFilter((options.samplingInterval ?? 1e6 / 99) / 1000) + : _ => true + const filteringEventHandler = event => { + if (eventFilter(event)) { + eventHandler(event) + } } if (options.codeHotspotsEnabled) { // Use Datadog instrumentation to collect events with span IDs. Still use // Node API for GC events. this.eventSource = new CompositeEventSource([ - new DatadogInstrumentationEventSource(eventHandler), - new NodeApiEventSource(eventHandler, ['gc']) + new DatadogInstrumentationEventSource(eventHandler, eventFilter), + new NodeApiEventSource(filteringEventHandler, ['gc']) ]) } else { // Use Node API instrumentation to collect events without span IDs - this.eventSource = new NodeApiEventSource(eventHandler) + this.eventSource = new NodeApiEventSource(filteringEventHandler) } } diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index dc3c0ba61ba..bcc7959074f 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -3,8 +3,6 @@ const { storage } = require('../../../../datadog-core') const dc = require('dc-polyfill') -const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags') -const { WEB } = require('../../../../../ext/types') const runtimeMetrics = require('../../runtime_metrics') const telemetryMetrics = require('../../telemetry/metrics') const { @@ -15,6 +13,8 @@ const { getThreadLabels } = require('./shared') +const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils') + const beforeCh = dc.channel('dd-trace:storage:before') const enterCh = dc.channel('dd-trace:storage:enter') const spanFinishCh = dc.channel('dd-trace:span:finish') @@ -29,21 +29,6 @@ function getActiveSpan () { return store && store.span } -function getStartedSpans (context) { - return context._trace.started -} - -function isWebServerSpan (tags) { - return tags[SPAN_TYPE] === WEB -} - -function endpointNameFromTags (tags) { - return tags[RESOURCE_NAME] || [ - tags[HTTP_METHOD], - tags[HTTP_ROUTE] - ].filter(v => v).join(' ') -} - let channelsActivated = false function ensureChannelsActivated () { if (channelsActivated) return diff --git a/packages/dd-trace/src/profiling/webspan-utils.js b/packages/dd-trace/src/profiling/webspan-utils.js new file mode 100644 index 00000000000..d002dcd2705 --- /dev/null +++ b/packages/dd-trace/src/profiling/webspan-utils.js @@ -0,0 +1,23 @@ +const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../ext/tags') +const { WEB } = require('../../../../ext/types') + +function isWebServerSpan (tags) { + return tags[SPAN_TYPE] === WEB +} + +function endpointNameFromTags (tags) { + return tags[RESOURCE_NAME] || [ + tags[HTTP_METHOD], + tags[HTTP_ROUTE] + ].filter(v => v).join(' ') +} + +function getStartedSpans (context) { + return context._trace.started +} + +module.exports = { + isWebServerSpan, + endpointNameFromTags, + getStartedSpans +} diff --git a/packages/dd-trace/src/proxy.js b/packages/dd-trace/src/proxy.js index 5c113399601..fd814c9d6e3 100644 --- a/packages/dd-trace/src/proxy.js +++ b/packages/dd-trace/src/proxy.js @@ -181,8 +181,13 @@ class Tracer extends NoopProxy { ) } } + + if (config.isTestDynamicInstrumentationEnabled) { + const testVisibilityDynamicInstrumentation = require('./ci-visibility/dynamic-instrumentation') + testVisibilityDynamicInstrumentation.start() + } } catch (e) { - log.error(e) + log.error('Error initialising tracer', e) } return this @@ -193,7 +198,7 @@ class Tracer extends NoopProxy { try { return require('./profiler').start(config) } catch (e) { - log.error(e) + log.error('Error starting profiler', e) } } diff --git a/packages/dd-trace/src/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics.js index b2711879a05..a9036612a67 100644 --- a/packages/dd-trace/src/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics.js @@ -7,11 +7,19 @@ const os = require('os') const { DogStatsDClient } = require('./dogstatsd') const log = require('./log') const Histogram = require('./histogram') -const { performance } = require('perf_hooks') +const { performance, PerformanceObserver } = require('perf_hooks') +const { NODE_MAJOR, NODE_MINOR } = require('../../../version') const INTERVAL = 10 * 1000 +// Node >=16 has PerformanceObserver with `gc` type, but <16.7 had a critical bug. +// See: https://github.com/nodejs/node/issues/39548 +const hasGCObserver = NODE_MAJOR >= 18 || (NODE_MAJOR === 16 && NODE_MINOR >= 7) +const hasGCProfiler = NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 15) + let nativeMetrics = null +let gcObserver = null +let gcProfiler = null let interval let client @@ -24,15 +32,20 @@ let elu reset() -module.exports = { +const runtimeMetrics = module.exports = { start (config) { const clientConfig = DogStatsDClient.generateClientConfig(config) try { nativeMetrics = require('@datadog/native-metrics') - nativeMetrics.start() + + if (hasGCObserver) { + nativeMetrics.start('loop') // Only add event loop watcher and not GC. + } else { + nativeMetrics.start() + } } catch (e) { - log.error(e) + log.error('Error starting native metrics', e) nativeMetrics = null } @@ -40,6 +53,9 @@ module.exports = { time = process.hrtime() + startGCObserver() + startGCProfiler() + if (nativeMetrics) { interval = setInterval(() => { captureCommonMetrics() @@ -138,6 +154,10 @@ function reset () { counters = {} histograms = {} nativeMetrics = null + gcObserver && gcObserver.disconnect() + gcObserver = null + gcProfiler && gcProfiler.stop() + gcProfiler = null } function captureCpuUsage () { @@ -202,6 +222,29 @@ function captureHeapSpace () { client.gauge('runtime.node.heap.physical_size.by.space', stats[i].physical_space_size, tags) } } +function captureGCMetrics () { + if (!gcProfiler) return + + const profile = gcProfiler.stop() + const pauseAll = new Histogram() + const pause = {} + + for (const stat of profile.statistics) { + const type = stat.gcType.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase() + + pause[type] = pause[type] || new Histogram() + pause[type].record(stat.cost) + pauseAll.record(stat.cost) + } + + histogram('runtime.node.gc.pause', pauseAll) + + for (const type in pause) { + histogram('runtime.node.gc.pause.by.type', pause[type], [`gc_type:${type}`]) + } + + gcProfiler.start() +} function captureGauges () { Object.keys(gauges).forEach(name => { @@ -256,6 +299,7 @@ function captureCommonMetrics () { captureCounters() captureHistograms() captureELU() + captureGCMetrics() } function captureNativeMetrics () { @@ -297,6 +341,11 @@ function captureNativeMetrics () { function histogram (name, stats, tags) { tags = [].concat(tags) + // Stats can contain garbage data when a value was never recorded. + if (stats.count === 0) { + stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0 } + } + client.gauge(`${name}.min`, stats.min, tags) client.gauge(`${name}.max`, stats.max, tags) client.increment(`${name}.sum`, stats.sum, tags) @@ -306,3 +355,57 @@ function histogram (name, stats, tags) { client.gauge(`${name}.median`, stats.median, tags) client.gauge(`${name}.95percentile`, stats.p95, tags) } + +function startGCObserver () { + if (gcObserver || hasGCProfiler || !hasGCObserver) return + + gcObserver = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + const type = gcType(entry.kind) + + runtimeMetrics.histogram('runtime.node.gc.pause.by.type', entry.duration, `gc_type:${type}`) + runtimeMetrics.histogram('runtime.node.gc.pause', entry.duration) + } + }) + + gcObserver.observe({ type: 'gc' }) +} + +function startGCProfiler () { + if (gcProfiler || !hasGCProfiler) return + + gcProfiler = new v8.GCProfiler() + gcProfiler.start() +} + +function gcType (kind) { + if (NODE_MAJOR >= 22) { + switch (kind) { + case 1: return 'scavenge' + case 2: return 'minor_mark_sweep' + case 4: return 'mark_sweep_compact' // Deprecated, might be removed soon. + case 8: return 'incremental_marking' + case 16: return 'process_weak_callbacks' + case 31: return 'all' + } + } else if (NODE_MAJOR >= 18) { + switch (kind) { + case 1: return 'scavenge' + case 2: return 'minor_mark_compact' + case 4: return 'mark_sweep_compact' + case 8: return 'incremental_marking' + case 16: return 'process_weak_callbacks' + case 31: return 'all' + } + } else { + switch (kind) { + case 1: return 'scavenge' + case 2: return 'mark_sweep_compact' + case 4: return 'incremental_marking' + case 8: return 'process_weak_callbacks' + case 15: return 'all' + } + } + + return 'unknown' +} diff --git a/packages/dd-trace/src/serverless.js b/packages/dd-trace/src/serverless.js index d352cae899e..415df38fc2c 100644 --- a/packages/dd-trace/src/serverless.js +++ b/packages/dd-trace/src/serverless.js @@ -23,7 +23,7 @@ function maybeStartServerlessMiniAgent (config) { try { require('child_process').spawn(rustBinaryPath, { stdio: 'inherit' }) } catch (err) { - log.error(`Error spawning mini agent process: ${err}`) + log.error('Error spawning mini agent process: %s', err.message) } } diff --git a/packages/dd-trace/src/service-naming/schemas/v0/serverless.js b/packages/dd-trace/src/service-naming/schemas/v0/serverless.js index fcccdcb465a..64202b11873 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/serverless.js @@ -3,7 +3,7 @@ const { identityService } = require('../util') const serverless = { server: { 'azure-functions': { - opName: () => 'azure-functions.invoke', + opName: () => 'azure.functions.invoke', serviceName: identityService } } diff --git a/packages/dd-trace/src/service-naming/schemas/v1/serverless.js b/packages/dd-trace/src/service-naming/schemas/v1/serverless.js index fcccdcb465a..64202b11873 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/serverless.js @@ -3,7 +3,7 @@ const { identityService } = require('../util') const serverless = { server: { 'azure-functions': { - opName: () => 'azure-functions.invoke', + opName: () => 'azure.functions.invoke', serviceName: identityService } } diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index deb92c02f34..46cf51b162b 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -87,22 +87,22 @@ class SpanProcessor { const id = context.toSpanId() if (finished.has(span)) { - log.error(`Span was already finished in the same trace: ${span}`) + log.error('Span was already finished in the same trace: %s', span) } else { finished.add(span) if (finishedIds.has(id)) { - log.error(`Another span with the same ID was already finished in the same trace: ${span}`) + log.error('Another span with the same ID was already finished in the same trace: %s', span) } else { finishedIds.add(id) } if (context._trace !== trace) { - log.error(`A span was finished in the wrong trace: ${span}.`) + log.error('A span was finished in the wrong trace: %s', span) } if (finishedSpans.has(span)) { - log.error(`Span was already finished in a different trace: ${span}`) + log.error('Span was already finished in a different trace: %s', span) } else { finishedSpans.add(span) } @@ -114,35 +114,35 @@ class SpanProcessor { const id = context.toSpanId() if (started.has(span)) { - log.error(`Span was already started in the same trace: ${span}`) + log.error('Span was already started in the same trace: %s', span) } else { started.add(span) if (startedIds.has(id)) { - log.error(`Another span with the same ID was already started in the same trace: ${span}`) + log.error('Another span with the same ID was already started in the same trace: %s', span) } else { startedIds.add(id) } if (context._trace !== trace) { - log.error(`A span was started in the wrong trace: ${span}.`) + log.error('A span was started in the wrong trace: %s', span) } if (startedSpans.has(span)) { - log.error(`Span was already started in a different trace: ${span}`) + log.error('Span was already started in a different trace: %s', span) } else { startedSpans.add(span) } } if (!finished.has(span)) { - log.error(`Span started in one trace but was finished in another trace: ${span}`) + log.error('Span started in one trace but was finished in another trace: %s', span) } } for (const span of trace.finished) { if (!started.has(span)) { - log.error(`Span finished in one trace but was started in another trace: ${span}`) + log.error('Span finished in one trace but was started in another trace: %s', span) } } } diff --git a/packages/dd-trace/src/tagger.js b/packages/dd-trace/src/tagger.js index 41c8616a086..bbd8a187940 100644 --- a/packages/dd-trace/src/tagger.js +++ b/packages/dd-trace/src/tagger.js @@ -44,7 +44,7 @@ function add (carrier, keyValuePairs, parseOtelTags = false) { Object.assign(carrier, keyValuePairs) } } catch (e) { - log.error(e) + log.error('Error adding tags', e) } } diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index 5df7d6fcae3..eb1fe376c67 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -137,6 +137,7 @@ function appClosing () { sendData(config, application, host, reqType, payload) // We flush before shutting down. metricsManager.send(config, application, host) + telemetryLogger.send(config, application, host) } function onBeforeExit () { diff --git a/packages/dd-trace/src/telemetry/init-telemetry.js b/packages/dd-trace/src/telemetry/init-telemetry.js deleted file mode 100644 index a126ecc6238..00000000000 --- a/packages/dd-trace/src/telemetry/init-telemetry.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' - -const fs = require('fs') -const { spawn } = require('child_process') -const tracerVersion = require('../../../../package.json').version -const log = require('../log') - -module.exports = sendTelemetry - -if (!process.env.DD_INJECTION_ENABLED) { - module.exports = () => {} -} - -if (!process.env.DD_TELEMETRY_FORWARDER_PATH) { - module.exports = () => {} -} - -if (!fs.existsSync(process.env.DD_TELEMETRY_FORWARDER_PATH)) { - module.exports = () => {} -} - -const metadata = { - language_name: 'nodejs', - language_version: process.versions.node, - runtime_name: 'nodejs', - runtime_version: process.versions.node, - tracer_version: tracerVersion, - pid: process.pid -} - -const seen = [] -function hasSeen (point) { - if (point.name === 'abort') { - // This one can only be sent once, regardless of tags - return seen.includes('abort') - } - if (point.name === 'abort.integration') { - // For now, this is the only other one we want to dedupe - const compiledPoint = point.name + point.tags.join('') - return seen.includes(compiledPoint) - } - return false -} - -function sendTelemetry (name, tags = []) { - let points = name - if (typeof name === 'string') { - points = [{ name, tags }] - } - if (['1', 'true', 'True'].includes(process.env.DD_INJECT_FORCE)) { - points = points.filter(p => ['error', 'complete'].includes(p.name)) - } - points = points.filter(p => !hasSeen(p)) - points.forEach(p => { - p.name = `library_entrypoint.${p.name}` - }) - if (points.length === 0) { - return - } - const proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], { - stdio: 'pipe' - }) - proc.on('error', () => { - log.error('Failed to spawn telemetry forwarder') - }) - proc.on('exit', (code) => { - if (code !== 0) { - log.error(`Telemetry forwarder exited with code ${code}`) - } - }) - proc.stdin.on('error', () => { - log.error('Failed to write telemetry data to telemetry forwarder') - }) - proc.stdin.end(JSON.stringify({ metadata, points })) -} diff --git a/packages/dd-trace/src/telemetry/logs/index.js b/packages/dd-trace/src/telemetry/logs/index.js index 54e7c51fa97..199b5fb7943 100644 --- a/packages/dd-trace/src/telemetry/logs/index.js +++ b/packages/dd-trace/src/telemetry/logs/index.js @@ -35,18 +35,23 @@ function onLog (log) { } function onErrorLog (msg) { - if (msg instanceof Error) { - onLog({ - level: 'ERROR', - message: msg.message, - stack_trace: msg.stack - }) - } else if (typeof msg === 'string') { - onLog({ - level: 'ERROR', - message: msg - }) + const { message, cause } = msg + if (!message && !cause) return + + const telLog = { + level: 'ERROR', + count: 1, + + // existing log.error(err) without message will be reported as 'Generic Error' + message: message ?? 'Generic Error' } + + if (cause) { + telLog.stack_trace = cause.stack + telLog.errorType = cause.constructor.name + } + + onLog(telLog) } function start (config) { diff --git a/packages/dd-trace/src/telemetry/logs/log-collector.js b/packages/dd-trace/src/telemetry/logs/log-collector.js index 182842fc4c4..a2ee9d06f4a 100644 --- a/packages/dd-trace/src/telemetry/logs/log-collector.js +++ b/packages/dd-trace/src/telemetry/logs/log-collector.js @@ -3,7 +3,7 @@ const log = require('../../log') const { calculateDDBasePath } = require('../../util') -const logs = new Map() +const logs = new Map() // hash -> log // NOTE: Is this a reasonable number? let maxEntries = 10000 @@ -47,15 +47,16 @@ function sanitize (logEntry) { .filter((line, index) => (isDDCode && index < firstIndex) || line.includes(ddBasePath)) .map(line => line.replace(ddBasePath, '')) - logEntry.stack_trace = stackLines.join(EOL) - if (logEntry.stack_trace === '') { - // If entire stack was removed, we'd just have a message saying "omitted" - // in which case we'd rather not log it at all. - return null + if (!isDDCode && logEntry.errorType && stackLines.length) { + stackLines = [`${logEntry.errorType}: redacted`, ...stackLines] } - if (!isDDCode) { - logEntry.message = 'omitted' + delete logEntry.errorType + + logEntry.stack_trace = stackLines.join(EOL) + if (logEntry.stack_trace === '' && (!logEntry.message || logEntry.message === 'Generic Error')) { + // If entire stack was removed and there is no message we'd rather not log it at all. + return null } return logEntry @@ -80,9 +81,11 @@ const logCollector = { if (!logs.has(hash)) { logs.set(hash, logEntry) return true + } else { + logs.get(hash).count++ } } catch (e) { - log.error(`Unable to add log to logCollector: ${e.message}`) + log.error('Unable to add log to logCollector: %s', e.message) } return false }, diff --git a/packages/dd-trace/src/telemetry/send-data.js b/packages/dd-trace/src/telemetry/send-data.js index 813fa427812..81406910c27 100644 --- a/packages/dd-trace/src/telemetry/send-data.js +++ b/packages/dd-trace/src/telemetry/send-data.js @@ -57,7 +57,7 @@ function sendData (config, application, host, reqType, payload = {}, cb = () => try { url = url || new URL(getAgentlessTelemetryEndpoint(config.site)) } catch (err) { - log.error(err) + log.error('Telemetry endpoint url is invalid', err) // No point to do the request if the URL is invalid return cb(err, { payload, reqType }) } @@ -100,7 +100,7 @@ function sendData (config, application, host, reqType, payload = {}, cb = () => path: '/api/v2/apmtelemetry' } if (backendUrl) { - request(data, backendOptions, (error) => { log.error(error) }) + request(data, backendOptions, (error) => { log.error('Error sending telemetry data', error) }) } else { log.error('Invalid Telemetry URL') } diff --git a/packages/dd-trace/src/util.js b/packages/dd-trace/src/util.js index 04048c9b187..8cfa3d6f58c 100644 --- a/packages/dd-trace/src/util.js +++ b/packages/dd-trace/src/util.js @@ -1,5 +1,6 @@ 'use strict' +const crypto = require('crypto') const path = require('path') function isTrue (str) { @@ -24,6 +25,8 @@ function isError (value) { // Matches a glob pattern to a given subject string function globMatch (pattern, subject) { + if (typeof pattern === 'string') pattern = pattern.toLowerCase() + if (typeof subject === 'string') subject = subject.toLowerCase() let px = 0 // [p]attern inde[x] let sx = 0 // [s]ubject inde[x] let nextPx = 0 @@ -63,6 +66,8 @@ function globMatch (pattern, subject) { return true } +// TODO: this adds stack traces relative to packages/ +// shouldn't paths be relative to the root of dd-trace? function calculateDDBasePath (dirname) { const dirSteps = dirname.split(path.sep) const packagesIndex = dirSteps.lastIndexOf('packages') @@ -73,11 +78,25 @@ function hasOwn (object, prop) { return Object.prototype.hasOwnProperty.call(object, prop) } +/** + * Generates a unique hash from an array of strings by joining them with | before hashing. + * Used to uniquely identify AWS requests for span pointers. + * @param {string[]} components - Array of strings to hash + * @returns {string} A 32-character hash uniquely identifying the components + */ +function generatePointerHash (components) { + // If passing S3's ETag as a component, make sure any quotes have already been removed! + const dataToHash = components.join('|') + const hash = crypto.createHash('sha256').update(dataToHash).digest('hex') + return hash.substring(0, 32) +} + module.exports = { isTrue, isFalse, isError, globMatch, calculateDDBasePath, - hasOwn + hasOwn, + generatePointerHash } 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 5a69af05a5c..9944f9d0871 100644 --- a/packages/dd-trace/test/appsec/api_security_sampler.spec.js +++ b/packages/dd-trace/test/appsec/api_security_sampler.spec.js @@ -1,71 +1,200 @@ 'use strict' -const apiSecuritySampler = require('../../src/appsec/api_security_sampler') +const proxyquire = require('proxyquire') +const { assert } = require('chai') +const { performance } = require('node:perf_hooks') +const { USER_KEEP, AUTO_KEEP, AUTO_REJECT, USER_REJECT } = require('../../../../ext/priority') -describe('Api Security Sampler', () => { - let config +describe('API Security Sampler', () => { + const req = { route: { path: '/test' }, method: 'GET' } + const res = { statusCode: 200 } + let apiSecuritySampler, webStub, span, clock, performanceNowStub beforeEach(() => { - config = { - apiSecurity: { - enabled: true, - requestSampling: 1 + clock = sinon.useFakeTimers({ now: 10 }) + performanceNowStub = sinon.stub(performance, 'now').callsFake(() => clock.now) + + webStub = { + root: sinon.stub(), + getContext: sinon.stub(), + _prioritySampler: { + isSampled: sinon.stub() } } - sinon.stub(Math, 'random').returns(0.3) + apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { + '../plugins/util/web': webStub + }) + + span = { + context: sinon.stub().returns({ + _sampling: { priority: AUTO_KEEP } + }) + } + + webStub.root.returns(span) + webStub.getContext.returns({ paths: ['path'] }) + }) + + afterEach(() => { + apiSecuritySampler.disable() + performanceNowStub.restore() + clock.restore() }) - afterEach(sinon.restore) + it('should return false if not enabled', () => { + apiSecuritySampler.disable() + assert.isFalse(apiSecuritySampler.sampleRequest({}, {})) + }) - describe('sampleRequest', () => { - it('should sample request if enabled and sampling 1', () => { - apiSecuritySampler.configure(config) + it('should return false if no root span', () => { + webStub.root.returns(null) + assert.isFalse(apiSecuritySampler.sampleRequest({}, {})) + }) + + it('should return false for AUTO_REJECT priority', () => { + span.context.returns({ _sampling: { priority: AUTO_REJECT } }) + assert.isFalse(apiSecuritySampler.sampleRequest(req, res)) + }) - expect(apiSecuritySampler.sampleRequest({})).to.true + it('should return false for USER_REJECT priority', () => { + span.context.returns({ _sampling: { priority: USER_REJECT } }) + assert.isFalse(apiSecuritySampler.sampleRequest(req, res)) + }) + + it('should not sample when method or statusCode is not available', () => { + assert.isFalse(apiSecuritySampler.sampleRequest(req, {}, true)) + assert.isFalse(apiSecuritySampler.sampleRequest({}, res, true)) + }) + + describe('with TTLCache', () => { + beforeEach(() => { + apiSecuritySampler.configure({ apiSecurity: { enabled: true, sampleDelay: 30 } }) }) - it('should not sample request if enabled and sampling 0', () => { - config.apiSecurity.requestSampling = 0 - apiSecuritySampler.configure(config) + it('should not sample before 30 seconds', () => { + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + + clock.tick(25000) - expect(apiSecuritySampler.sampleRequest({})).to.false + assert.isFalse(apiSecuritySampler.sampleRequest(req, res, true)) + const key = apiSecuritySampler.computeKey(req, res) + assert.isTrue(apiSecuritySampler.isSampled(key)) }) - it('should sample request if enabled and sampling greater than random', () => { - config.apiSecurity.requestSampling = 0.5 + it('should sample after 30 seconds', () => { + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) - apiSecuritySampler.configure(config) + clock.tick(35000) - expect(apiSecuritySampler.sampleRequest({})).to.true + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) }) - it('should not sample request if enabled and sampling less than random', () => { - config.apiSecurity.requestSampling = 0.1 + it('should remove oldest entry when max size is exceeded', () => { + for (let i = 0; i < 4097; i++) { + const path = `/test${i}` + webStub.getContext.returns({ paths: [path] }) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + } - apiSecuritySampler.configure(config) + webStub.getContext.returns({ paths: ['/test0'] }) + const key1 = apiSecuritySampler.computeKey(req, res) + assert.isFalse(apiSecuritySampler.isSampled(key1)) - expect(apiSecuritySampler.sampleRequest()).to.false + webStub.getContext.returns({ paths: ['/test4096'] }) + const key2 = apiSecuritySampler.computeKey(req, res) + assert.isTrue(apiSecuritySampler.isSampled(key2)) }) - it('should not sample request if incorrect config value', () => { - config.apiSecurity.requestSampling = NaN + it('should set enabled to false and clear the cache', () => { + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + + apiSecuritySampler.disable() + + assert.isFalse(apiSecuritySampler.sampleRequest(req, res, true)) + }) - apiSecuritySampler.configure(config) + it('should create different keys for different methods', () => { + const getReq = { method: 'GET' } + const postReq = { method: 'POST' } + assert.isTrue(apiSecuritySampler.sampleRequest(getReq, res, true)) + assert.isTrue(apiSecuritySampler.sampleRequest(postReq, res, true)) - expect(apiSecuritySampler.sampleRequest()).to.false + const key1 = apiSecuritySampler.computeKey(getReq, res) + assert.isTrue(apiSecuritySampler.isSampled(key1)) + const key2 = apiSecuritySampler.computeKey(postReq, res) + assert.isTrue(apiSecuritySampler.isSampled(key2)) }) - it('should sample request according to the config', () => { - config.apiSecurity.requestSampling = 1 + it('should create different keys for different status codes', () => { + const res200 = { statusCode: 200 } + const res404 = { statusCode: 404 } - apiSecuritySampler.configure(config) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res200, true)) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res404, true)) - expect(apiSecuritySampler.sampleRequest({})).to.true + const key1 = apiSecuritySampler.computeKey(req, res200) + assert.isTrue(apiSecuritySampler.isSampled(key1)) + const key2 = apiSecuritySampler.computeKey(req, res404) + assert.isTrue(apiSecuritySampler.isSampled(key2)) + }) + + it('should sample for AUTO_KEEP priority without checking prioritySampler', () => { + span.context.returns({ _sampling: { priority: AUTO_KEEP } }) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res)) + }) + + it('should sample for USER_KEEP priority without checking prioritySampler', () => { + span.context.returns({ _sampling: { priority: USER_KEEP } }) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res)) + }) + }) + + describe('with NoopTTLCache', () => { + beforeEach(() => { + apiSecuritySampler.configure({ apiSecurity: { enabled: true, sampleDelay: 0 } }) + }) + + it('should always return true for sampleRequest', () => { + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + + clock.tick(50000) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + }) + + it('should never mark requests as sampled', () => { + apiSecuritySampler.sampleRequest(req, res, true) + const key = apiSecuritySampler.computeKey(req, res) + assert.isFalse(apiSecuritySampler.isSampled(key)) + }) + + it('should handle multiple different requests', () => { + const requests = [ + { req: { method: 'GET', route: { path: '/test1' } }, res: { statusCode: 200 } }, + { req: { method: 'POST', route: { path: '/test2' } }, res: { statusCode: 201 } }, + { req: { method: 'PUT', route: { path: '/test3' } }, res: { statusCode: 204 } } + ] + + requests.forEach(({ req, res }) => { + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + const key = apiSecuritySampler.computeKey(req, res) + assert.isFalse(apiSecuritySampler.isSampled(key)) + }) + }) + + it('should not be affected by max size', () => { + for (let i = 0; i < 5000; i++) { + webStub.getContext.returns({ paths: [`/test${i}`] }) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) + } - apiSecuritySampler.setRequestSampling(0) + webStub.getContext.returns({ paths: ['/test0'] }) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) - expect(apiSecuritySampler.sampleRequest()).to.false + webStub.getContext.returns({ paths: ['/test4999'] }) + assert.isTrue(apiSecuritySampler.sampleRequest(req, res, true)) }) }) }) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js index bc7c918965c..112d634cca9 100644 --- a/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js @@ -8,72 +8,74 @@ const agent = require('../plugins/agent') const appsec = require('../../src/appsec') const Config = require('../../src/config') -describe('Attacker fingerprinting', () => { - let port, server +withVersions('express', 'express', expressVersion => { + describe('Attacker fingerprinting', () => { + let port, server - before(() => { - return agent.load(['express', 'http'], { client: false }) - }) + before(() => { + return agent.load(['express', 'http'], { client: false }) + }) - before((done) => { - const express = require('../../../../versions/express').get() - const bodyParser = require('../../../../versions/body-parser').get() + before((done) => { + const express = require(`../../../../versions/express@${expressVersion}`).get() + const bodyParser = require('../../../../versions/body-parser').get() - const app = express() - app.use(bodyParser.json()) + const app = express() + app.use(bodyParser.json()) - app.post('/', (req, res) => { - res.end('DONE') - }) + app.post('/', (req, res) => { + res.end('DONE') + }) - server = app.listen(port, () => { - port = server.address().port - done() + server = app.listen(port, () => { + port = server.address().port + done() + }) }) - }) - after(() => { - server.close() - return agent.close({ ritmReset: false }) - }) + after(() => { + server.close() + return agent.close({ ritmReset: false }) + }) - beforeEach(() => { - appsec.enable(new Config( - { - appsec: { - enabled: true, - rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') + beforeEach(() => { + appsec.enable(new Config( + { + appsec: { + enabled: true, + rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') + } } - } - )) - }) + )) + }) - afterEach(() => { - appsec.disable() - }) + afterEach(() => { + appsec.disable() + }) - it('should report http fingerprints', async () => { - await axios.post( - `http://localhost:${port}/?key=testattack`, - { - bodyParam: 'bodyValue' - }, - { - headers: { - headerName: 'headerValue', - 'x-real-ip': '255.255.255.255' + it('should report http fingerprints', async () => { + await axios.post( + `http://localhost:${port}/?key=testattack`, + { + bodyParam: 'bodyValue' + }, + { + headers: { + headerName: 'headerValue', + 'x-real-ip': '255.255.255.255' + } } - } - ) + ) - await agent.use((traces) => { - const span = traces[0][0] - assert.property(span.meta, '_dd.appsec.fp.http.header') - assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') - assert.property(span.meta, '_dd.appsec.fp.http.network') - assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') - assert.property(span.meta, '_dd.appsec.fp.http.endpoint') - assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') + await agent.use((traces) => { + const span = traces[0][0] + assert.property(span.meta, '_dd.appsec.fp.http.header') + assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') + assert.property(span.meta, '_dd.appsec.fp.http.network') + assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') + assert.property(span.meta, '_dd.appsec.fp.http.endpoint') + assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') + }) }) }) }) diff --git a/packages/dd-trace/test/appsec/blocking.spec.js b/packages/dd-trace/test/appsec/blocking.spec.js index 04a3c496b46..8a5496b4ecf 100644 --- a/packages/dd-trace/test/appsec/blocking.spec.js +++ b/packages/dd-trace/test/appsec/blocking.spec.js @@ -58,7 +58,7 @@ describe('blocking', () => { block(req, res, rootSpan) expect(log.warn).to.have.been - .calledOnceWithExactly('Cannot send blocking response when headers have already been sent') + .calledOnceWithExactly('[ASM] Cannot send blocking response when headers have already been sent') expect(rootSpan.addTags).to.not.have.been.called expect(res.setHeader).to.not.have.been.called expect(res.end).to.not.have.been.called diff --git a/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js index 4177dc78aba..64e15b9161b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/code-injection-analyzer.express.plugin.spec.js @@ -6,6 +6,10 @@ const path = require('path') const os = require('os') const fs = require('fs') const { clearCache } = require('../../../../src/appsec/iast/vulnerability-reporter') +const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') +const { storage } = require('../../../../../datadog-core') +const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') describe('Code injection vulnerability', () => { withVersions('express', 'express', '>4.18.0', version => { @@ -29,7 +33,6 @@ describe('Code injection vulnerability', () => { (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { testThatRequestHasVulnerability({ fn: (req, res) => { - // eslint-disable-next-line no-eval res.send(require(evalFunctionsPath).runEval(req.query.script, 'test-result')) }, vulnerability: 'CODE_INJECTION', @@ -42,6 +45,19 @@ describe('Code injection vulnerability', () => { } }) + testThatRequestHasVulnerability({ + fn: (req, res) => { + const source = '1 + 2' + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + res.send(require(evalFunctionsPath).runEval(str, 'test-result')) + }, + vulnerability: 'CODE_INJECTION', + testDescription: 'Should detect CODE_INJECTION vulnerability with DB source' + }) + testThatRequestHasNoVulnerability({ fn: (req, res) => { res.send('' + require(evalFunctionsPath).runFakeEval(req.query.script)) 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 fdc51ce0153..e20c83ef33d 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 @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const path = require('path') 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 59413db0a4f..c8af2de6846 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 @@ -1,14 +1,27 @@ 'use strict' const proxyquire = require('proxyquire') +const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') describe('ldap-injection-analyzer', () => { const NOT_TAINTED_QUERY = 'no vulnerable query' const TAINTED_QUERY = 'vulnerable query' const TaintTrackingMock = { - isTainted: (iastContext, string) => { + getRanges: (iastContext, string) => { return string === TAINTED_QUERY + ? [ + { + start: 0, + end: string.length, + iinfo: { + parameterName: 'param', + parameterValue: string, + type: HTTP_REQUEST_PARAMETER + } + } + ] + : [] } } diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js index e05537ce04b..f1042142100 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js @@ -9,7 +9,8 @@ const { prepareTestServerForIastInExpress } = require('../utils') const agent = require('../../../plugins/agent') describe('nosql injection detection in mongodb - whole feature', () => { - withVersions('express', 'express', '>4.18.0', expressVersion => { + // https://github.com/fiznool/express-mongo-sanitize/issues/200 + withVersions('mongodb', 'express', '>4.18.0 <5.0.0', expressVersion => { withVersions('mongodb', 'mongodb', mongodbVersion => { const mongodb = require(`../../../../../../versions/mongodb@${mongodbVersion}`) 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 f09264225a9..75337c63b3f 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 @@ -10,7 +10,7 @@ const fs = require('fs') const { NODE_MAJOR } = require('../../../../../../version') describe('nosql injection detection in mongodb - whole feature', () => { - withVersions('express', 'express', '>4.18.0', expressVersion => { + withVersions('mongoose', 'express', expressVersion => { withVersions('mongoose', 'mongoose', '>4.0.0', mongooseVersion => { const specificMongooseVersion = require(`../../../../../../versions/mongoose@${mongooseVersion}`).version() if (NODE_MAJOR === 14 && semver.satisfies(specificMongooseVersion, '>=8')) return @@ -27,11 +27,16 @@ describe('nosql injection detection in mongodb - whole feature', () => { const dbName = id().toString() mongoose = require(`../../../../../../versions/mongoose@${mongooseVersion}`).get() - mongoose.connect(`mongodb://localhost:27017/${dbName}`, { + await mongoose.connect(`mongodb://localhost:27017/${dbName}`, { useNewUrlParser: true, useUnifiedTopology: true }) + if (mongoose.models.Test) { + delete mongoose.models?.Test + delete mongoose.modelSchemas?.Test + } + Test = mongoose.model('Test', { name: String }) const src = path.join(__dirname, 'resources', vulnerableMethodFilename) @@ -46,7 +51,12 @@ describe('nosql injection detection in mongodb - whole feature', () => { }) after(() => { - fs.unlinkSync(tmpFilePath) + try { + fs.unlinkSync(tmpFilePath) + } catch (e) { + // ignore the error + } + return mongoose.disconnect() }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js index 7cf71f7a86e..a91b428211c 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js @@ -9,7 +9,8 @@ const semver = require('semver') const fs = require('fs') describe('nosql injection detection with mquery', () => { - withVersions('express', 'express', '>4.18.0', expressVersion => { + // https://github.com/fiznool/express-mongo-sanitize/issues/200 + withVersions('mongodb', 'express', '>4.18.0 <5.0.0', expressVersion => { withVersions('mongodb', 'mongodb', mongodbVersion => { const mongodb = require(`../../../../../../versions/mongodb@${mongodbVersion}`) @@ -316,7 +317,7 @@ describe('nosql injection detection with mquery', () => { withVersions('express-mongo-sanitize', 'express-mongo-sanitize', expressMongoSanitizeVersion => { prepareTestServerForIastInExpress('Test with sanitization middleware', expressVersion, (expressApp) => { const mongoSanitize = - require(`../../../../../../versions/express-mongo-sanitize@${expressMongoSanitizeVersion}`).get() + require(`../../../../../../versions/express-mongo-sanitize@${expressMongoSanitizeVersion}`).get() expressApp.use(mongoSanitize()) }, (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { testThatRequestHasNoVulnerability({ 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 6c39799f916..3fe86dacd8d 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 @@ -12,6 +12,7 @@ const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking const { prepareTestServerForIast } = require('../utils') const fs = require('fs') +const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') const iastContext = { rootSpan: { @@ -25,26 +26,23 @@ const iastContext = { } } -const TaintTrackingMock = { - isTainted: sinon.stub() +const getRanges = (ctx, val) => { + return [ + { + start: 0, + end: val.length, + iinfo: { + parameterName: 'param', + parameterValue: val, + type: HTTP_REQUEST_PARAMETER + } + } + ] } -const getIastContext = sinon.stub() -const hasQuota = sinon.stub() -const addVulnerability = sinon.stub() - -const ProxyAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/vulnerability-analyzer', { - '../iast-context': { getIastContext }, - '../overhead-controller': { hasQuota }, - '../vulnerability-reporter': { addVulnerability } -}) - -const InjectionAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/injection-analyzer', { - './vulnerability-analyzer': ProxyAnalyzer, - '../taint-tracking/operations': TaintTrackingMock -}) - describe('path-traversal-analyzer', () => { + let TaintTrackingMock, getIastContext, hasQuota, addVulnerability, ProxyAnalyzer, InjectionAnalyzer + before(() => { pathTraversalAnalyzer.enable() }) @@ -53,6 +51,28 @@ describe('path-traversal-analyzer', () => { pathTraversalAnalyzer.disable() }) + beforeEach(() => { + TaintTrackingMock = { + isTainted: sinon.stub(), + getRanges: sinon.stub() + } + + getIastContext = sinon.stub() + hasQuota = sinon.stub() + addVulnerability = sinon.stub() + + ProxyAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/vulnerability-analyzer', { + '../iast-context': { getIastContext }, + '../overhead-controller': { hasQuota }, + '../vulnerability-reporter': { addVulnerability } + }) + + InjectionAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/injection-analyzer', { + './vulnerability-analyzer': ProxyAnalyzer, + '../taint-tracking/operations': TaintTrackingMock + }) + }) + it('Analyzer should be subscribed to proper channel', () => { expect(pathTraversalAnalyzer._subscriptions).to.have.lengthOf(1) expect(pathTraversalAnalyzer._subscriptions[0]._channel.name).to.equals('apm:fs:operation:start') @@ -72,26 +92,25 @@ describe('path-traversal-analyzer', () => { }) it('if context exists but value is not a string it should not call isTainted', () => { - const isTainted = sinon.stub() + const getRanges = sinon.stub() const iastContext = {} const proxyPathAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/path-traversal-analyzer', { - '../taint-tracking': { isTainted } + '../taint-tracking': { getRanges } }) proxyPathAnalyzer._isVulnerable(undefined, iastContext) - expect(isTainted).not.to.have.been.called + expect(getRanges).not.to.have.been.called }) it('if context and value are valid it should call isTainted', () => { - // const isTainted = sinon.stub() const iastContext = {} const proxyPathAnalyzer = proxyquire('../../../../src/appsec/iast/analyzers/path-traversal-analyzer', { './injection-analyzer': InjectionAnalyzer }) - TaintTrackingMock.isTainted.returns(false) + TaintTrackingMock.getRanges.returns([]) const result = proxyPathAnalyzer._isVulnerable('test', iastContext) expect(result).to.be.false - expect(TaintTrackingMock.isTainted).to.have.been.calledOnce + expect(TaintTrackingMock.getRanges).to.have.been.calledOnce }) it('Should report proper vulnerability type', () => { @@ -102,7 +121,7 @@ describe('path-traversal-analyzer', () => { getIastContext.returns(iastContext) hasQuota.returns(true) - TaintTrackingMock.isTainted.returns(true) + TaintTrackingMock.getRanges.callsFake(getRanges) proxyPathAnalyzer.analyze(['test']) expect(addVulnerability).to.have.been.calledOnce @@ -116,9 +135,8 @@ describe('path-traversal-analyzer', () => { '../iast-context': { getIastContext: () => iastContext } }) - addVulnerability.reset() getIastContext.returns(iastContext) - TaintTrackingMock.isTainted.returns(true) + TaintTrackingMock.getRanges.callsFake(getRanges) hasQuota.returns(true) proxyPathAnalyzer.analyze(['taintedArg1', 'taintedArg2']) @@ -132,11 +150,10 @@ describe('path-traversal-analyzer', () => { '../iast-context': { getIastContext: () => iastContext } }) - addVulnerability.reset() - TaintTrackingMock.isTainted.reset() getIastContext.returns(iastContext) - TaintTrackingMock.isTainted.onFirstCall().returns(false) - TaintTrackingMock.isTainted.onSecondCall().returns(true) + + TaintTrackingMock.getRanges.onFirstCall().returns([]) + TaintTrackingMock.getRanges.onSecondCall().callsFake(getRanges) hasQuota.returns(true) proxyPathAnalyzer.analyze(['arg1', 'taintedArg2']) @@ -155,10 +172,8 @@ describe('path-traversal-analyzer', () => { return { path: mockPath, line: 3 } } - addVulnerability.reset() - TaintTrackingMock.isTainted.reset() getIastContext.returns(iastContext) - TaintTrackingMock.isTainted.returns(true) + TaintTrackingMock.getRanges.callsFake(getRanges) hasQuota.returns(true) proxyPathAnalyzer.analyze(['arg1']) 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 7716f0ae478..8c4d26103d3 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,16 +2,29 @@ const proxyquire = require('proxyquire') -const iastLog = require('../../../../src/appsec/iast/iast-log') +const log = require('../../../../src/log') const dc = require('dc-polyfill') +const { HTTP_REQUEST_PARAMETER } = require('../../../../src/appsec/iast/taint-tracking/source-types') describe('sql-injection-analyzer', () => { const NOT_TAINTED_QUERY = 'no vulnerable query' const TAINTED_QUERY = 'vulnerable query' const TaintTrackingMock = { - isTainted: (iastContext, string) => { + getRanges: (iastContext, string) => { return string === TAINTED_QUERY + ? [ + { + start: 0, + end: string.length, + iinfo: { + parameterName: 'param', + parameterValue: string, + type: HTTP_REQUEST_PARAMETER + } + } + ] + : [] } } @@ -103,11 +116,11 @@ describe('sql-injection-analyzer', () => { }) it('should not report an error when context is not initialized', () => { - sinon.stub(iastLog, 'errorAndPublish') + sinon.stub(log, 'error') sqlInjectionAnalyzer.configure(true) dc.channel('datadog:sequelize:query:finish').publish() sqlInjectionAnalyzer.configure(false) - expect(iastLog.errorAndPublish).not.to.be.called + expect(log.error).not.to.be.called }) describe('analyze', () => { diff --git a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js new file mode 100644 index 00000000000..b3398543a04 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.handlebars.plugin.spec.js @@ -0,0 +1,105 @@ +'use strict' + +const { prepareTestServerForIast } = require('../utils') +const { storage } = require('../../../../../datadog-core') +const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') +const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') + +describe('template-injection-analyzer with handlebars', () => { + withVersions('handlebars', 'handlebars', version => { + let source + before(() => { + source = '

{{name}}

' + }) + + describe('compile', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/handlebars@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', 'Request') + lib.compile(template) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compile(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.compile(source) + }, 'TEMPLATE_INJECTION') + }) + }) + + describe('precompile', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/handlebars@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', 'Request') + lib.precompile(template) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.precompile(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.precompile(source) + }, 'TEMPLATE_INJECTION') + }) + }) + + describe('registerPartial', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/handlebars@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const partial = newTaintedString(iastContext, source, 'param', 'Request') + + lib.registerPartial('vulnerablePartial', partial) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const partial = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + + lib.registerPartial('vulnerablePartial', partial) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.registerPartial('vulnerablePartial', source) + }, 'TEMPLATE_INJECTION') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js new file mode 100644 index 00000000000..574f256fd53 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/analyzers/template-injection-analyzer.pug.plugin.spec.js @@ -0,0 +1,133 @@ +'use strict' + +const { prepareTestServerForIast } = require('../utils') +const { storage } = require('../../../../../datadog-core') +const iastContextFunctions = require('../../../../src/appsec/iast/iast-context') +const { newTaintedString } = require('../../../../src/appsec/iast/taint-tracking/operations') +const { SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') + +describe('template-injection-analyzer with pug', () => { + withVersions('pug', 'pug', version => { + let source + before(() => { + source = 'string of pug' + }) + + describe('compile', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/pug@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', 'Request') + lib.compile(template) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compile(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + const template = lib.compile(source) + template() + }, 'TEMPLATE_INJECTION') + }) + }) + + describe('compileClient', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/pug@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', 'Request') + lib.compileClient(template) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compileClient(template) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.compileClient(source) + }, 'TEMPLATE_INJECTION') + }) + }) + + describe('compileClientWithDependenciesTracked', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/pug@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', 'Request') + lib.compileClientWithDependenciesTracked(template, {}) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const template = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.compileClientWithDependenciesTracked(template, {}) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.compileClient(source) + }, 'TEMPLATE_INJECTION') + }) + }) + + describe('render', () => { + prepareTestServerForIast('template injection analyzer', + (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let lib + beforeEach(() => { + lib = require(`../../../../../../versions/pug@${version}`).get() + }) + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', 'Request') + lib.render(str) + }, 'TEMPLATE_INJECTION') + + testThatRequestHasVulnerability(() => { + const store = storage.getStore() + const iastContext = iastContextFunctions.getIastContext(store) + const str = newTaintedString(iastContext, source, 'param', SQL_ROW_VALUE) + lib.render(str) + }, 'TEMPLATE_INJECTION', undefined, undefined, undefined, + 'Should detect TEMPLATE_INJECTION vulnerability with DB source') + + testThatRequestHasNoVulnerability(() => { + lib.render(source) + }, 'TEMPLATE_INJECTION') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js index 332e0c29e35..b47fb95b81b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js @@ -133,27 +133,6 @@ describe('vulnerability-analyzer', () => { ) }) - it('should wrap subscription handler and catch thrown Errors', () => { - const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE) - const handler = sinon.spy(() => { - throw new Error('handler Error') - }) - const wrapped = vulnerabilityAnalyzer._wrapHandler(handler) - - const iastContext = { - name: 'test' - } - iastContextHandler.getIastContext.returns(iastContext) - - expect(typeof wrapped).to.be.equal('function') - const message = {} - const name = 'test' - expect(() => wrapped(message, name)).to.not.throw() - const args = handler.firstCall.args - expect(args[0]).to.be.equal(message) - expect(args[1]).to.be.equal(name) - }) - it('should catch thrown Errors inside subscription handlers', () => { const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE) vulnerabilityAnalyzer.addSub({ channelName: 'dd-trace:test:error:sub' }, () => { diff --git a/packages/dd-trace/test/appsec/iast/iast-log.spec.js b/packages/dd-trace/test/appsec/iast/iast-log.spec.js deleted file mode 100644 index bd62a45e06c..00000000000 --- a/packages/dd-trace/test/appsec/iast/iast-log.spec.js +++ /dev/null @@ -1,98 +0,0 @@ -const { expect } = require('chai') -const proxyquire = require('proxyquire') - -describe('IAST log', () => { - let iastLog - let telemetryLog - let log - - beforeEach(() => { - log = { - debug: sinon.stub(), - info: sinon.stub(), - warn: sinon.stub(), - error: sinon.stub() - } - - telemetryLog = { - hasSubscribers: true, - publish: sinon.stub() - } - - iastLog = proxyquire('../../../src/appsec/iast/iast-log', { - 'dc-polyfill': { - channel: () => telemetryLog - }, - '../../log': log - }) - }) - - afterEach(() => { - sinon.reset() - }) - - describe('debug', () => { - it('should call log.debug', () => { - iastLog.debug('debug') - - expect(log.debug).to.be.calledOnceWith('debug') - }) - - it('should call log.debug and publish msg via telemetry', () => { - iastLog.debugAndPublish('debug') - - expect(log.debug).to.be.calledOnceWith('debug') - expect(telemetryLog.publish).to.be.calledOnceWith({ message: 'debug', level: 'DEBUG' }) - }) - }) - - describe('warn', () => { - it('should call log.warn', () => { - iastLog.warn('warn') - - expect(log.warn).to.be.calledOnceWith('warn') - }) - - it('should call log.warn and publish msg via telemetry', () => { - iastLog.warnAndPublish('warn') - - expect(log.warn).to.be.calledOnceWith('warn') - expect(telemetryLog.publish).to.be.calledOnceWith({ message: 'warn', level: 'WARN' }) - }) - - it('should chain multiple warn calls', () => { - iastLog.warn('warn').warnAndPublish('warnAndPublish').warn('warn2') - - expect(log.warn).to.be.calledThrice - expect(log.warn.getCall(0).args[0]).to.be.eq('warn') - expect(log.warn.getCall(1).args[0]).to.be.eq('warnAndPublish') - expect(log.warn.getCall(2).args[0]).to.be.eq('warn2') - expect(telemetryLog.publish).to.be.calledOnceWith({ message: 'warnAndPublish', level: 'WARN' }) - }) - }) - - describe('error', () => { - it('should call log.error', () => { - iastLog.error('error') - - expect(log.error).to.be.calledOnceWith('error') - }) - - it('should call log.error and publish msg via telemetry', () => { - iastLog.errorAndPublish('error') - - expect(log.error).to.be.calledOnceWith('error') - expect(telemetryLog.publish).to.not.be.called // handled by log.error() - }) - - it('should chain multiple error calls', () => { - iastLog.error('error').errorAndPublish('errorAndPublish').error('error2') - - expect(log.error).to.be.calledThrice - expect(log.error.getCall(0).args[0]).to.be.eq('error') - expect(log.error.getCall(1).args[0]).to.be.eq('errorAndPublish') - expect(log.error.getCall(2).args[0]).to.be.eq('error2') - expect(telemetryLog.publish).to.not.be.called // handled by log.error() - }) - }) -}) 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 1c3af349794..21696d3b70f 100644 --- a/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js @@ -4,6 +4,7 @@ const { expect } = require('chai') const { channel } = require('dc-polyfill') const proxyquire = require('proxyquire') const { getExecutedMetric, getInstrumentedMetric, TagKey } = require('../../../src/appsec/iast/telemetry/iast-metric') +const { IastPlugin } = require('../../../src/appsec/iast/iast-plugin') const VULNERABILITY_TYPE = TagKey.VULNERABILITY_TYPE const SOURCE_TYPE = TagKey.SOURCE_TYPE @@ -51,8 +52,8 @@ describe('IAST Plugin', () => { const iastPluginMod = proxyquire('../../../src/appsec/iast/iast-plugin', { '../../plugins/plugin': PluginClass, - './iast-log': { - errorAndPublish: logError + '../../log': { + error: logError }, './iast-context': { getIastContext @@ -71,33 +72,23 @@ describe('IAST Plugin', () => { }) describe('addSub', () => { - it('should call Plugin.addSub with channelName and wrapped handler', () => { + it('should call Plugin.addSub with channelName and handler', () => { iastPlugin.addSub('test', handler) expect(addSubMock).to.be.calledOnce const args = addSubMock.getCall(0).args expect(args[0]).equal('test') - - const wrapped = args[1] - expect(wrapped).to.be.a('function') - expect(wrapped).to.not.be.equal(handler) - expect(wrapped()).to.not.throw - expect(logError).to.be.calledOnce + expect(args[1]).to.equal(handler) }) - it('should call Plugin.addSub with channelName and wrapped handler after registering iastPluginSub', () => { + it('should call Plugin.addSub with channelName and handler after registering iastPluginSub', () => { const iastPluginSub = { channelName: 'test' } iastPlugin.addSub(iastPluginSub, handler) expect(addSubMock).to.be.calledOnce const args = addSubMock.getCall(0).args expect(args[0]).equal('test') - - const wrapped = args[1] - expect(wrapped).to.be.a('function') - expect(wrapped).to.not.be.equal(handler) - expect(wrapped()).to.not.throw - expect(logError).to.be.calledOnce + expect(args[1]).to.equal(handler) }) it('should infer moduleName from channelName after registering iastPluginSub', () => { @@ -117,20 +108,15 @@ describe('IAST Plugin', () => { }) it('should not call _getTelemetryHandler', () => { - const wrapHandler = sinon.stub() - iastPlugin._wrapHandler = wrapHandler const getTelemetryHandler = sinon.stub() iastPlugin._getTelemetryHandler = getTelemetryHandler iastPlugin.addSub({ channelName, tagKey: VULNERABILITY_TYPE }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.not.called - wrapHandler.reset() getTelemetryHandler.reset() iastPlugin.addSub({ channelName, tagKey: SOURCE_TYPE, tag: 'test-tag' }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.not.called }) }) @@ -205,8 +191,8 @@ describe('IAST Plugin', () => { } const IastPlugin = proxyquire('../../../src/appsec/iast/iast-plugin', { '../../plugins/plugin': PluginClass, - './iast-log': { - errorAndPublish: logError + '../../log': { + error: logError }, './telemetry': iastTelemetry, '../../../../datadog-instrumentations/src/helpers/instrumentations': {} @@ -235,20 +221,15 @@ describe('IAST Plugin', () => { describe('addSub', () => { it('should call _getTelemetryHandler with correct metrics', () => { - const wrapHandler = sinon.stub() - iastPlugin._wrapHandler = wrapHandler const getTelemetryHandler = sinon.stub() iastPlugin._getTelemetryHandler = getTelemetryHandler iastPlugin.addSub({ channelName, tagKey: VULNERABILITY_TYPE }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.calledOnceWith(iastPlugin.pluginSubs[0]) - wrapHandler.reset() getTelemetryHandler.reset() iastPlugin.addSub({ channelName, tagKey: SOURCE_TYPE, tag: 'test-tag' }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.calledOnceWith(iastPlugin.pluginSubs[1]) }) @@ -399,4 +380,50 @@ describe('IAST Plugin', () => { }) }) }) + + describe('Add sub to iast plugin', () => { + class BadPlugin extends IastPlugin { + static get id () { return 'badPlugin' } + + constructor () { + super() + this.addSub('appsec:badPlugin:start', this.start) + } + + start () { + throw new Error('this is one bad plugin') + } + } + class GoodPlugin extends IastPlugin { + static get id () { return 'goodPlugin' } + + constructor () { + super() + this.addSub('appsec:goodPlugin:start', this.start) + } + + start () {} + } + + const badPlugin = new BadPlugin() + const goodPlugin = new GoodPlugin() + + it('should disable bad plugin', () => { + badPlugin.configure({ enabled: true }) + expect(badPlugin._enabled).to.be.true + + channel('appsec:badPlugin:start').publish({ foo: 'bar' }) + + expect(badPlugin._enabled).to.be.false + }) + + it('should not disable good plugin', () => { + goodPlugin.configure({ enabled: true }) + expect(goodPlugin._enabled).to.be.true + + channel('appsec:goodPlugin:start').publish({ foo: 'bar' }) + + expect(goodPlugin._enabled).to.be.true + }) + }) }) 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 7bde02537d9..c5003be25ad 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -331,7 +331,8 @@ describe('Overhead controller', () => { iast: { enabled: true, requestSampling: 100, - maxConcurrentRequests: 2 + maxConcurrentRequests: 2, + deduplicationEnabled: false } } }) @@ -365,7 +366,6 @@ describe('Overhead controller', () => { } else if (url === SECOND_REQUEST) { setImmediate(() => { requestResolvers[FIRST_REQUEST]() - vulnerabilityReporter.clearCache() }) } }) @@ -373,7 +373,6 @@ describe('Overhead controller', () => { if (url === FIRST_REQUEST) { setImmediate(() => { requestResolvers[SECOND_REQUEST]() - vulnerabilityReporter.clearCache() }) } }) @@ -388,7 +387,8 @@ describe('Overhead controller', () => { iast: { enabled: true, requestSampling: 100, - maxConcurrentRequests: 2 + maxConcurrentRequests: 2, + deduplicationEnabled: false } } }) @@ -435,7 +435,6 @@ describe('Overhead controller', () => { requestResolvers[FIRST_REQUEST]() } else if (url === FIFTH_REQUEST) { requestResolvers[SECOND_REQUEST]() - vulnerabilityReporter.clearCache() } }) testRequestEventEmitter.on(TEST_REQUEST_FINISHED, (url) => { @@ -444,7 +443,6 @@ describe('Overhead controller', () => { axios.get(`http://localhost:${serverConfig.port}${FIFTH_REQUEST}`).then().catch(done) } else if (url === SECOND_REQUEST) { setImmediate(() => { - vulnerabilityReporter.clearCache() requestResolvers[THIRD_REQUEST]() requestResolvers[FOURTH_REQUEST]() requestResolvers[FIFTH_REQUEST]() 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 1a21b0a5b08..af575ce9652 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 @@ -8,14 +8,18 @@ const { HTTP_REQUEST_COOKIE_VALUE, HTTP_REQUEST_HEADER_VALUE, HTTP_REQUEST_PATH_PARAM, - HTTP_REQUEST_URI + HTTP_REQUEST_URI, + SQL_ROW_VALUE } = require('../../../../src/appsec/iast/taint-tracking/source-types') +const Config = require('../../../../src/config') const middlewareNextChannel = dc.channel('apm:express:middleware:next') -const queryParseFinishChannel = dc.channel('datadog:qs:parse:finish') +const queryReadFinishChannel = dc.channel('datadog:query:read:finish') const bodyParserFinishChannel = dc.channel('datadog:body-parser:read:finish') const cookieParseFinishCh = dc.channel('datadog:cookie:parse:finish') const processParamsStartCh = dc.channel('datadog:express:process_params:start') +const routerParamStartCh = dc.channel('datadog:router:param:start') +const sequelizeFinish = dc.channel('datadog:sequelize:query:finish') describe('IAST Taint tracking plugin', () => { let taintTrackingPlugin @@ -33,7 +37,8 @@ describe('IAST Taint tracking plugin', () => { './operations': sinon.spy(taintTrackingOperations), '../../../../../datadog-core': datadogCore }) - taintTrackingPlugin.enable() + const config = new Config() + taintTrackingPlugin.enable(config.iast) }) afterEach(() => { @@ -42,16 +47,20 @@ describe('IAST Taint tracking plugin', () => { }) it('Should subscribe to body parser, qs, cookie and process_params channel', () => { - expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(9) + expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(13) expect(taintTrackingPlugin._subscriptions[0]._channel.name).to.equals('datadog:body-parser:read:finish') expect(taintTrackingPlugin._subscriptions[1]._channel.name).to.equals('datadog:multer:read:finish') - expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('datadog:qs:parse:finish') - expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('apm:express:middleware:next') - expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('datadog:cookie:parse:finish') - expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('datadog:express:process_params:start') - expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('apm:graphql:resolve:start') - expect(taintTrackingPlugin._subscriptions[7]._channel.name).to.equals('datadog:url:parse:finish') - expect(taintTrackingPlugin._subscriptions[8]._channel.name).to.equals('datadog:url:getter:finish') + expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('datadog:query:read:finish') + expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('datadog:express:query:finish') + expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('apm:express:middleware:next') + expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('datadog:cookie:parse:finish') + expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('datadog:sequelize:query:finish') + expect(taintTrackingPlugin._subscriptions[7]._channel.name).to.equals('apm:pg:query:finish') + expect(taintTrackingPlugin._subscriptions[8]._channel.name).to.equals('datadog:express:process_params:start') + expect(taintTrackingPlugin._subscriptions[9]._channel.name).to.equals('datadog:router:param:start') + expect(taintTrackingPlugin._subscriptions[10]._channel.name).to.equals('apm:graphql:resolve:start') + expect(taintTrackingPlugin._subscriptions[11]._channel.name).to.equals('datadog:url:parse:finish') + expect(taintTrackingPlugin._subscriptions[12]._channel.name).to.equals('datadog:url:getter:finish') }) describe('taint sources', () => { @@ -136,7 +145,7 @@ describe('IAST Taint tracking plugin', () => { } } - queryParseFinishChannel.publish({ qs: req.query }) + queryReadFinishChannel.publish({ query: req.query }) expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( iastContext, @@ -209,7 +218,7 @@ describe('IAST Taint tracking plugin', () => { ) }) - it('Should taint request params when process params event is published', () => { + it('Should taint request params when process params event is published with processParamsStartCh', () => { const req = { params: { parameter1: 'tainted1' @@ -224,6 +233,21 @@ describe('IAST Taint tracking plugin', () => { ) }) + it('Should taint request params when process params event is published with routerParamStartCh', () => { + const req = { + params: { + parameter1: 'tainted1' + } + } + + routerParamStartCh.publish({ req }) + expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + iastContext, + req.params, + HTTP_REQUEST_PATH_PARAM + ) + }) + it('Should not taint request params when process params event is published with non params request', () => { const req = {} @@ -253,5 +277,259 @@ describe('IAST Taint tracking plugin', () => { HTTP_REQUEST_URI ) }) + + describe('taint database sources', () => { + it('Should not taint if config is set to 0', () => { + taintTrackingPlugin.disable() + const config = new Config() + config.dbRowsToTaint = 0 + taintTrackingPlugin.enable(config) + + const result = [ + { + id: 1, + name: 'string value 1' + }, + { + id: 2, + name: 'string value 2' + }] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.not.have.been.called + }) + + describe('with default config', () => { + it('Should taint first database row coming from sequelize', () => { + const result = [ + { + id: 1, + name: 'string value 1' + }, + { + id: 2, + name: 'string value 2' + }] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + iastContext, + 'string value 1', + '0.name', + SQL_ROW_VALUE + ) + }) + + it('Should taint whole object', () => { + const result = { id: 1, description: 'value' } + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + iastContext, + 'value', + 'description', + SQL_ROW_VALUE + ) + }) + + it('Should taint first row in nested objects', () => { + const result = [ + { + id: 1, + description: 'value', + children: [ + { + id: 11, + name: 'child1' + }, + { + id: 12, + name: 'child2' + } + ] + }, + { + id: 2, + description: 'value', + children: [ + { + id: 21, + name: 'child3' + }, + { + id: 22, + name: 'child4' + } + ] + } + ] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledTwice + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'value', + '0.description', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child1', + '0.children.0.name', + SQL_ROW_VALUE + ) + }) + }) + + describe('with config set to 2', () => { + beforeEach(() => { + taintTrackingPlugin.disable() + const config = new Config() + config.dbRowsToTaint = 2 + taintTrackingPlugin.enable(config) + }) + + it('Should taint first database row coming from sequelize', () => { + const result = [ + { + id: 1, + name: 'string value 1' + }, + { + id: 2, + name: 'string value 2' + }, + { + id: 3, + name: 'string value 2' + }] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledTwice + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'string value 1', + '0.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'string value 2', + '1.name', + SQL_ROW_VALUE + ) + }) + + it('Should taint whole object', () => { + const result = { id: 1, description: 'value' } + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.be.calledOnceWith( + iastContext, + 'value', + 'description', + SQL_ROW_VALUE + ) + }) + + it('Should taint first row in nested objects', () => { + const result = [ + { + id: 1, + description: 'value', + children: [ + { + id: 11, + name: 'child1' + }, + { + id: 12, + name: 'child2' + }, + { + id: 13, + name: 'child3' + } + ] + }, + { + id: 2, + description: 'value2', + children: [ + { + id: 21, + name: 'child4' + }, + { + id: 22, + name: 'child5' + }, + { + id: 23, + name: 'child6' + } + ] + }, + { + id: 3, + description: 'value3', + children: [ + { + id: 31, + name: 'child7' + }, + { + id: 32, + name: 'child8' + }, + { + id: 33, + name: 'child9' + } + ] + } + ] + sequelizeFinish.publish({ result }) + + expect(taintTrackingOperations.newTaintedString).to.callCount(6) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'value', + '0.description', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child1', + '0.children.0.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child2', + '0.children.1.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'value2', + '1.description', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child4', + '1.children.0.name', + SQL_ROW_VALUE + ) + expect(taintTrackingOperations.newTaintedString).to.be.calledWith( + iastContext, + 'child5', + '1.children.1.name', + SQL_ROW_VALUE + ) + }) + }) + }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.pg.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.pg.plugin.spec.js new file mode 100644 index 00000000000..69e73b0ccb0 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.pg.plugin.spec.js @@ -0,0 +1,113 @@ +'use strict' + +const { prepareTestServerForIast } = require('../../utils') + +const connectionData = { + host: '127.0.0.1', + user: 'postgres', + password: 'postgres', + database: 'postgres', + application_name: 'test' +} + +describe('db sources with pg', () => { + let pg + withVersions('pg', 'pg', '>=8.0.3', version => { + let client + beforeEach(async () => { + pg = require(`../../../../../../../versions/pg@${version}`).get() + client = new pg.Client(connectionData) + await client.connect() + + await client.query(`CREATE TABLE IF NOT EXISTS examples ( + id INT, + name VARCHAR(50), + query VARCHAR(100), + command VARCHAR(50))`) + + await client.query(`INSERT INTO examples (id, name, query, command) + VALUES (1, 'Item1', 'SELECT 1', 'ls'), + (2, 'Item2', 'SELECT 1', 'ls'), + (3, 'Item3', 'SELECT 1', 'ls')`) + }) + + afterEach(async () => { + await client.query('DROP TABLE examples') + client.end() + }) + + prepareTestServerForIast('sequelize', (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + describe('using pg.Client', () => { + testThatRequestHasVulnerability(async (req, res) => { + const result = await client.query('SELECT * FROM examples') + + const firstItem = result.rows[0] + + await client.query(firstItem.query) + + res.end() + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await client.query('SELECT * FROM examples') + + const secondItem = result.rows[1] + + await client.query(secondItem.query) + + res.end() + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await client.query('SELECT * from examples') + const firstItem = result.rows[0] + + const childProcess = require('child_process') + childProcess.execSync(firstItem.command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + + describe('using pg.Pool', () => { + let pool + + beforeEach(() => { + pool = new pg.Pool(connectionData) + }) + + testThatRequestHasVulnerability(async (req, res) => { + const result = await pool.query('SELECT * FROM examples') + + const firstItem = result.rows[0] + + await client.query(firstItem.query) + + res.end() + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await pool.query('SELECT * FROM examples') + + const secondItem = result.rows[1] + + await client.query(secondItem.query) + + res.end() + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await pool.query('SELECT * from examples') + const firstItem = result.rows[0] + + const childProcess = require('child_process') + childProcess.execSync(firstItem.command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.sequelize.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.sequelize.plugin.spec.js new file mode 100644 index 00000000000..0e1e84888c7 --- /dev/null +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/sql_row.sequelize.plugin.spec.js @@ -0,0 +1,106 @@ +'use strict' + +const { prepareTestServerForIast } = require('../../utils') + +describe('db sources with sequelize', () => { + withVersions('sequelize', 'sequelize', sequelizeVersion => { + prepareTestServerForIast('sequelize', (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { + let Sequelize, sequelize + + beforeEach(async () => { + Sequelize = require(`../../../../../../../versions/sequelize@${sequelizeVersion}`).get() + sequelize = new Sequelize('database', 'username', 'password', { + dialect: 'sqlite', + logging: false + }) + await sequelize.query(`CREATE TABLE examples ( + id INT, + name VARCHAR(50), + query VARCHAR(100), + command VARCHAR(50), + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP )`) + + await sequelize.query(`INSERT INTO examples (id, name, query, command) + VALUES (1, 'Item1', 'SELECT 1', 'ls'), + (2, 'Item2', 'SELECT 1', 'ls'), + (3, 'Item3', 'SELECT 1', 'ls')`) + }) + + afterEach(() => { + return sequelize.close() + }) + + describe('using query method', () => { + testThatRequestHasVulnerability(async (req, res) => { + const result = await sequelize.query('SELECT * from examples') + + await sequelize.query(result[0][0].query) + + res.end('OK') + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await sequelize.query('SELECT * from examples') + + await sequelize.query(result[0][1].query) + + res.end('OK') + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const result = await sequelize.query('SELECT * from examples') + + const childProcess = require('child_process') + childProcess.execSync(result[0][0].command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + + describe('using Model', () => { + // let Model + let Example + + beforeEach(() => { + Example = sequelize.define('example', { + id: { + type: Sequelize.DataTypes.INTEGER, + primaryKey: true + }, + name: Sequelize.DataTypes.STRING, + query: Sequelize.DataTypes.STRING, + command: Sequelize.DataTypes.STRING + }) + }) + + testThatRequestHasVulnerability(async (req, res) => { + const examples = await Example.findAll() + + await sequelize.query(examples[0].query) + + res.end('OK') + }, 'SQL_INJECTION', { occurrences: 1 }, null, null, + 'Should have SQL_INJECTION using the first row of the result') + + testThatRequestHasNoVulnerability(async (req, res) => { + const examples = await Example.findAll() + + await sequelize.query(examples[1].query) + + res.end('OK') + }, 'SQL_INJECTION', null, 'Should not taint the second row of a query with default configuration') + + testThatRequestHasNoVulnerability(async (req, res) => { + const examples = await Example.findAll() + + const childProcess = require('child_process') + childProcess.execSync(examples[0].command) + + res.end('OK') + }, 'COMMAND_INJECTION', null, 'Should not detect COMMAND_INJECTION with database source') + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js index 7465f6b2408..8fc32f1c03a 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js @@ -46,9 +46,10 @@ describe('URI sourcing with express', () => { iast.disable() }) - it('should taint uri', done => { + it('should taint uri', (done) => { const app = express() - app.get('/path/*', (req, res) => { + const pathPattern = semver.intersects(version, '>=5.0.0') ? '/path/*splat' : '/path/*' + app.get(pathPattern, (req, res) => { const store = storage.getStore() const iastContext = iastContextFunctions.getIastContext(store) const isPathTainted = isTainted(iastContext, req.url) @@ -76,11 +77,11 @@ describe('Path params sourcing with express', () => { let appListener withVersions('express', 'express', version => { - const checkParamIsTaintedAndNext = (req, res, next, param) => { + const checkParamIsTaintedAndNext = (req, res, next, param, name) => { const store = storage.getStore() const iastContext = iastContextFunctions.getIastContext(store) - const pathParamValue = param + const pathParamValue = name ? req.params[name] : req.params const isParameterTainted = isTainted(iastContext, pathParamValue) expect(isParameterTainted).to.be.true const taintedParameterValueRanges = getRanges(iastContext, pathParamValue) @@ -188,8 +189,7 @@ describe('Path params sourcing with express', () => { res.status(200).send() }) - app.param('parameter1', checkParamIsTaintedAndNext) - app.param('parameter2', checkParamIsTaintedAndNext) + app.param(['parameter1', 'parameter2'], checkParamIsTaintedAndNext) appListener = app.listen(0, 'localhost', () => { const port = appListener.address().port @@ -202,6 +202,9 @@ describe('Path params sourcing with express', () => { }) it('should taint path param on router.params callback with custom implementation', function (done) { + if (!semver.satisfies(expressVersion, '4')) { + this.skip() + } const app = express() app.use('/:parameter1/:parameter2', (req, res) => { 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 c105eb5b97c..68023dc710e 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 @@ -169,15 +169,14 @@ describe('IAST TaintTracking Operations', () => { trim: id => id } - const iastLogStub = { - error (data) { return this }, - errorAndPublish (data) { return this } + const logStub = { + error (data) { return this } } - const logSpy = sinon.spy(iastLogStub) + const logSpy = sinon.spy(logStub) const operationsTaintObject = proxyquire('../../../../src/appsec/iast/taint-tracking/operations-taint-object', { '@datadog/native-iast-taint-tracking': taintedUtils, - '../iast-log': logSpy + '../../../log': logSpy }) const taintTrackingOperations = proxyquire('../../../../src/appsec/iast/taint-tracking/operations', { '../../../../../datadog-core': datadogCore, @@ -188,7 +187,6 @@ describe('IAST TaintTracking Operations', () => { taintTrackingOperations.createTransaction(transactionId, iastContext) const result = taintTrackingOperations.taintObject(iastContext, obj, null) expect(logSpy.error).to.have.been.calledOnce - expect(logSpy.errorAndPublish).to.have.been.calledOnce expect(result).to.equal(obj) }) }) diff --git a/packages/dd-trace/test/appsec/iast/utils.js b/packages/dd-trace/test/appsec/iast/utils.js index 6e427bcb629..01274dd954e 100644 --- a/packages/dd-trace/test/appsec/iast/utils.js +++ b/packages/dd-trace/test/appsec/iast/utils.js @@ -256,8 +256,8 @@ function prepareTestServerForIast (description, tests, iastConfig) { }) } - function testThatRequestHasNoVulnerability (fn, vulnerability, makeRequest) { - it(`should not have ${vulnerability} vulnerability`, function (done) { + function testThatRequestHasNoVulnerability (fn, vulnerability, makeRequest, description) { + it(description || `should not have ${vulnerability} vulnerability`, function (done) { app = fn checkNoVulnerabilityInRequest(vulnerability, config, done, makeRequest) }) diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/evidence-redaction/sensitive-handler.spec.js b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/evidence-redaction/sensitive-handler.spec.js index a9c1ae465ce..20ddeb75cfc 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/evidence-redaction/sensitive-handler.spec.js +++ b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/evidence-redaction/sensitive-handler.spec.js @@ -48,10 +48,10 @@ describe('Sensitive handler', () => { }) describe('Not valid custom patterns', () => { - const iastLog = require('../../../../../src/appsec/iast/iast-log') + const log = require('../../../../../src/log') beforeEach(() => { - sinon.stub(iastLog, 'warn') + sinon.stub(log, 'warn') }) afterEach(() => { @@ -63,9 +63,9 @@ describe('Sensitive handler', () => { expect(sensitiveHandler._namePattern.source).to.be.equals(DEFAULT_IAST_REDACTION_NAME_PATTERN) expect(sensitiveHandler._valuePattern.source).to.be.equals(DEFAULT_IAST_REDACTION_VALUE_PATTERN) - expect(iastLog.warn).to.have.been.calledTwice - expect(iastLog.warn.firstCall.args[0]).to.be.equals('Redaction name pattern is not valid') - expect(iastLog.warn.secondCall.args[0]).to.be.equals('Redaction value pattern is not valid') + expect(log.warn).to.have.been.calledTwice + expect(log.warn.firstCall.args[0]).to.be.equals('[ASM] Redaction name pattern is not valid') + expect(log.warn.secondCall.args[0]).to.be.equals('[ASM] Redaction value pattern is not valid') }) it('should use default name pattern when custom name pattern is not valid', () => { @@ -74,7 +74,7 @@ describe('Sensitive handler', () => { expect(sensitiveHandler._namePattern.source).to.be.equals(DEFAULT_IAST_REDACTION_NAME_PATTERN) expect(sensitiveHandler._valuePattern.source).to.be.equals(customValuePattern) - expect(iastLog.warn).to.have.been.calledOnceWithExactly('Redaction name pattern is not valid') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Redaction name pattern is not valid') }) it('should use default value pattern when custom value pattern is not valid', () => { @@ -83,7 +83,7 @@ describe('Sensitive handler', () => { expect(sensitiveHandler._namePattern.source).to.be.equals(customNamePattern) expect(sensitiveHandler._valuePattern.source).to.be.equals(DEFAULT_IAST_REDACTION_VALUE_PATTERN) - expect(iastLog.warn).to.have.been.calledOnceWithExactly('Redaction value pattern is not valid') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Redaction value pattern is not valid') }) }) 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 d77c5fb8e9b..884df6ebb3d 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 @@ -9,7 +9,8 @@ const excludedVulnerabilityTypes = ['XSS', 'EMAIL_HTML_INJECTION'] const excludedTests = [ 'Query with single quoted string literal and null source', // does not apply 'Redacted source that needs to be truncated', // not implemented yet - 'CODE_INJECTION - Tainted range based redaction - with null source ' // does not apply + 'CODE_INJECTION - Tainted range based redaction - with null source ', // does not apply + 'TEMPLATE_INJECTION - Tainted range based redaction - with null source ' // does not apply ] function doTest (testCase, parameters) { diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json index d40546b7328..945c676a688 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json +++ b/packages/dd-trace/test/appsec/iast/vulnerability-formatter/resources/evidence-redaction-suite.json @@ -2911,7 +2911,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ @@ -2969,7 +2970,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ @@ -3029,7 +3031,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ @@ -3083,7 +3086,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ @@ -3162,7 +3166,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ @@ -3238,7 +3243,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ @@ -3311,7 +3317,8 @@ "$1": [ "XSS", "CODE_INJECTION", - "EMAIL_HTML_INJECTION" + "EMAIL_HTML_INJECTION", + "TEMPLATE_INJECTION" ] }, "input": [ 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 1f4516218af..2ebe646a2d8 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js +++ b/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js @@ -52,14 +52,14 @@ describe('vulnerability-reporter', () => { expect(iastContext.vulnerabilities).to.be.an('array') }) - it('should add multiple vulnerabilities', () => { + it('should deduplicate same vulnerabilities', () => { addVulnerability(iastContext, vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, -555)) addVulnerability(iastContext, vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 888)) addVulnerability(iastContext, vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 123)) - expect(iastContext.vulnerabilities).to.have.length(3) + expect(iastContext.vulnerabilities).to.have.length(1) }) it('should add in the context evidence properties', () => { @@ -260,7 +260,12 @@ describe('vulnerability-reporter', () => { '[{"value":"SELECT id FROM u WHERE email = \'"},{"value":"joe@mail.com","source":1},{"value":"\';"}]},' + '"location":{"spanId":888,"path":"filename.js","line":99}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + + expect(prioritySampler.setPriority).to.have.been.calledTwice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should send multiple vulnerabilities with same tainted source', () => { @@ -313,7 +318,12 @@ describe('vulnerability-reporter', () => { '[{"value":"UPDATE u SET name=\'"},{"value":"joe","source":0},{"value":"\' WHERE id=1;"}]},' + '"location":{"spanId":888,"path":"filename.js","line":99}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + + expect(prioritySampler.setPriority).to.have.been.calledTwice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should send once with multiple vulnerabilities', () => { @@ -334,7 +344,13 @@ describe('vulnerability-reporter', () => { '{"type":"INSECURE_HASHING","hash":1755238473,"evidence":{"value":"md5"},' + '"location":{"spanId":-5,"path":"/path/to/file3.js","line":3}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority).to.have.been.calledThrice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.thirdCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should send once vulnerability with one vulnerability', () => { @@ -366,23 +382,6 @@ describe('vulnerability-reporter', () => { expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) - it('should not send duplicated vulnerabilities in multiple sends', () => { - const iastContext = { rootSpan: span } - addVulnerability(iastContext, - vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 888, - { path: 'filename.js', line: 88 })) - addVulnerability(iastContext, - vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 888, - { path: 'filename.js', line: 88 })) - sendVulnerabilities(iastContext.vulnerabilities, span) - sendVulnerabilities(iastContext.vulnerabilities, span) - expect(span.addTags).to.have.been.calledOnceWithExactly({ - '_dd.iast.json': '{"sources":[],"vulnerabilities":[{"type":"INSECURE_HASHING","hash":3410512691,' + - '"evidence":{"value":"sha1"},"location":{"spanId":888,"path":"filename.js","line":88}}]}' - }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - it('should not deduplicate vulnerabilities if not enabled', () => { start({ iast: { @@ -401,7 +400,11 @@ describe('vulnerability-reporter', () => { '{"type":"INSECURE_HASHING","hash":3410512691,"evidence":{"value":"sha1"},"location":' + '{"spanId":888,"path":"filename.js","line":88}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority).to.have.been.calledTwice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should add _dd.p.appsec trace tag with standalone enabled', () => { 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 c38d496623b..1c6a8aeb86d 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -19,7 +19,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../../versions/express').get() + const express = require(`../../../../versions/express@${version}`).get() const app = express() @@ -44,11 +44,7 @@ withVersions('express', 'express', version => { paramCallbackSpy = sinon.spy(paramCallback) - app.param(() => { - return paramCallbackSpy - }) - - app.param('callbackedParameter') + app.param('callbackedParameter', paramCallbackSpy) getPort().then((port) => { server = app.listen(port, () => { @@ -191,7 +187,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../../versions/express').get() + const express = require(`../../../../versions/express@${version}`).get() const app = express() @@ -256,7 +252,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../../versions/express').get() + const express = require(`../../../../versions/express@${version}`).get() const bodyParser = require('../../../../versions/body-parser').get() const app = express() @@ -275,7 +271,7 @@ withVersions('express', 'express', version => { }) app.post('/json', (req, res) => { - res.jsonp({ jsonResKey: 'jsonResValue' }) + res.json({ jsonResKey: 'jsonResValue' }) }) getPort().then((port) => { @@ -307,9 +303,9 @@ withVersions('express', 'express', version => { appsec.disable() }) - describe('with requestSampling 1.0', () => { + describe('with sample delay 10', () => { beforeEach(() => { - config.appsec.apiSecurity.requestSampling = 1.0 + config.appsec.apiSecurity.sampleDelay = 10 appsec.enable(config) }) @@ -374,7 +370,8 @@ withVersions('express', 'express', version => { }) it('should not get the schema', async () => { - config.appsec.apiSecurity.requestSampling = 0 + config.appsec.apiSecurity.enabled = false + config.appsec.apiSecurity.sampleDelay = 10 appsec.enable(config) const res = await axios.post('/', { key: 'value' }) diff --git a/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js b/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js index d444b82ec5e..49442e361b2 100644 --- a/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.sequelize.plugin.spec.js @@ -21,7 +21,7 @@ describe('sequelize', () => { rules: path.join(__dirname, 'express-rules.json'), apiSecurity: { enabled: true, - requestSampling: 1 + sampleDelay: 10 } } })) diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 4b8c6c0438c..7ca54e9241b 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -15,6 +15,7 @@ const { nextBodyParsed, nextQueryParsed, expressProcessParams, + routerParam, responseBody, responseWriteHead, responseSetHeader @@ -43,7 +44,7 @@ describe('AppSec Index', function () { let AppSec let web let blocking - let passport + let UserTracking let log let appsecTelemetry let graphql @@ -64,12 +65,11 @@ describe('AppSec Index', function () { blockedTemplateHtml: blockedTemplate.html, blockedTemplateJson: blockedTemplate.json, eventTracking: { - enabled: true, - mode: 'safe' + mode: 'anon' }, apiSecurity: { enabled: false, - requestSampling: 0 + sampleDelay: 10 }, rasp: { enabled: true @@ -78,15 +78,20 @@ describe('AppSec Index', function () { } web = { - root: sinon.stub() + root: sinon.stub(), + getContext: sinon.stub(), + _prioritySampler: { + isSampled: sinon.stub() + } } blocking = { setTemplates: sinon.stub() } - passport = { - passportTrackEvent: sinon.stub() + UserTracking = { + setCollectionMode: sinon.stub(), + trackLogin: sinon.stub() } log = { @@ -105,9 +110,10 @@ describe('AppSec Index', function () { disable: sinon.stub() } - apiSecuritySampler = require('../../src/appsec/api_security_sampler') + apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { + '../plugins/util/web': web + }) sinon.spy(apiSecuritySampler, 'sampleRequest') - sinon.spy(apiSecuritySampler, 'isSampled') rasp = { enable: sinon.stub(), @@ -118,7 +124,7 @@ describe('AppSec Index', function () { '../log': log, '../plugins/util/web': web, './blocking': blocking, - './passport': passport, + './user_tracking': UserTracking, './telemetry': appsecTelemetry, './graphql': graphql, './api_security_sampler': apiSecuritySampler, @@ -146,6 +152,7 @@ describe('AppSec Index', function () { expect(blocking.setTemplates).to.have.been.calledOnceWithExactly(config) expect(RuleManager.loadRules).to.have.been.calledOnceWithExactly(config.appsec) expect(Reporter.setRateLimit).to.have.been.calledOnceWithExactly(42) + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly('anon', false) expect(incomingHttpRequestStart.subscribe) .to.have.been.calledOnceWithExactly(AppSec.incomingHttpStartTranslator) expect(incomingHttpRequestEnd.subscribe).to.have.been.calledOnceWithExactly(AppSec.incomingHttpEndTranslator) @@ -160,9 +167,7 @@ describe('AppSec Index', function () { AppSec.enable(config) - expect(log.error).to.have.been.calledTwice - expect(log.error.firstCall).to.have.been.calledWithExactly('Unable to start AppSec') - expect(log.error.secondCall).to.have.been.calledWithExactly(err) + expect(log.error).to.have.been.calledOnceWithExactly('[ASM] Unable to start AppSec', err) expect(incomingHttpRequestStart.subscribe).to.not.have.been.called expect(incomingHttpRequestEnd.subscribe).to.not.have.been.called }) @@ -175,6 +180,7 @@ describe('AppSec Index', function () { expect(nextBodyParsed.hasSubscribers).to.be.false expect(nextQueryParsed.hasSubscribers).to.be.false expect(expressProcessParams.hasSubscribers).to.be.false + expect(routerParam.hasSubscribers).to.be.false expect(responseWriteHead.hasSubscribers).to.be.false expect(responseSetHeader.hasSubscribers).to.be.false @@ -187,17 +193,18 @@ describe('AppSec Index', function () { expect(nextBodyParsed.hasSubscribers).to.be.true expect(nextQueryParsed.hasSubscribers).to.be.true expect(expressProcessParams.hasSubscribers).to.be.true + expect(routerParam.hasSubscribers).to.be.true expect(responseWriteHead.hasSubscribers).to.be.true expect(responseSetHeader.hasSubscribers).to.be.true }) - it('should not subscribe to passportVerify if eventTracking is disabled', () => { - config.appsec.eventTracking.enabled = false + it('should still subscribe to passportVerify if eventTracking is disabled', () => { + config.appsec.eventTracking.mode = 'disabled' AppSec.disable() AppSec.enable(config) - expect(passportVerify.hasSubscribers).to.be.false + expect(passportVerify.hasSubscribers).to.be.true }) it('should call appsec telemetry enable', () => { @@ -268,6 +275,7 @@ describe('AppSec Index', function () { expect(nextBodyParsed.hasSubscribers).to.be.false expect(nextQueryParsed.hasSubscribers).to.be.false expect(expressProcessParams.hasSubscribers).to.be.false + expect(routerParam.hasSubscribers).to.be.false expect(responseWriteHead.hasSubscribers).to.be.false expect(responseSetHeader.hasSubscribers).to.be.false }) @@ -358,7 +366,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -396,7 +404,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -442,7 +450,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -472,47 +480,13 @@ describe('AppSec Index', function () { } web.root.returns(rootSpan) - }) - - it('should not trigger schema extraction with sampling disabled', () => { - config.appsec.apiSecurity = { - enabled: true, - requestSampling: 0 - } - - AppSec.enable(config) - - const req = { - url: '/path', - headers: { - 'user-agent': 'Arachni', - host: 'localhost', - cookie: 'a=1;b=2' - }, - method: 'POST', - socket: { - remoteAddress: '127.0.0.1', - remotePort: 8080 - } - } - const res = {} - - AppSec.incomingHttpStartTranslator({ req, res }) - - expect(waf.run).to.have.been.calledOnceWithExactly({ - persistent: { - 'server.request.uri.raw': '/path', - 'server.request.headers.no_cookies': { 'user-agent': 'Arachni', host: 'localhost' }, - 'server.request.method': 'POST', - 'http.client_ip': '127.0.0.1' - } - }, req) + web.getContext.returns({ paths: ['path'] }) }) it('should not trigger schema extraction with feature disabled', () => { config.appsec.apiSecurity = { enabled: false, - requestSampling: 1 + sampleDelay: 1 } AppSec.enable(config) @@ -528,18 +502,34 @@ describe('AppSec Index', function () { socket: { remoteAddress: '127.0.0.1', remotePort: 8080 + }, + body: { + a: '1' + }, + query: { + b: '2' + }, + route: { + path: '/path/:c' } } - const res = {} + const res = { + getHeaders: () => ({ + 'content-type': 'application/json', + 'content-length': 42 + }), + statusCode: 201 + } - AppSec.incomingHttpStartTranslator({ req, res }) + web.patch(req) + + sinon.stub(Reporter, 'finishRequest') + AppSec.incomingHttpEndTranslator({ req, res }) expect(waf.run).to.have.been.calledOnceWithExactly({ persistent: { - 'server.request.uri.raw': '/path', - 'server.request.headers.no_cookies': { 'user-agent': 'Arachni', host: 'localhost' }, - 'server.request.method': 'POST', - 'http.client_ip': '127.0.0.1' + 'server.request.body': { a: '1' }, + 'server.request.query': { b: '2' } } }, req) }) @@ -547,34 +537,52 @@ describe('AppSec Index', function () { it('should trigger schema extraction with sampling enabled', () => { config.appsec.apiSecurity = { enabled: true, - requestSampling: 1 + sampleDelay: 1 } AppSec.enable(config) const req = { - url: '/path', + route: { + path: '/path' + }, headers: { 'user-agent': 'Arachni', - host: 'localhost', - cookie: 'a=1;b=2' + host: 'localhost' }, method: 'POST', socket: { remoteAddress: '127.0.0.1', remotePort: 8080 + }, + body: { + a: '1' } } - const res = {} + const res = { + getHeaders: () => ({ + 'content-type': 'application/json', + 'content-length': 42 + }), + statusCode: 201 + } - AppSec.incomingHttpStartTranslator({ req, res }) + const span = { + context: sinon.stub().returns({ + _sampling: { + priority: 1 + } + }) + } + + web.root.returns(span) + web._prioritySampler.isSampled.returns(true) + + AppSec.incomingHttpEndTranslator({ req, res }) expect(waf.run).to.have.been.calledOnceWithExactly({ persistent: { - 'server.request.uri.raw': '/path', - 'server.request.headers.no_cookies': { 'user-agent': 'Arachni', host: 'localhost' }, - 'server.request.method': 'POST', - 'http.client_ip': '127.0.0.1', + 'server.request.body': { a: '1' }, 'waf.context.processor': { 'extract-schema': true } } }, req) @@ -584,8 +592,9 @@ describe('AppSec Index', function () { beforeEach(() => { config.appsec.apiSecurity = { enabled: true, - requestSampling: 1 + sampleDelay: 1 } + AppSec.enable(config) }) @@ -597,28 +606,30 @@ describe('AppSec Index', function () { responseBody.publish({ req: {}, body: 'string' }) responseBody.publish({ req: {}, body: null }) - expect(apiSecuritySampler.isSampled).to.not.been.called + expect(apiSecuritySampler.sampleRequest).to.not.been.called expect(waf.run).to.not.been.called }) it('should not call to the waf if it is not a sampled request', () => { - apiSecuritySampler.isSampled = apiSecuritySampler.isSampled.instantiateFake(() => false) + apiSecuritySampler.sampleRequest = apiSecuritySampler.sampleRequest.instantiateFake(() => false) const req = {} + const res = {} - responseBody.publish({ req, body: {} }) + responseBody.publish({ req, res, body: {} }) - expect(apiSecuritySampler.isSampled).to.have.been.calledOnceWith(req) + expect(apiSecuritySampler.sampleRequest).to.have.been.calledOnceWith(req, res) expect(waf.run).to.not.been.called }) it('should call to the waf if it is a sampled request', () => { - apiSecuritySampler.isSampled = apiSecuritySampler.isSampled.instantiateFake(() => true) + apiSecuritySampler.sampleRequest = apiSecuritySampler.sampleRequest.instantiateFake(() => true) const req = {} + const res = {} const body = {} - responseBody.publish({ req, body }) + responseBody.publish({ req, res, body }) - expect(apiSecuritySampler.isSampled).to.have.been.calledOnceWith(req) + expect(apiSecuritySampler.sampleRequest).to.have.been.calledOnceWith(req, res) expect(waf.run).to.been.calledOnceWith({ persistent: { [addresses.HTTP_INCOMING_RESPONSE_BODY]: body @@ -639,6 +650,17 @@ describe('AppSec Index', function () { abortController = { abort: sinon.stub() } + res = { + getHeaders: () => ({ + 'content-type': 'application/json', + 'content-length': 42 + }), + writeHead: sinon.stub(), + end: sinon.stub(), + getHeaderNames: sinon.stub().returns([]) + } + res.writeHead.returns(res) + req = { url: '/path', headers: { @@ -649,18 +671,9 @@ describe('AppSec Index', function () { socket: { remoteAddress: '127.0.0.1', remotePort: 8080 - } - } - res = { - getHeaders: () => ({ - 'content-type': 'application/json', - 'content-lenght': 42 - }), - writeHead: sinon.stub(), - end: sinon.stub(), - getHeaderNames: sinon.stub().returns([]) + }, + res } - res.writeHead.returns(res) AppSec.enable(config) AppSec.incomingHttpStartTranslator({ req, res }) @@ -797,31 +810,84 @@ describe('AppSec Index', function () { }) describe('onPassportVerify', () => { - it('Should call passportTrackEvent', () => { - const credentials = { type: 'local', username: 'test' } - const user = { id: '1234', username: 'Test' } + beforeEach(() => { + web.root.resetHistory() + sinon.stub(storage, 'getStore').returns({ req }) + }) - sinon.stub(storage, 'getStore').returns({ req: {} }) + it('should block when UserTracking.login() returns action', () => { + UserTracking.trackLogin.returns(resultActions) - passportVerify.publish({ credentials, user }) + const abortController = new AbortController() + const payload = { + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234' }, + success: true, + abortController + } - expect(passport.passportTrackEvent).to.have.been.calledOnceWithExactly( - credentials, - user, - rootSpan, - config.appsec.eventTracking.mode) + passportVerify.publish(payload) + + expect(storage.getStore).to.have.been.calledOnce + expect(web.root).to.have.been.calledOnceWithExactly(req) + expect(UserTracking.trackLogin).to.have.been.calledOnceWithExactly( + payload.framework, + payload.login, + payload.user, + payload.success, + rootSpan + ) + expect(abortController.signal.aborted).to.be.true + expect(res.end).to.have.been.called }) - it('Should call log if no rootSpan is found', () => { - const credentials = { type: 'local', username: 'test' } - const user = { id: '1234', username: 'Test' } + it('should not block when UserTracking.login() returns nothing', () => { + UserTracking.trackLogin.returns(undefined) + + const abortController = new AbortController() + const payload = { + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234' }, + success: true, + abortController + } - sinon.stub(storage, 'getStore').returns(undefined) + passportVerify.publish(payload) + + expect(storage.getStore).to.have.been.calledOnce + expect(web.root).to.have.been.calledOnceWithExactly(req) + expect(UserTracking.trackLogin).to.have.been.calledOnceWithExactly( + payload.framework, + payload.login, + payload.user, + payload.success, + rootSpan + ) + expect(abortController.signal.aborted).to.be.false + expect(res.end).to.not.have.been.called + }) + + it('should not block and call log if no rootSpan is found', () => { + storage.getStore.returns(undefined) + + const abortController = new AbortController() + const payload = { + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234' }, + success: true, + abortController + } - passportVerify.publish({ credentials, user }) + passportVerify.publish(payload) - expect(log.warn).to.have.been.calledOnceWithExactly('No rootSpan found in onPassportVerify') - expect(passport.passportTrackEvent).not.to.have.been.called + expect(storage.getStore).to.have.been.calledOnce + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] No rootSpan found in onPassportVerify') + expect(UserTracking.trackLogin).to.not.have.been.called + expect(abortController.signal.aborted).to.be.false + expect(res.end).to.not.have.been.called }) }) @@ -831,7 +897,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -842,7 +908,7 @@ describe('AppSec Index', function () { 'server.response.status': '404', 'server.response.headers.no_cookies': { 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 } } }, req) @@ -863,7 +929,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -874,7 +940,7 @@ describe('AppSec Index', function () { 'server.response.status': '404', 'server.response.headers.no_cookies': { 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 } } }, req) @@ -894,7 +960,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -910,7 +976,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -921,7 +987,7 @@ describe('AppSec Index', function () { 'server.response.status': '404', 'server.response.headers.no_cookies': { 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 } } }, req) @@ -937,7 +1003,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } responseWriteHead.publish({ req, res, abortController, statusCode: 404, responseHeaders }) diff --git a/packages/dd-trace/test/appsec/passport.spec.js b/packages/dd-trace/test/appsec/passport.spec.js deleted file mode 100644 index 7a3db36798c..00000000000 --- a/packages/dd-trace/test/appsec/passport.spec.js +++ /dev/null @@ -1,245 +0,0 @@ -'use strict' - -const proxyquire = require('proxyquire') - -describe('Passport', () => { - const rootSpan = { - context: () => { return {} } - } - const loginLocal = { type: 'local', username: 'test' } - const userUuid = { - id: '591dc126-8431-4d0f-9509-b23318d3dce4', - email: 'testUser@test.com', - username: 'Test User' - } - - let passportModule, log, events, setUser - - beforeEach(() => { - rootSpan.context = () => { return {} } - - log = { - warn: sinon.stub() - } - - events = { - trackEvent: sinon.stub() - } - - setUser = { - setUserTags: sinon.stub() - } - - passportModule = proxyquire('../../src/appsec/passport', { - '../log': log, - './sdk/track_event': events, - './sdk/set_user': setUser - }) - }) - - describe('passportTrackEvent', () => { - it('should call log when credentials is undefined', () => { - passportModule.passportTrackEvent(undefined, undefined, undefined, 'safe') - - expect(log.warn).to.have.been.calledOnceWithExactly('No user ID found in authentication instrumentation') - }) - - it('should call log when type is not known', () => { - const credentials = { type: 'unknown', username: 'test' } - - passportModule.passportTrackEvent(credentials, undefined, undefined, 'safe') - - expect(log.warn).to.have.been.calledOnceWithExactly('No user ID found in authentication instrumentation') - }) - - it('should call log when type is known but username not present', () => { - const credentials = { type: 'http' } - - passportModule.passportTrackEvent(credentials, undefined, undefined, 'safe') - - expect(log.warn).to.have.been.calledOnceWithExactly('No user ID found in authentication instrumentation') - }) - - it('should report login failure when passportUser is not present', () => { - passportModule.passportTrackEvent(loginLocal, undefined, undefined, 'safe') - - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.failure', - { 'usr.id': '' }, - 'passportTrackEvent', - undefined, - 'safe' - ) - }) - - it('should report login success when passportUser is present', () => { - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'safe') - - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly({ id: userUuid.id }, rootSpan) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'safe' - ) - }) - - it('should report login success and blank id in safe mode when id is not a uuid', () => { - const user = { - id: 'publicName', - email: 'testUser@test.com', - username: 'Test User' - } - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'safe') - - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly({ id: '' }, rootSpan) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'safe' - ) - }) - - it('should report login success and the extended fields in extended mode', () => { - const user = { - id: 'publicName', - email: 'testUser@test.com', - username: 'Test User' - } - - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'extended') - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly( - { - id: 'publicName', - login: 'test', - email: 'testUser@test.com', - username: 'Test User' - }, - rootSpan - ) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - - it('should not call trackEvent in safe mode if sdk user event functions are already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.users.login.success.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'safe') - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).not.to.have.been.called - }) - - it('should not call trackEvent in extended mode if trackUserLoginSuccessEvent is already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.users.login.success.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'extended') - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).not.to.have.been.called - }) - - it('should call trackEvent in extended mode if trackCustomEvent function is already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.custom.event.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'extended') - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - - it('should not call trackEvent in extended mode if trackUserLoginFailureEvent is already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.users.login.failure.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'extended') - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).not.to.have.been.called - }) - - it('should report login success with the _id field', () => { - const user = { - _id: '591dc126-8431-4d0f-9509-b23318d3dce4', - email: 'testUser@test.com', - username: 'Test User' - } - - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'extended') - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly( - { - id: '591dc126-8431-4d0f-9509-b23318d3dce4', - login: 'test', - email: 'testUser@test.com', - username: 'Test User' - }, - rootSpan - ) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - - it('should report login success with the username field passport name', () => { - const user = { - email: 'testUser@test.com', - name: 'Test User' - } - - rootSpan.context = () => { return {} } - - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'extended') - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly( - { - id: 'test', - login: 'test', - email: 'testUser@test.com', - username: 'Test User' - }, rootSpan) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - }) -}) 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 c91c49b65df..4ebb8c4910a 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 @@ -10,7 +10,7 @@ describe('RASP - command_injection - integration', () => { let axios, sandbox, cwd, appPort, appFile, agent, proc before(async function () { - this.timeout(60000) + this.timeout(process.platform === 'win32' ? 90000 : 30000) sandbox = await createSandbox( ['express'], 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 b5b825cc628..210c3849ece 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 @@ -102,7 +102,7 @@ describe('RASP - lfi', () => { describe(description, () => { const getAppFn = options.getAppFn ?? getApp - it('should block param from the request', async () => { + it('should block param from the request', () => { app = getAppFn(fn, args, options) const file = args[vulnerableIndex] diff --git a/packages/dd-trace/test/appsec/remote_config/index.spec.js b/packages/dd-trace/test/appsec/remote_config/index.spec.js index b1804e0b646..f3cc6a32dac 100644 --- a/packages/dd-trace/test/appsec/remote_config/index.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/index.spec.js @@ -7,15 +7,19 @@ let config let rc let RemoteConfigManager let RuleManager +let UserTracking +let log let appsec let remoteConfig -let apiSecuritySampler describe('Remote Config index', () => { beforeEach(() => { config = { appsec: { - enabled: undefined + enabled: undefined, + eventTracking: { + mode: 'identification' + } } } @@ -33,9 +37,12 @@ describe('Remote Config index', () => { updateWafFromRC: sinon.stub() } - apiSecuritySampler = { - configure: sinon.stub(), - setRequestSampling: sinon.stub() + UserTracking = { + setCollectionMode: sinon.stub() + } + + log = { + error: sinon.stub() } appsec = { @@ -46,72 +53,55 @@ describe('Remote Config index', () => { remoteConfig = proxyquire('../src/appsec/remote_config', { './manager': RemoteConfigManager, '../rule_manager': RuleManager, - '../api_security_sampler': apiSecuritySampler, + '../user_tracking': UserTracking, + '../../log': log, '..': appsec }) }) describe('enable', () => { it('should listen to remote config when appsec is not explicitly configured', () => { - config.appsec = { enabled: undefined } + config.appsec.enabled = undefined remoteConfig.enable(config) expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) expect(rc.updateCapabilities).to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ACTIVATION, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) expect(rc.setProductHandler).to.have.been.calledWith('ASM_FEATURES') expect(rc.setProductHandler.firstCall.args[1]).to.be.a('function') }) it('should listen to remote config when appsec is explicitly configured as enabled=true', () => { - config.appsec = { enabled: true } + config.appsec.enabled = true remoteConfig.enable(config) expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) - expect(rc.updateCapabilities).to.not.have.been.calledWith('ASM_ACTIVATION') + expect(rc.updateCapabilities).to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_ACTIVATION) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) expect(rc.setProductHandler).to.have.been.calledOnceWith('ASM_FEATURES') expect(rc.setProductHandler.firstCall.args[1]).to.be.a('function') }) it('should not listen to remote config when appsec is explicitly configured as enabled=false', () => { - config.appsec = { enabled: false } + config.appsec.enabled = false remoteConfig.enable(config) expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) expect(rc.updateCapabilities).to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_ACTIVATION, true) - expect(rc.setProductHandler).to.not.have.been.called - }) - - it('should listen ASM_API_SECURITY_SAMPLE_RATE when appsec.enabled=undefined and appSecurity.enabled=true', () => { - config.appsec = { enabled: undefined, apiSecurity: { enabled: true } } - - remoteConfig.enable(config) - - expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) - expect(rc.updateCapabilities) - .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ACTIVATION, true) - expect(rc.updateCapabilities) - .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_API_SECURITY_SAMPLE_RATE, true) - }) - - it('should listen ASM_API_SECURITY_SAMPLE_RATE when appsec.enabled=true and appSecurity.enabled=true', () => { - config.appsec = { enabled: true, apiSecurity: { enabled: true } } - - remoteConfig.enable(config) - - expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) expect(rc.updateCapabilities) - .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_API_SECURITY_SAMPLE_RATE, true) + .to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + expect(rc.setProductHandler).to.not.have.been.called }) describe('ASM_FEATURES remote config listener', () => { let listener beforeEach(() => { - config.appsec = { enabled: undefined } - remoteConfig.enable(config, appsec) listener = rc.setProductHandler.firstCall.args[1] @@ -129,8 +119,8 @@ describe('Remote Config index', () => { expect(appsec.enable).to.have.been.called }) - it('should disable appsec when listener is called with unnaply and enabled', () => { - listener('unnaply', { asm: { enabled: true } }) + it('should disable appsec when listener is called with unapply and enabled', () => { + listener('unapply', { asm: { enabled: true } }) expect(appsec.disable).to.have.been.calledOnce }) @@ -141,104 +131,58 @@ describe('Remote Config index', () => { expect(appsec.enable).to.not.have.been.called expect(appsec.disable).to.not.have.been.called }) - }) - - describe('API Security Request Sampling', () => { - describe('OneClick', () => { - let listener - beforeEach(() => { - config = { - appsec: { - enabled: undefined, - apiSecurity: { - requestSampling: 0.1 - } - } - } + describe('auto_user_instrum', () => { + const rcConfig = { auto_user_instrum: { mode: 'anonymous' } } + const configId = 'collectionModeId' - remoteConfig.enable(config) - - listener = rc.setProductHandler.firstCall.args[1] + afterEach(() => { + listener('unapply', rcConfig, configId) }) - it('should update apiSecuritySampler config', () => { - listener('apply', { - api_security: { - request_sample_rate: 0.5 - } - }) + it('should not update collection mode when not a string', () => { + listener('apply', { auto_user_instrum: { mode: 123 } }, configId) - expect(apiSecuritySampler.setRequestSampling).to.be.calledOnceWithExactly(0.5) + expect(UserTracking.setCollectionMode).to.not.have.been.called }) - it('should update apiSecuritySampler config and disable it', () => { - listener('apply', { - api_security: { - request_sample_rate: 0 - } - }) + it('should throw when called two times with different config ids', () => { + listener('apply', rcConfig, configId) - expect(apiSecuritySampler.setRequestSampling).to.be.calledOnceWithExactly(0) + expect(() => listener('apply', rcConfig, 'anotherId')).to.throw() + expect(log.error).to.have.been.calledOnceWithExactly( + '[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config' + ) }) - it('should not update apiSecuritySampler config with values greater than 1', () => { - listener('apply', { - api_security: { - request_sample_rate: 5 - } - }) + it('should update collection mode when called with apply', () => { + listener('apply', rcConfig, configId) - expect(apiSecuritySampler.configure).to.not.be.called + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly(rcConfig.auto_user_instrum.mode) }) - it('should not update apiSecuritySampler config with values less than 0', () => { - listener('apply', { - api_security: { - request_sample_rate: -0.4 - } - }) + it('should update collection mode when called with modify', () => { + listener('modify', rcConfig, configId) - expect(apiSecuritySampler.configure).to.not.be.called + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly(rcConfig.auto_user_instrum.mode) }) - it('should not update apiSecuritySampler config with incorrect values', () => { - listener('apply', { - api_security: { - request_sample_rate: 'not_a_number' - } - }) - - expect(apiSecuritySampler.configure).to.not.be.called - }) - }) + it('should revert collection mode when called with unapply', () => { + listener('apply', rcConfig, configId) + UserTracking.setCollectionMode.resetHistory() - describe('Enabled', () => { - let listener + listener('unapply', rcConfig, configId) - beforeEach(() => { - config = { - appsec: { - enabled: true, - apiSecurity: { - requestSampling: 0.1 - } - } - } - - remoteConfig.enable(config) - - listener = rc.setProductHandler.firstCall.args[1] + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly(config.appsec.eventTracking.mode) }) - it('should update config apiSecurity.requestSampling property value', () => { - listener('apply', { - api_security: { - request_sample_rate: 0.5 - } - }) + it('should not revert collection mode when called with unapply and unknown id', () => { + listener('apply', rcConfig, configId) + UserTracking.setCollectionMode.resetHistory() + + listener('unapply', rcConfig, 'unknownId') - expect(apiSecuritySampler.setRequestSampling).to.be.calledOnceWithExactly(0.5) + expect(UserTracking.setCollectionMode).to.not.have.been.called }) }) }) diff --git a/packages/dd-trace/test/appsec/remote_config/manager.spec.js b/packages/dd-trace/test/appsec/remote_config/manager.spec.js index f9aea97ce08..2a32e834e06 100644 --- a/packages/dd-trace/test/appsec/remote_config/manager.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/manager.spec.js @@ -212,7 +212,7 @@ describe('RemoteConfigManager', () => { rc.poll(() => { expect(request).to.have.been.calledOnceWith(payload, expectedPayload) - expect(log.error).to.have.been.calledOnceWithExactly(err) + expect(log.error).to.have.been.calledOnceWithExactly('[RC] Error in request', err) expect(rc.parseConfig).to.not.have.been.called cb() }) @@ -232,10 +232,11 @@ describe('RemoteConfigManager', () => { }) 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(new Error('Unable to parse config')) + rc.parseConfig.onFirstCall().throws(error) const payload = JSON.stringify(rc.state) @@ -243,7 +244,7 @@ describe('RemoteConfigManager', () => { expect(request).to.have.been.calledOnceWith(payload, expectedPayload) expect(rc.parseConfig).to.have.been.calledOnceWithExactly({ a: 'b' }) expect(log.error).to.have.been - .calledOnceWithExactly('Could not parse remote config response: Error: Unable to parse config') + .calledOnceWithExactly('[RC] Could not parse remote config response', error) expect(rc.state.client.state.has_error).to.be.true expect(rc.state.client.state.error).to.equal('Error: Unable to parse config') diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 757884c3566..cd7cc9a1581 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -223,6 +223,22 @@ describe('reporter', () => { storage.disable() }) + it('should add tags to request span when socket is not there', () => { + delete req.socket + + const result = Reporter.reportAttack('[{"rule":{},"rule_matches":[{}]}]') + + expect(result).to.not.be.false + expect(web.root).to.have.been.calledOnceWith(req) + + expect(span.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.event': 'true', + '_dd.origin': 'appsec', + '_dd.appsec.json': '{"triggers":[{"rule":{},"rule_matches":[{}]}]}' + }) + expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + }) + it('should add tags to request span', () => { const result = Reporter.reportAttack('[{"rule":{},"rule_matches":[{}]}]') expect(result).to.not.be.false diff --git a/packages/dd-trace/test/appsec/response_blocking.spec.js b/packages/dd-trace/test/appsec/response_blocking.spec.js index 03541858955..5ccd250eea2 100644 --- a/packages/dd-trace/test/appsec/response_blocking.spec.js +++ b/packages/dd-trace/test/appsec/response_blocking.spec.js @@ -55,6 +55,9 @@ describe('HTTP Response Blocking', () => { rules: path.join(__dirname, 'response_blocking_rules.json'), rasp: { enabled: false // disable rasp to not trigger waf.run executions due to lfi + }, + apiSecurity: { + enabled: false } } })) 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 9327a88afcd..29eb25560a1 100644 --- a/packages/dd-trace/test/appsec/sdk/set_user.spec.js +++ b/packages/dd-trace/test/appsec/sdk/set_user.spec.js @@ -32,14 +32,14 @@ describe('set_user', () => { describe('setUser', () => { it('should not call setTag when no user is passed', () => { setUser(tracer) - expect(log.warn).to.have.been.calledOnceWithExactly('Invalid user provided to setUser') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Invalid user provided to setUser') expect(rootSpan.setTag).to.not.have.been.called }) it('should not call setTag when user is empty', () => { const user = {} setUser(tracer, user) - expect(log.warn).to.have.been.calledOnceWithExactly('Invalid user provided to setUser') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Invalid user provided to setUser') expect(rootSpan.setTag).to.not.have.been.called }) @@ -48,7 +48,7 @@ describe('set_user', () => { setUser(tracer, { id: 'user' }) expect(getRootSpan).to.be.calledOnceWithExactly(tracer) - expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in setUser') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Root span not available in setUser') expect(rootSpan.setTag).to.not.have.been.called }) 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 fca01030c03..8e3c1a177bd 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -4,7 +4,7 @@ const proxyquire = require('proxyquire') const agent = require('../../plugins/agent') const axios = require('axios') const tracer = require('../../../../../index') -const { LOGIN_SUCCESS, LOGIN_FAILURE } = require('../../../src/appsec/addresses') +const { LOGIN_SUCCESS, LOGIN_FAILURE, USER_ID, USER_LOGIN } = require('../../../src/appsec/addresses') const { SAMPLING_MECHANISM_APPSEC } = require('../../../src/constants') const { USER_KEEP } = require('../../../../../ext/priority') @@ -12,13 +12,13 @@ describe('track_event', () => { describe('Internal API', () => { const tracer = {} let log + let prioritySampler let rootSpan let getRootSpan let setUserTags - let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent, trackEvent let sample let waf - let prioritySampler + let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent beforeEach(() => { log = { @@ -62,11 +62,6 @@ describe('track_event', () => { trackUserLoginSuccessEvent = trackEvents.trackUserLoginSuccessEvent trackUserLoginFailureEvent = trackEvents.trackUserLoginFailureEvent trackCustomEvent = trackEvents.trackCustomEvent - trackEvent = trackEvents.trackEvent - }) - - afterEach(() => { - sinon.restore() }) describe('trackUserLoginSuccessEvent', () => { @@ -75,9 +70,10 @@ describe('track_event', () => { trackUserLoginSuccessEvent(tracer, {}, { key: 'value' }) expect(log.warn).to.have.been.calledTwice - expect(log.warn.firstCall).to.have.been.calledWithExactly('Invalid user provided to trackUserLoginSuccessEvent') + expect(log.warn.firstCall) + .to.have.been.calledWithExactly('[ASM] Invalid user provided to trackUserLoginSuccessEvent') expect(log.warn.secondCall) - .to.have.been.calledWithExactly('Invalid user provided to trackUserLoginSuccessEvent') + .to.have.been.calledWithExactly('[ASM] Invalid user provided to trackUserLoginSuccessEvent') expect(setUserTags).to.not.have.been.called expect(rootSpan.addTags).to.not.have.been.called }) @@ -87,7 +83,8 @@ describe('track_event', () => { trackUserLoginSuccessEvent(tracer, { id: 'user_id' }, { key: 'value' }) - expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in trackUserLoginSuccessEvent') + expect(log.warn) + .to.have.been.calledOnceWithExactly('[ASM] Root span not available in trackUserLoginSuccessEvent') expect(setUserTags).to.not.have.been.called }) @@ -106,12 +103,21 @@ describe('track_event', () => { { 'appsec.events.users.login.success.track': 'true', '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'user_id', 'appsec.events.users.login.success.metakey1': 'metaValue1', 'appsec.events.users.login.success.metakey2': 'metaValue2', 'appsec.events.users.login.success.metakey3': 'metaValue3' }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_SUCCESS]: null, + [USER_ID]: 'user_id', + [USER_LOGIN]: 'user_id' + } + }) }) it('should call setUser and addTags without metadata', () => { @@ -123,33 +129,56 @@ describe('track_event', () => { expect(setUserTags).to.have.been.calledOnceWithExactly(user, rootSpan) expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ 'appsec.events.users.login.success.track': 'true', - '_dd.appsec.events.users.login.success.sdk': 'true' + '_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, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_SUCCESS]: null, + [USER_ID]: 'user_id', + [USER_LOGIN]: 'user_id' + } + }) }) - it('should call waf run with login success address', () => { - const user = { id: 'user_id' } + it('should call waf with user login', () => { + const user = { id: 'user_id', login: 'user_login' } trackUserLoginSuccessEvent(tracer, user) - sinon.assert.calledOnceWithExactly( - waf.run, - { persistent: { [LOGIN_SUCCESS]: null } } - ) + + expect(log.warn).to.not.have.been.called + expect(setUserTags).to.have.been.calledOnceWithExactly(user, rootSpan) + expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.events.users.login.success.track': 'true', + '_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, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_SUCCESS]: null, + [USER_ID]: 'user_id', + [USER_LOGIN]: 'user_login' + } + }) }) }) describe('trackUserLoginFailureEvent', () => { it('should log warning when passed invalid userId', () => { - trackUserLoginFailureEvent(tracer, null, false) - trackUserLoginFailureEvent(tracer, [], false) + trackUserLoginFailureEvent(tracer, null, false, { key: 'value' }) + trackUserLoginFailureEvent(tracer, [], false, { key: 'value' }) expect(log.warn).to.have.been.calledTwice expect(log.warn.firstCall) - .to.have.been.calledWithExactly('Invalid userId provided to trackUserLoginFailureEvent') + .to.have.been.calledWithExactly('[ASM] Invalid userId provided to trackUserLoginFailureEvent') expect(log.warn.secondCall) - .to.have.been.calledWithExactly('Invalid userId provided to trackUserLoginFailureEvent') + .to.have.been.calledWithExactly('[ASM] Invalid userId provided to trackUserLoginFailureEvent') expect(setUserTags).to.not.have.been.called expect(rootSpan.addTags).to.not.have.been.called }) @@ -157,15 +186,18 @@ describe('track_event', () => { it('should log warning when root span is not available', () => { rootSpan = undefined - trackUserLoginFailureEvent(tracer, 'user_id', false) + trackUserLoginFailureEvent(tracer, 'user_id', false, { key: 'value' }) - expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in trackUserLoginFailureEvent') + expect(log.warn) + .to.have.been.calledOnceWithExactly('[ASM] Root span not available in %s', 'trackUserLoginFailureEvent') expect(setUserTags).to.not.have.been.called }) it('should call addTags with metadata', () => { trackUserLoginFailureEvent(tracer, 'user_id', true, { - metakey1: 'metaValue1', metakey2: 'metaValue2', metakey3: 'metaValue3' + metakey1: 'metaValue1', + metakey2: 'metaValue2', + metakey3: 'metaValue3' }) expect(log.warn).to.not.have.been.called @@ -174,6 +206,7 @@ describe('track_event', () => { 'appsec.events.users.login.failure.track': 'true', '_dd.appsec.events.users.login.failure.sdk': 'true', 'appsec.events.users.login.failure.usr.id': 'user_id', + 'appsec.events.users.login.failure.usr.login': 'user_id', 'appsec.events.users.login.failure.usr.exists': 'true', 'appsec.events.users.login.failure.metakey1': 'metaValue1', 'appsec.events.users.login.failure.metakey2': 'metaValue2', @@ -181,11 +214,20 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_FAILURE]: null, + [USER_LOGIN]: 'user_id' + } + }) }) it('should send false `usr.exists` property when the user does not exist', () => { trackUserLoginFailureEvent(tracer, 'user_id', false, { - metakey1: 'metaValue1', metakey2: 'metaValue2', metakey3: 'metaValue3' + metakey1: 'metaValue1', + metakey2: 'metaValue2', + metakey3: 'metaValue3' }) expect(log.warn).to.not.have.been.called @@ -194,6 +236,7 @@ describe('track_event', () => { 'appsec.events.users.login.failure.track': 'true', '_dd.appsec.events.users.login.failure.sdk': 'true', 'appsec.events.users.login.failure.usr.id': 'user_id', + 'appsec.events.users.login.failure.usr.login': 'user_id', 'appsec.events.users.login.failure.usr.exists': 'false', 'appsec.events.users.login.failure.metakey1': 'metaValue1', 'appsec.events.users.login.failure.metakey2': 'metaValue2', @@ -201,6 +244,13 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_FAILURE]: null, + [USER_LOGIN]: 'user_id' + } + }) }) it('should call addTags without metadata', () => { @@ -212,18 +262,18 @@ describe('track_event', () => { 'appsec.events.users.login.failure.track': 'true', '_dd.appsec.events.users.login.failure.sdk': 'true', 'appsec.events.users.login.failure.usr.id': 'user_id', + '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, SAMPLING_MECHANISM_APPSEC) - }) - - it('should call waf run with login failure address', () => { - trackUserLoginFailureEvent(tracer, 'user_id') - sinon.assert.calledOnceWithExactly( - waf.run, - { persistent: { [LOGIN_FAILURE]: null } } - ) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_FAILURE]: null, + [USER_LOGIN]: 'user_id' + } + }) }) }) @@ -233,8 +283,10 @@ describe('track_event', () => { trackCustomEvent(tracer, { name: 'name' }) expect(log.warn).to.have.been.calledTwice - expect(log.warn.firstCall).to.have.been.calledWithExactly('Invalid eventName provided to trackCustomEvent') - expect(log.warn.secondCall).to.have.been.calledWithExactly('Invalid eventName provided to trackCustomEvent') + 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') expect(setUserTags).to.not.have.been.called expect(rootSpan.addTags).to.not.have.been.called }) @@ -244,12 +296,16 @@ describe('track_event', () => { trackCustomEvent(tracer, 'custom_event') - expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in trackCustomEvent') + expect(log.warn) + .to.have.been.calledOnceWithExactly('[ASM] Root span not available in %s', 'trackCustomEvent') expect(setUserTags).to.not.have.been.called }) it('should call addTags with metadata', () => { - trackCustomEvent(tracer, 'custom_event', { metaKey1: 'metaValue1', metakey2: 'metaValue2' }) + trackCustomEvent(tracer, 'custom_event', { + metaKey1: 'metaValue1', + metakey2: 'metaValue2' + }) expect(log.warn).to.not.have.been.called expect(setUserTags).to.not.have.been.called @@ -261,6 +317,7 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) }) it('should call addTags without metadata', () => { @@ -274,42 +331,6 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - }) - - describe('trackEvent', () => { - it('should call addTags with safe mode', () => { - trackEvent('event', { metaKey1: 'metaValue1', metakey2: 'metaValue2' }, 'trackEvent', rootSpan, 'safe') - expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ - 'appsec.events.event.track': 'true', - '_dd.appsec.events.event.auto.mode': 'safe', - 'appsec.events.event.metaKey1': 'metaValue1', - 'appsec.events.event.metakey2': 'metaValue2' - }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - - it('should call addTags with extended mode', () => { - trackEvent('event', { metaKey1: 'metaValue1', metakey2: 'metaValue2' }, 'trackEvent', rootSpan, 'extended') - expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ - 'appsec.events.event.track': 'true', - '_dd.appsec.events.event.auto.mode': 'extended', - 'appsec.events.event.metaKey1': 'metaValue1', - 'appsec.events.event.metakey2': 'metaValue2' - }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - - it('should call standalone sample', () => { - trackEvent('event', undefined, 'trackEvent', rootSpan, undefined) - - expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ - 'appsec.events.event.track': 'true' - }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) expect(sample).to.have.been.calledOnceWithExactly(rootSpan) }) }) 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 6df68104e85..3a361eb382a 100644 --- a/packages/dd-trace/test/appsec/sdk/user_blocking.spec.js +++ b/packages/dd-trace/test/appsec/sdk/user_blocking.spec.js @@ -64,13 +64,13 @@ describe('user_blocking', () => { it('should return false and log warn when passed no user', () => { const ret = userBlocking.checkUserAndSetUser() expect(ret).to.be.false - expect(log.warn).to.have.been.calledOnceWithExactly('Invalid user provided to isUserBlocked') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Invalid user provided to isUserBlocked') }) it('should return false and log warn when passed invalid user', () => { const ret = userBlocking.checkUserAndSetUser({}) expect(ret).to.be.false - expect(log.warn).to.have.been.calledOnceWithExactly('Invalid user provided to isUserBlocked') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Invalid user provided to isUserBlocked') }) it('should set user when not already set', () => { @@ -97,7 +97,7 @@ describe('user_blocking', () => { const ret = userBlocking.checkUserAndSetUser(tracer, { id: 'user' }) expect(ret).to.be.true expect(getRootSpan).to.have.been.calledOnceWithExactly(tracer) - expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in isUserBlocked') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Root span not available in isUserBlocked') expect(rootSpan.setTag).to.not.have.been.called }) @@ -122,7 +122,8 @@ describe('user_blocking', () => { const ret = userBlocking.blockRequest(tracer) expect(ret).to.be.false expect(storage.getStore).to.have.been.calledOnce - expect(log.warn).to.have.been.calledOnceWithExactly('Requests or response object not available in blockRequest') + expect(log.warn) + .to.have.been.calledOnceWithExactly('[ASM] Requests or response object not available in blockRequest') expect(block).to.not.have.been.called }) @@ -131,7 +132,7 @@ describe('user_blocking', () => { const ret = userBlocking.blockRequest(tracer, {}, {}) expect(ret).to.be.false - expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in blockRequest') + expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] Root span not available in blockRequest') expect(block).to.not.have.been.called }) diff --git a/packages/dd-trace/test/appsec/sdk/utils.spec.js b/packages/dd-trace/test/appsec/sdk/utils.spec.js new file mode 100644 index 00000000000..157d69e4411 --- /dev/null +++ b/packages/dd-trace/test/appsec/sdk/utils.spec.js @@ -0,0 +1,166 @@ +'use strict' + +const { assert } = require('chai') + +const { getRootSpan } = require('../../../src/appsec/sdk/utils') +const DatadogTracer = require('../../../src/tracer') +const Config = require('../../../src/config') +const id = require('../../../src/id') + +describe('Appsec SDK utils', () => { + let tracer + + before(() => { + tracer = new DatadogTracer(new Config({ + enabled: true + })) + }) + + describe('getRootSpan', () => { + it('should return root span if there are no childs', () => { + tracer.trace('parent', { }, parent => { + const root = getRootSpan(tracer) + + assert.equal(root, parent) + }) + }) + + it('should return root span of single child', () => { + const childOf = tracer.startSpan('parent') + + tracer.trace('child1', { childOf }, child1 => { + const root = getRootSpan(tracer) + + assert.equal(root, childOf) + }) + }) + + it('should return root span of single child from unknown parent', () => { + const childOf = tracer.startSpan('parent') + childOf.context()._parentId = id() + + tracer.trace('child1', { childOf }, child1 => { + const root = getRootSpan(tracer) + + assert.equal(root, childOf) + }) + }) + + it('should return root span of multiple child', () => { + const childOf = tracer.startSpan('parent') + + tracer.trace('child1.1', { childOf }, child11 => { + tracer.trace('child1.1.2', { childOf: child11 }, child112 => {}) + }) + tracer.trace('child1.2', { childOf }, child12 => { + const root = getRootSpan(tracer) + + assert.equal(root, childOf) + }) + }) + + it('should return root span of single child discarding inferred spans', () => { + const childOf = tracer.startSpan('parent') + childOf.setTag('_inferred_span', {}) + + tracer.trace('child1', { childOf }, child1 => { + const root = getRootSpan(tracer) + + assert.equal(root, child1) + }) + }) + + it('should return root span of an inferred span', () => { + const childOf = tracer.startSpan('parent') + + tracer.trace('child1', { childOf }, child1 => { + child1.setTag('_inferred_span', {}) + + const root = getRootSpan(tracer) + + assert.equal(root, childOf) + }) + }) + + it('should return root span of an inferred span with inferred parent', () => { + const childOf = tracer.startSpan('parent') + childOf.setTag('_inferred_span', {}) + + tracer.trace('child1', { childOf }, child1 => { + child1.setTag('_inferred_span', {}) + + const root = getRootSpan(tracer) + + assert.equal(root, child1) + }) + }) + + it('should return root span discarding inferred spans (mutiple childs)', () => { + const childOf = tracer.startSpan('parent') + childOf.setTag('_inferred_span', {}) + + tracer.trace('child1.1', { childOf }, child11 => {}) + tracer.trace('child1.2', { childOf }, child12 => { + tracer.trace('child1.2.1', { childOf: child12 }, child121 => { + const root = getRootSpan(tracer) + + assert.equal(root, child12) + }) + }) + }) + + it('should return root span discarding inferred spans if it is direct parent (mutiple childs)', () => { + const childOf = tracer.startSpan('parent') + + tracer.trace('child1.1', { childOf }, child11 => {}) + tracer.trace('child1.2', { childOf }, child12 => { + child12.setTag('_inferred_span', {}) + + tracer.trace('child1.2.1', { childOf: child12 }, child121 => { + const root = getRootSpan(tracer) + + assert.equal(root, childOf) + }) + }) + }) + + it('should return root span discarding multiple inferred spans', () => { + const childOf = tracer.startSpan('parent') + + tracer.trace('child1.1', { childOf }, child11 => {}) + tracer.trace('child1.2', { childOf }, child12 => { + child12.setTag('_inferred_span', {}) + + tracer.trace('child1.2.1', { childOf: child12 }, child121 => { + child121.setTag('_inferred_span', {}) + + tracer.trace('child1.2.1.1', { childOf: child121 }, child1211 => { + const root = getRootSpan(tracer) + + assert.equal(root, childOf) + }) + }) + }) + }) + + it('should return itself as root span if all are inferred spans', () => { + const childOf = tracer.startSpan('parent') + childOf.setTag('_inferred_span', {}) + + tracer.trace('child1.1', { childOf }, child11 => {}) + tracer.trace('child1.2', { childOf }, child12 => { + child12.setTag('_inferred_span', {}) + + tracer.trace('child1.2.1', { childOf: child12 }, child121 => { + child121.setTag('_inferred_span', {}) + + tracer.trace('child1.2.1.1', { childOf: child121 }, child1211 => { + const root = getRootSpan(tracer) + + assert.equal(root, child1211) + }) + }) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/appsec/telemetry.spec.js b/packages/dd-trace/test/appsec/telemetry.spec.js index a297ede3280..3eb3b8521b4 100644 --- a/packages/dd-trace/test/appsec/telemetry.spec.js +++ b/packages/dd-trace/test/appsec/telemetry.spec.js @@ -339,6 +339,17 @@ describe('Appsec Telemetry metrics', () => { expect(count).to.not.have.been.called }) }) + + describe('incrementMissingUserLoginMetric', () => { + it('should increment instrum.user_auth.missing_user_login metric', () => { + appsecTelemetry.incrementMissingUserLoginMetric('passport-local', 'login_success') + + expect(count).to.have.been.calledOnceWithExactly('instrum.user_auth.missing_user_login', { + framework: 'passport-local', + event_type: 'login_success' + }) + }) + }) }) describe('if disabled', () => { diff --git a/packages/dd-trace/test/appsec/user_tracking.spec.js b/packages/dd-trace/test/appsec/user_tracking.spec.js new file mode 100644 index 00000000000..651048d5515 --- /dev/null +++ b/packages/dd-trace/test/appsec/user_tracking.spec.js @@ -0,0 +1,696 @@ +'use strict' + +const assert = require('assert') + +const log = require('../../src/log') +const telemetry = require('../../src/appsec/telemetry') +const { SAMPLING_MECHANISM_APPSEC } = require('../../src/constants') +const standalone = require('../../src/appsec/standalone') +const waf = require('../../src/appsec/waf') + +describe('User Tracking', () => { + let currentTags + let rootSpan + let keepTrace + + let setCollectionMode + let trackLogin + + beforeEach(() => { + sinon.stub(log, 'warn') + sinon.stub(log, 'error') + sinon.stub(telemetry, 'incrementMissingUserLoginMetric') + sinon.stub(standalone, 'sample') + sinon.stub(waf, 'run').returns(['action1']) + + currentTags = {} + + rootSpan = { + context: () => ({ _tags: currentTags }), + addTags: sinon.stub() + } + + keepTrace = sinon.stub() + + const UserTracking = proxyquire('../src/appsec/user_tracking', { + '../priority_sampler': { keepTrace } + }) + + setCollectionMode = UserTracking.setCollectionMode + trackLogin = UserTracking.trackLogin + }) + + afterEach(() => { + sinon.restore() + }) + + describe('getUserId', () => { + beforeEach(() => { + setCollectionMode('identification') + }) + + it('should find an id field in user object', () => { + const user = { + notId: 'no', + id: '123', + email: 'a@b.c' + } + + const results = trackLogin('passport-local', 'login', user, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should find an id-like field in user object when no id field is present', () => { + const user = { + notId: 'no', + email: 'a@b.c', + username: 'azerty' + } + + const results = trackLogin('passport-local', 'login', user, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': 'a@b.c', + 'usr.id': 'a@b.c' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': 'a@b.c', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should find a stringifiable id in user object', () => { + const stringifiableObject = { + a: 1, + toString: () => '123' + } + + const user = { + notId: 'no', + id: { a: 1 }, + _id: stringifiableObject, + email: 'a@b.c' + } + + const results = trackLogin('passport-local', 'login', user, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + }) + + describe('trackLogin', () => { + it('should not do anything if collectionMode is empty or disabled', () => { + setCollectionMode('disabled') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + it('should log error when rootSpan is not found', () => { + setCollectionMode('identification') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.calledOnceWithExactly(log.error, '[ASM] No rootSpan found in AppSec trackLogin') + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + it('should log error and send telemetry when login success is not a string', () => { + setCollectionMode('identification') + + const results = trackLogin('passport-local', {}, { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.calledOnceWithExactly(log.error, '[ASM] Invalid login provided to AppSec trackLogin') + sinon.assert.calledOnceWithExactly(telemetry.incrementMissingUserLoginMetric, 'passport-local', 'login_success') + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + it('should log error and send telemetry when login failure is not a string', () => { + setCollectionMode('identification') + + const results = trackLogin('passport-local', {}, { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.calledOnceWithExactly(log.error, '[ASM] Invalid login provided to AppSec trackLogin') + sinon.assert.calledOnceWithExactly(telemetry.incrementMissingUserLoginMetric, 'passport-local', 'login_failure') + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + describe('when collectionMode is indentification', () => { + beforeEach(() => { + setCollectionMode('identification') + }) + + it('should write tags and call waf when success is true', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf when success is false', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.failure.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'appsec.events.users.login.failure.usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should not overwrite tags set by SDK when success is true', () => { + currentTags = { + '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'sdk_login', + 'usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + '_dd.appsec.usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should not overwwrite tags set by SDK when success is false', () => { + currentTags = { + '_dd.appsec.events.users.login.failure.sdk': 'true', + 'appsec.events.users.login.failure.usr.login': 'sdk_login', + 'appsec.events.users.login.failure.usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + '_dd.appsec.usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should write tags and call waf without user object when success is true', () => { + const results = trackLogin('passport-local', 'login', null, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf without user object when success is false', () => { + const results = trackLogin('passport-local', 'login', null, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.failure.usr.login': 'login' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.failure': null + } + }) + }) + }) + + describe('when collectionMode is anonymization', () => { + beforeEach(() => { + setCollectionMode('anonymization') + }) + + it('should write tags and call waf when success is true', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf when success is false', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.failure.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'appsec.events.users.login.failure.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should not overwrite tags set by SDK when success is true', () => { + currentTags = { + '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'sdk_login', + 'usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should not overwwrite tags set by SDK when success is false', () => { + currentTags = { + '_dd.appsec.events.users.login.failure.sdk': 'true', + 'appsec.events.users.login.failure.usr.login': 'sdk_login', + 'appsec.events.users.login.failure.usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should write tags and call waf without user object when success is true', () => { + const results = trackLogin('passport-local', 'login', null, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf without user object when success is false', () => { + const results = trackLogin('passport-local', 'login', null, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.failure.usr.login': 'anon_428821350e9691491f616b754cd8315f' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.failure': null + } + }) + }) + }) + + describe('collectionMode aliases', () => { + it('should log warning and use anonymization mode when collectionMode is safe', () => { + setCollectionMode('safe') + + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Using deprecated value "safe" in config.appsec.eventTracking.mode' + ) + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should use anonymization mode when collectionMode is anon', () => { + setCollectionMode('anon') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should log warning and use identification mode when collectionMode is extended', () => { + setCollectionMode('extended') + + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Using deprecated value "extended" in config.appsec.eventTracking.mode' + ) + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should use identification mode when collectionMode is ident', () => { + setCollectionMode('ident') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should use disabled mode when collectionMode is not recognized', () => { + setCollectionMode('saperlipopette') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + }) + }) +}) 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 cffe9718ee2..436f6c093d4 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 @@ -151,7 +151,7 @@ describe('WAFContextWrapper', () => { wafContextWrapper.run(payload) sinon.assert.notCalled(ddwafContext.run) - sinon.assert.calledOnceWithExactly(log.warn, 'Calling run on a disposed context') + sinon.assert.calledOnceWithExactly(log.warn, '[ASM] Calling run on a disposed context') }) }) }) 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 62e10e9753e..61ffee21181 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 @@ -111,7 +111,7 @@ describe('CI Visibility Coverage Writer', () => { encoder.makePayload.returns(payload) coverageWriter.flush(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error).to.have.been.calledWith('Error sending CI coverage payload', error) done() }) }) 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 85765c6bf3a..29ac58fbd31 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 @@ -113,7 +113,7 @@ describe('CI Visibility Writer', () => { encoder.count.returns(1) writer.flush(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error).to.have.been.calledWith('Error sending CI agentless payload', error) done() }) }) diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 001ff8acf27..8e87b6fa855 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -28,6 +28,14 @@ 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 DD_GIT_PROPERTIES_FILE = require.resolve('./fixtures/config/git.properties') + const CONFIG_NORM_RULES_PATH = require.resolve('./fixtures/telemetry/config_norm_rules.json') + const CONFIG_NORM_RULES = readFileSync(CONFIG_NORM_RULES_PATH, { encoding: 'utf8' }) + const CONFIG_PREFIX_BLOCK_LIST_PATH = require.resolve('./fixtures/telemetry/config_prefix_block_list.json') + const CONFIG_PREFIX_BLOCK_LIST = readFileSync(CONFIG_PREFIX_BLOCK_LIST_PATH, { encoding: 'utf8' }) + const CONFIG_AGGREGATION_LIST_PATH = require.resolve('./fixtures/telemetry/config_aggregation_list.json') + const CONFIG_AGGREGATION_LIST = readFileSync(CONFIG_AGGREGATION_LIST_PATH, { encoding: 'utf8' }) + const NODEJS_CONFIG_RULES_PATH = require.resolve('./fixtures/telemetry/nodejs_config_rules.json') + const NODEJS_CONFIG_RULES = readFileSync(NODEJS_CONFIG_RULES_PATH, { encoding: 'utf8' }) function reloadLoggerAndConfig () { log = proxyquire('../src/log', {}) @@ -162,7 +170,7 @@ describe('Config', () => { it('should correctly map OTEL_RESOURCE_ATTRIBUTES', () => { process.env.OTEL_RESOURCE_ATTRIBUTES = - 'deployment.environment=test1,service.name=test2,service.version=5,foo=bar1,baz=qux1' + 'deployment.environment=test1,service.name=test2,service.version=5,foo=bar1,baz=qux1' const config = new Config() expect(config).to.have.property('env', 'test1') @@ -212,7 +220,7 @@ describe('Config', () => { expect(config).to.have.property('queryStringObfuscation').with.length(626) expect(config).to.have.property('clientIpEnabled', false) expect(config).to.have.property('clientIpHeader', null) - expect(config).to.have.nested.property('crashtracking.enabled', false) + expect(config).to.have.nested.property('crashtracking.enabled', true) expect(config).to.have.property('sampleRate', undefined) expect(config).to.have.property('runtimeMetrics', false) expect(config.tags).to.have.property('service', 'node') @@ -251,10 +259,9 @@ describe('Config', () => { 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.eventTracking.enabled', true) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'safe') + expect(config).to.have.nested.property('appsec.eventTracking.mode', 'identification') expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.requestSampling', 0.1) + expect(config).to.have.nested.property('appsec.apiSecurity.sampleDelay', 30) expect(config).to.have.nested.property('appsec.sca.enabled', null) expect(config).to.have.nested.property('appsec.standalone.enabled', undefined) expect(config).to.have.nested.property('remoteConfig.enabled', true) @@ -277,15 +284,16 @@ describe('Config', () => { { name: 'appsec.blockedTemplateHtml', value: undefined, origin: 'default' }, { name: 'appsec.blockedTemplateJson', value: undefined, origin: 'default' }, { name: 'appsec.enabled', value: undefined, origin: 'default' }, + { name: 'appsec.eventTracking.mode', value: 'identification', origin: 'default' }, { name: 'appsec.obfuscatorKeyRegex', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\\.net[_-]sessionid|sid|jwt', origin: 'default' }, { name: 'appsec.obfuscatorValueRegex', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\\.net(?:[_-]|-)sessionid|sid|jwt)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}', origin: 'default' }, @@ -316,6 +324,7 @@ describe('Config', () => { { name: 'headerTags', value: [], origin: 'default' }, { name: 'hostname', value: '127.0.0.1', origin: 'default' }, { name: 'iast.cookieFilterPattern', value: '.{32,}', origin: 'default' }, + { name: 'iast.dbRowsToTaint', value: 1, origin: 'default' }, { name: 'iast.deduplicationEnabled', value: true, origin: 'default' }, { name: 'iast.enabled', value: false, origin: 'default' }, { name: 'iast.maxConcurrentRequests', value: 2, origin: 'default' }, @@ -334,6 +343,8 @@ describe('Config', () => { { name: 'isGitUploadEnabled', value: false, origin: 'default' }, { name: 'isIntelligentTestRunnerEnabled', value: false, origin: 'default' }, { name: 'isManualApiEnabled', value: false, origin: 'default' }, + { name: 'langchain.spanCharLimit', value: 128, origin: 'default' }, + { name: 'langchain.spanPromptCompletionSampleRate', value: 1.0, origin: 'default' }, { name: 'llmobs.agentlessEnabled', value: false, origin: 'default' }, { name: 'llmobs.mlApp', value: undefined, origin: 'default' }, { name: 'ciVisibilityTestSessionName', value: '', origin: 'default' }, @@ -352,7 +363,7 @@ describe('Config', () => { { name: 'protocolVersion', value: '0.4', origin: 'default' }, { name: 'queryStringObfuscation', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}', origin: 'default' }, @@ -378,7 +389,7 @@ describe('Config', () => { { name: 'telemetry.dependencyCollection', value: true, origin: 'default' }, { name: 'telemetry.enabled', value: true, origin: 'env_var' }, { name: 'telemetry.heartbeatInterval', value: 60000, origin: 'default' }, - { name: 'telemetry.logCollection', value: false, origin: 'default' }, + { name: 'telemetry.logCollection', value: true, origin: 'default' }, { name: 'telemetry.metrics', value: true, origin: 'default' }, { name: 'traceId128BitGenerationEnabled', value: true, origin: 'default' }, { name: 'traceId128BitLoggingEnabled', value: false, origin: 'default' }, @@ -441,7 +452,7 @@ describe('Config', () => { process.env.DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP = '.*' process.env.DD_TRACE_CLIENT_IP_ENABLED = 'true' process.env.DD_TRACE_CLIENT_IP_HEADER = 'x-true-client-ip' - process.env.DD_CRASHTRACKING_ENABLED = 'true' + process.env.DD_CRASHTRACKING_ENABLED = 'false' process.env.DD_RUNTIME_METRICS_ENABLED = 'true' process.env.DD_TRACE_REPORT_HOSTNAME = 'true' process.env.DD_ENV = 'test' @@ -494,6 +505,7 @@ describe('Config', () => { process.env.DD_IAST_MAX_CONCURRENT_REQUESTS = '3' process.env.DD_IAST_MAX_CONTEXT_OPERATIONS = '4' process.env.DD_IAST_COOKIE_FILTER_PATTERN = '.*' + process.env.DD_IAST_DB_ROWS_TO_TAINT = 2 process.env.DD_IAST_DEDUPLICATION_ENABLED = false process.env.DD_IAST_REDACTION_ENABLED = false process.env.DD_IAST_REDACTION_NAME_PATTERN = 'REDACTION_NAME_PATTERN' @@ -504,11 +516,13 @@ describe('Config', () => { process.env.DD_PROFILING_ENABLED = 'true' process.env.DD_INJECTION_ENABLED = 'profiler' process.env.DD_API_SECURITY_ENABLED = 'true' - process.env.DD_API_SECURITY_REQUEST_SAMPLE_RATE = 1 + process.env.DD_API_SECURITY_SAMPLE_DELAY = '25' process.env.DD_INSTRUMENTATION_INSTALL_ID = '68e75c48-57ca-4a12-adfc-575c4b05fcbe' process.env.DD_INSTRUMENTATION_INSTALL_TYPE = 'k8s_single_step' process.env.DD_INSTRUMENTATION_INSTALL_TIME = '1703188212' process.env.DD_INSTRUMENTATION_CONFIG_ID = 'abcdef123' + process.env.DD_LANGCHAIN_SPAN_CHAR_LIMIT = 50 + process.env.DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE = 0.5 process.env.DD_LLMOBS_AGENTLESS_ENABLED = 'true' process.env.DD_LLMOBS_ML_APP = 'myMlApp' process.env.DD_TRACE_ENABLED = 'true' @@ -531,7 +545,7 @@ describe('Config', () => { expect(config).to.have.property('queryStringObfuscation', '.*') expect(config).to.have.property('clientIpEnabled', true) expect(config).to.have.property('clientIpHeader', 'x-true-client-ip') - expect(config).to.have.nested.property('crashtracking.enabled', true) + expect(config).to.have.nested.property('crashtracking.enabled', false) 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('runtimeMetrics', true) @@ -591,10 +605,9 @@ describe('Config', () => { 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.blockedTemplateGraphql', BLOCKED_TEMPLATE_GRAPHQL) - expect(config).to.have.nested.property('appsec.eventTracking.enabled', true) expect(config).to.have.nested.property('appsec.eventTracking.mode', 'extended') expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.requestSampling', 1) + expect(config).to.have.nested.property('appsec.apiSecurity.sampleDelay', 25) expect(config).to.have.nested.property('appsec.sca.enabled', true) expect(config).to.have.nested.property('appsec.standalone.enabled', true) expect(config).to.have.nested.property('remoteConfig.enabled', false) @@ -604,6 +617,7 @@ describe('Config', () => { 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.cookieFilterPattern', '.*') + 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.redactionEnabled', false) expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') @@ -623,6 +637,7 @@ describe('Config', () => { { name: 'appsec.blockedTemplateHtml', value: BLOCKED_TEMPLATE_HTML_PATH, origin: 'env_var' }, { name: 'appsec.blockedTemplateJson', value: BLOCKED_TEMPLATE_JSON_PATH, origin: 'env_var' }, { name: 'appsec.enabled', value: true, origin: 'env_var' }, + { name: 'appsec.eventTracking.mode', value: 'extended', origin: 'env_var' }, { name: 'appsec.obfuscatorKeyRegex', value: '.*', origin: 'env_var' }, { name: 'appsec.obfuscatorValueRegex', value: '.*', origin: 'env_var' }, { name: 'appsec.rateLimit', value: '42', origin: 'env_var' }, @@ -636,7 +651,7 @@ describe('Config', () => { { name: 'appsec.wafTimeout', value: '42', origin: 'env_var' }, { name: 'clientIpEnabled', value: true, origin: 'env_var' }, { name: 'clientIpHeader', value: 'x-true-client-ip', origin: 'env_var' }, - { name: 'crashtracking.enabled', value: true, origin: 'env_var' }, + { name: 'crashtracking.enabled', value: false, origin: 'env_var' }, { name: 'codeOriginForSpans.enabled', value: true, origin: 'env_var' }, { name: 'dogstatsd.hostname', value: 'dsd-agent', origin: 'env_var' }, { name: 'dogstatsd.port', value: '5218', origin: 'env_var' }, @@ -647,6 +662,7 @@ describe('Config', () => { { name: 'experimental.runtimeId', value: true, origin: 'env_var' }, { name: 'hostname', value: 'agent', origin: 'env_var' }, { name: 'iast.cookieFilterPattern', value: '.*', origin: 'env_var' }, + { name: 'iast.dbRowsToTaint', value: 2, origin: 'env_var' }, { name: 'iast.deduplicationEnabled', value: false, origin: 'env_var' }, { name: 'iast.enabled', value: true, origin: 'env_var' }, { name: 'iast.maxConcurrentRequests', value: '3', origin: 'env_var' }, @@ -684,7 +700,9 @@ describe('Config', () => { { name: 'tracing', value: false, origin: 'env_var' }, { name: 'version', value: '1.0.0', origin: 'env_var' }, { name: 'llmobs.mlApp', value: 'myMlApp', origin: 'env_var' }, - { name: 'llmobs.agentlessEnabled', value: true, origin: 'env_var' } + { name: 'llmobs.agentlessEnabled', value: true, origin: 'env_var' }, + { name: 'langchain.spanCharLimit', value: 50, origin: 'env_var' }, + { name: 'langchain.spanPromptCompletionSampleRate', value: 0.5, origin: 'env_var' } ]) }) @@ -759,6 +777,15 @@ describe('Config', () => { expect(config).to.have.nested.deep.property('crashtracking.enabled', false) }) + it('should prioritize DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE over DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING', () => { + process.env.DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE = 'anonymous' + process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = 'extended' + + const config = new Config() + + expect(config).to.have.nested.property('appsec.eventTracking.mode', 'anonymous') + }) + it('should initialize from the options', () => { const logger = {} const tags = { @@ -834,6 +861,7 @@ describe('Config', () => { maxConcurrentRequests: 4, maxContextOperations: 5, cookieFilterPattern: '.*', + dbRowsToTaint: 2, deduplicationEnabled: false, redactionEnabled: false, redactionNamePattern: 'REDACTION_NAME_PATTERN', @@ -906,6 +934,7 @@ describe('Config', () => { 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.cookieFilterPattern', '.*') + 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.redactionEnabled', false) expect(config).to.have.nested.property('iast.redactionNamePattern', 'REDACTION_NAME_PATTERN') @@ -953,6 +982,7 @@ describe('Config', () => { { name: 'flushMinSpans', value: 500, origin: 'code' }, { name: 'hostname', value: 'agent', origin: 'code' }, { name: 'iast.cookieFilterPattern', value: '.*', origin: 'code' }, + { name: 'iast.dbRowsToTaint', value: 2, origin: 'code' }, { name: 'iast.deduplicationEnabled', value: false, origin: 'code' }, { name: 'iast.enabled', value: true, origin: 'code' }, { name: 'iast.maxConcurrentRequests', value: 4, origin: 'code' }, @@ -1173,11 +1203,12 @@ describe('Config', () => { process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML = BLOCKED_TEMPLATE_JSON_PATH // note the inversion between process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON = BLOCKED_TEMPLATE_HTML_PATH // json and html here process.env.DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON = BLOCKED_TEMPLATE_JSON_PATH // json and html here + process.env.DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE = 'disabled' process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = 'disabled' process.env.DD_API_SECURITY_ENABLED = 'false' - process.env.DD_API_SECURITY_REQUEST_SAMPLE_RATE = 0.5 process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS = 11 process.env.DD_IAST_ENABLED = 'false' + process.env.DD_IAST_DB_ROWS_TO_TAINT = '2' process.env.DD_IAST_COOKIE_FILTER_PATTERN = '.*' process.env.DD_IAST_REDACTION_NAME_PATTERN = 'name_pattern_to_be_overriden_by_options' process.env.DD_IAST_REDACTION_VALUE_PATTERN = 'value_pattern_to_be_overriden_by_options' @@ -1238,11 +1269,10 @@ describe('Config', () => { blockedTemplateJson: BLOCKED_TEMPLATE_JSON_PATH, blockedTemplateGraphql: BLOCKED_TEMPLATE_GRAPHQL_PATH, eventTracking: { - mode: 'safe' + mode: 'anonymous' }, apiSecurity: { - enabled: true, - requestSampling: 1.0 + enabled: true }, rasp: { enabled: false @@ -1256,6 +1286,7 @@ describe('Config', () => { iast: { enabled: true, cookieFilterPattern: '.{10,}', + dbRowsToTaint: 3, redactionNamePattern: 'REDACTION_NAME_PATTERN', redactionValuePattern: 'REDACTION_VALUE_PATTERN' }, @@ -1317,15 +1348,14 @@ describe('Config', () => { 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.blockedTemplateGraphql', BLOCKED_TEMPLATE_GRAPHQL) - expect(config).to.have.nested.property('appsec.eventTracking.enabled', true) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'safe') + expect(config).to.have.nested.property('appsec.eventTracking.mode', 'anonymous') expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) - expect(config).to.have.nested.property('appsec.apiSecurity.requestSampling', 1.0) expect(config).to.have.nested.property('remoteConfig.pollInterval', 42) expect(config).to.have.nested.property('iast.enabled', true) expect(config).to.have.nested.property('iast.requestSampling', 30) 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.dbRowsToTaint', 3) expect(config).to.have.nested.property('iast.deduplicationEnabled', true) expect(config).to.have.nested.property('iast.cookieFilterPattern', '.{10,}') expect(config).to.have.nested.property('iast.redactionEnabled', true) @@ -1351,8 +1381,7 @@ describe('Config', () => { mode: 'disabled' }, apiSecurity: { - enabled: true, - requestSampling: 1.0 + enabled: true }, rasp: { enabled: false @@ -1364,6 +1393,7 @@ describe('Config', () => { maxConcurrentRequests: 3, maxContextOperations: 4, cookieFilterPattern: '.*', + dbRowsToTaint: 3, deduplicationEnabled: false, redactionEnabled: false, redactionNamePattern: 'REDACTION_NAME_PATTERN', @@ -1382,11 +1412,10 @@ describe('Config', () => { blockedTemplateJson: BLOCKED_TEMPLATE_JSON_PATH, blockedTemplateGraphql: BLOCKED_TEMPLATE_GRAPHQL_PATH, eventTracking: { - mode: 'safe' + mode: 'anonymous' }, apiSecurity: { - enabled: false, - requestSampling: 0.5 + enabled: false }, rasp: { enabled: true @@ -1398,6 +1427,7 @@ describe('Config', () => { maxConcurrentRequests: 6, maxContextOperations: 7, cookieFilterPattern: '.{10,}', + dbRowsToTaint: 2, deduplicationEnabled: true, redactionEnabled: true, redactionNamePattern: 'IGNORED_REDACTION_NAME_PATTERN', @@ -1418,12 +1448,11 @@ describe('Config', () => { blockedTemplateJson: undefined, blockedTemplateGraphql: undefined, eventTracking: { - enabled: false, mode: 'disabled' }, apiSecurity: { enabled: true, - requestSampling: 1.0 + sampleDelay: 30 }, sca: { enabled: null @@ -1447,6 +1476,7 @@ describe('Config', () => { maxConcurrentRequests: 3, maxContextOperations: 4, cookieFilterPattern: '.*', + dbRowsToTaint: 3, deduplicationEnabled: false, redactionEnabled: false, redactionNamePattern: 'REDACTION_NAME_PATTERN', @@ -1576,7 +1606,7 @@ describe('Config', () => { 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.false + expect(config.telemetry.logCollection).to.be.true expect(config.telemetry.debug).to.be.false expect(config.telemetry.metrics).to.be.true }) @@ -1614,7 +1644,7 @@ describe('Config', () => { process.env.DD_TELEMETRY_METRICS_ENABLED = origTelemetryMetricsEnabledValue }) - it('should not set DD_TELEMETRY_LOG_COLLECTION_ENABLED', () => { + it('should disable log collection if DD_TELEMETRY_LOG_COLLECTION_ENABLED is false', () => { const origLogsValue = process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED = 'false' @@ -1625,17 +1655,6 @@ describe('Config', () => { process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED = origLogsValue }) - it('should set DD_TELEMETRY_LOG_COLLECTION_ENABLED if DD_IAST_ENABLED', () => { - const origIastEnabledValue = process.env.DD_IAST_ENABLED - process.env.DD_IAST_ENABLED = 'true' - - const config = new Config() - - expect(config.telemetry.logCollection).to.be.true - - process.env.DD_IAST_ENABLED = origIastEnabledValue - }) - it('should set DD_TELEMETRY_DEBUG', () => { const origTelemetryDebugValue = process.env.DD_TELEMETRY_DEBUG process.env.DD_TELEMETRY_DEBUG = 'true' @@ -1791,9 +1810,12 @@ describe('Config', () => { }) expect(log.error).to.be.callCount(3) - expect(log.error.firstCall).to.have.been.calledWithExactly(error) - expect(log.error.secondCall).to.have.been.calledWithExactly(error) - expect(log.error.thirdCall).to.have.been.calledWithExactly(error) + 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) expect(config.appsec.enabled).to.be.true expect(config.appsec.rules).to.eq('path/to/rules.json') @@ -1827,6 +1849,15 @@ describe('Config', () => { expect(config.appsec.apiSecurity.enabled).to.be.true }) + it('should prioritize DD_DOGSTATSD_HOST over DD_DOGSTATSD_HOSTNAME', () => { + process.env.DD_DOGSTATSD_HOSTNAME = 'dsd-agent' + process.env.DD_DOGSTATSD_HOST = 'localhost' + + const config = new Config() + + expect(config).to.have.nested.property('dogstatsd.hostname', 'localhost') + }) + context('auto configuration w/ unix domain sockets', () => { context('on windows', () => { it('should not be used', () => { @@ -2180,35 +2211,6 @@ describe('Config', () => { }) }) - it('should sanitize values for API Security sampling between 0 and 1', () => { - expect(new Config({ - appsec: { - apiSecurity: { - enabled: true, - requestSampling: 5 - } - } - })).to.have.nested.property('appsec.apiSecurity.requestSampling', 1) - - expect(new Config({ - appsec: { - apiSecurity: { - enabled: true, - requestSampling: -5 - } - } - })).to.have.nested.property('appsec.apiSecurity.requestSampling', 0) - - expect(new Config({ - appsec: { - apiSecurity: { - enabled: true, - requestSampling: 0.1 - } - } - })).to.have.nested.property('appsec.apiSecurity.requestSampling', 0.1) - }) - context('payload tagging', () => { let env @@ -2286,5 +2288,76 @@ describe('Config', () => { expect(taggingConfig).to.have.property('responsesEnabled', true) expect(taggingConfig).to.have.property('maxDepth', 7) }) + + it('config_norm_rules completeness', () => { + // ⚠️ Did this test just fail? Read here! ⚠️ + // + // Some files are manually copied from dd-go from/to the following paths + // from: https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-payload/static/ + // to: packages/dd-trace/test/fixtures/telemetry/ + // files: + // - config_norm_rules.json + // - config_prefix_block_list.json + // - config_aggregation_list.json + // - nodejs_config_rules.json + // + // If this test fails, it means that a telemetry key was found in config.js that does not + // exist in any of the files listed above in dd-go + // The impact is that telemetry will not be reported to the Datadog backend won't be unusable + // + // To fix this, you must update dd-go to either + // 1) Add an exact config key to match config_norm_rules.json + // 2) Add a prefix that matches the config keys to config_prefix_block_list.json + // 3) Add a prefix rule that fits an existing prefix to config_aggregation_list.json + // 4) (Discouraged) Add a language-specific rule to nodejs_config_rules.json + // + // Once dd-go is updated, you can copy over the files to this repo and merge them in as part of your changes + + function getKeysInDotNotation (obj, parentKey = '') { + const keys = [] + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const fullKey = parentKey ? `${parentKey}.${key}` : key + + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + keys.push(...getKeysInDotNotation(obj[key], fullKey)) + } else { + keys.push(fullKey) + } + } + } + + return keys + } + + const config = new Config() + + const libraryConfigKeys = getKeysInDotNotation(config).sort() + + const nodejsConfigRules = JSON.parse(NODEJS_CONFIG_RULES) + const configNormRules = JSON.parse(CONFIG_NORM_RULES) + const configPrefixBlockList = JSON.parse(CONFIG_PREFIX_BLOCK_LIST) + const configAggregationList = JSON.parse(CONFIG_AGGREGATION_LIST) + + const allowedConfigKeys = [ + ...Object.keys(configNormRules), + ...Object.keys(nodejsConfigRules.normalization_rules) + ] + const blockedConfigKeyPrefixes = [...configPrefixBlockList, ...nodejsConfigRules.prefix_block_list] + const configAggregationPrefixes = [ + ...Object.keys(configAggregationList), + ...Object.keys(nodejsConfigRules.reduce_rules) + ] + + const missingConfigKeys = libraryConfigKeys.filter(key => { + const isAllowed = allowedConfigKeys.includes(key) + const isBlocked = blockedConfigKeyPrefixes.some(prefix => key.startsWith(prefix)) + const isReduced = configAggregationPrefixes.some(prefix => key.startsWith(prefix)) + return !isAllowed && !isBlocked && !isReduced + }) + + expect(missingConfigKeys).to.be.empty + }) }) }) diff --git a/packages/dd-trace/test/crashtracking/crashtracker.spec.js b/packages/dd-trace/test/crashtracking/crashtracker.spec.js index 9f1c0a81112..75d97e6ce55 100644 --- a/packages/dd-trace/test/crashtracking/crashtracker.spec.js +++ b/packages/dd-trace/test/crashtracking/crashtracker.spec.js @@ -73,6 +73,15 @@ describe('crashtracking', () => { expect(() => crashtracker.start(config)).to.not.throw() }) + + it('should handle unix sockets', () => { + config.url = new URL('unix:///var/datadog/apm/test.socket') + + crashtracker.start(config) + + expect(binding.init).to.have.been.called + expect(log.error).to.not.have.been.called + }) }) describe('configure', () => { diff --git a/packages/dd-trace/test/datastreams/processor.spec.js b/packages/dd-trace/test/datastreams/processor.spec.js index 0c30bc77947..110d9ff6c35 100644 --- a/packages/dd-trace/test/datastreams/processor.spec.js +++ b/packages/dd-trace/test/datastreams/processor.spec.js @@ -294,11 +294,11 @@ describe('DataStreamsProcessor', () => { Service: 'service1', Version: 'v1', Stats: [{ - Start: new Uint64(1680000000000), - Duration: new Uint64(10000000000), + Start: 1680000000000n, + Duration: 10000000000n, Stats: [{ - Hash: new Uint64(DEFAULT_CURRENT_HASH), - ParentHash: new Uint64(DEFAULT_PARENT_HASH), + Hash: DEFAULT_CURRENT_HASH.readBigUInt64BE(), + ParentHash: DEFAULT_PARENT_HASH.readBigUInt64BE(), EdgeTags: mockCheckpoint.edgeTags, EdgeLatency: edgeLatency.toProto(), PathwayLatency: pathwayLatency.toProto(), 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 41433f453c5..365d86d6e96 100644 --- a/packages/dd-trace/test/debugger/devtools_client/status.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/status.spec.js @@ -79,7 +79,7 @@ describe('diagnostic message http request caching', function () { function assertRequestData (request, { probeId, version, status, exception }) { const payload = getFormPayload(request) - const diagnostics = { probeId, runtimeId, version, status } + const diagnostics = { probeId, runtimeId, probeVersion: version, status } // Error requests will also contain an `exception` property if (exception) diagnostics.exception = exception diff --git a/packages/dd-trace/test/debugger/devtools_client/utils.js b/packages/dd-trace/test/debugger/devtools_client/utils.js new file mode 100644 index 00000000000..e15d567a7c1 --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/utils.js @@ -0,0 +1,25 @@ +'use strict' + +const { randomUUID } = require('node:crypto') + +module.exports = { + generateProbeConfig +} + +function generateProbeConfig (breakpoint, overrides = {}) { + overrides.capture = { maxReferenceDepth: 3, ...overrides.capture } + overrides.sampling = { snapshotsPerSecond: 5000, ...overrides.sampling } + return { + id: randomUUID(), + version: 0, + type: 'LOG_PROBE', + language: 'javascript', + where: { sourceFile: breakpoint.file, lines: [String(breakpoint.line)] }, + tags: [], + template: 'Hello World!', + segments: [{ str: 'Hello World!' }], + captureSnapshot: false, + evaluateAt: 'EXIT', + ...overrides + } +} 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 54ddab1a2a6..259ff78df2e 100644 --- a/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js +++ b/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js @@ -67,14 +67,14 @@ describe('agentless-ci-visibility-encode', () => { const buffer = encoder.makePayload() const decodedTrace = msgpack.decode(buffer, { codec }) - expect(decodedTrace.version.toNumber()).to.equal(1) + expect(decodedTrace.version).to.equal(1) expect(decodedTrace.metadata['*']).to.contain({ language: 'javascript', library_version: ddTraceVersion }) const spanEvent = decodedTrace.events[0] expect(spanEvent.type).to.equal('span') - expect(spanEvent.version.toNumber()).to.equal(1) + expect(spanEvent.version).to.equal(1) expect(spanEvent.content.trace_id.toString(10)).to.equal(trace[0].trace_id.toString(10)) expect(spanEvent.content.span_id.toString(10)).to.equal(trace[0].span_id.toString(10)) expect(spanEvent.content.parent_id.toString(10)).to.equal(trace[0].parent_id.toString(10)) @@ -84,9 +84,9 @@ describe('agentless-ci-visibility-encode', () => { service: 'test-s', type: 'foo' }) - expect(spanEvent.content.error.toNumber()).to.equal(0) - expect(spanEvent.content.start.toNumber()).to.equal(123) - expect(spanEvent.content.duration.toNumber()).to.equal(456) + expect(spanEvent.content.error).to.equal(0) + expect(spanEvent.content.start).to.equal(123) + expect(spanEvent.content.duration).to.equal(456) expect(spanEvent.content.meta).to.eql({ bar: 'baz' @@ -276,6 +276,6 @@ describe('agentless-ci-visibility-encode', () => { const decodedTrace = msgpack.decode(buffer, { codec }) const spanEvent = decodedTrace.events[0] expect(spanEvent.type).to.equal('span') - expect(spanEvent.version.toNumber()).to.equal(1) + expect(spanEvent.version).to.equal(1) }) }) diff --git a/packages/dd-trace/test/exporters/agent/writer.spec.js b/packages/dd-trace/test/exporters/agent/writer.spec.js index ce7a62d49bf..aad8749ef37 100644 --- a/packages/dd-trace/test/exporters/agent/writer.spec.js +++ b/packages/dd-trace/test/exporters/agent/writer.spec.js @@ -149,6 +149,7 @@ function describeWriter (protocolVersion) { it('should log request errors', done => { const error = new Error('boom') + error.status = 42 request.yields(error) @@ -156,7 +157,8 @@ function describeWriter (protocolVersion) { writer.flush() setTimeout(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error) + .to.have.been.calledWith('Error sending payload to the agent (status code: %s)', error.status, error) done() }) }) diff --git a/packages/dd-trace/test/exporters/common/docker.spec.js b/packages/dd-trace/test/exporters/common/docker.spec.js index dd1610c8e60..2c2bc9275b8 100644 --- a/packages/dd-trace/test/exporters/common/docker.spec.js +++ b/packages/dd-trace/test/exporters/common/docker.spec.js @@ -53,7 +53,7 @@ describe('docker', () => { it('should support IDs with Kubernetes format', () => { const cgroup = [ - '1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2d3da189_6407_48e3_9ab6_78188d75e609.slice/docker-7b8952daecf4c0e44bbcefe1b5c5ebc7b4839d4eefeccefe694709d3809b6199.scope' // eslint-disable-line max-len + '1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2d3da189_6407_48e3_9ab6_78188d75e609.slice/docker-7b8952daecf4c0e44bbcefe1b5c5ebc7b4839d4eefeccefe694709d3809b6199.scope' // eslint-disable-line @stylistic/js/max-len ].join('\n') fs.readFileSync.withArgs('/proc/self/cgroup').returns(Buffer.from(cgroup)) diff --git a/packages/dd-trace/test/exporters/common/request.spec.js b/packages/dd-trace/test/exporters/common/request.spec.js index 55bcb603a27..a6efcc45fa6 100644 --- a/packages/dd-trace/test/exporters/common/request.spec.js +++ b/packages/dd-trace/test/exporters/common/request.spec.js @@ -429,7 +429,7 @@ describe('request', function () { 'accept-encoding': 'gzip' } }, (err, res) => { - expect(log.error).to.have.been.calledWith('Could not gunzip response: unexpected end of file') + expect(log.error).to.have.been.calledWith('Could not gunzip response: %s', 'unexpected end of file') expect(res).to.equal('') done(err) }) 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 d65d480409d..f8e65500e04 100644 --- a/packages/dd-trace/test/exporters/span-stats/writer.spec.js +++ b/packages/dd-trace/test/exporters/span-stats/writer.spec.js @@ -106,7 +106,7 @@ describe('span-stats writer', () => { encoder.count.returns(1) writer.flush(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error).to.have.been.calledWith('Error sending span stats', error) done() }) }) diff --git a/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs b/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs index 9f9bd110f04..ea6a7ab34fe 100644 --- a/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs +++ b/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs @@ -20,6 +20,7 @@ esmHook(['express', 'os'], (exports, name, baseDir) => { const { freemem } = await import('os') const expressResult = expressDefault() const express = typeof expressResult === 'function' ? 'express()' : expressResult + // eslint-disable-next-line no-console console.log(JSON.stringify({ express, freemem: freemem() diff --git a/packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json b/packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json new file mode 100644 index 00000000000..b23fc7ff760 --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json @@ -0,0 +1,24 @@ +{ + "tags": "tags", + "global_tag_": "global_tags", + "trace_global_tags": "trace_global_tags", + "DD_TAGS": "tags", + "trace_span_tags": "trace_span_tags", + "http_client_tag_headers": "http_client_tag_headers", + "DD_TRACE_HEADER_TAGS": "trace_header_tags", + "trace_header_tags": "trace_header_tags", + "_options.headertags": "trace_header_tags", + "trace_request_header_tags": "trace_request_header_tags", + "trace_response_header_tags": "trace_response_header_tags", + "trace_request_header_tags_comma_allowed": "trace_request_header_tags", + "trace.header_tags": "trace_header_tags", + "DD_TRACE_GRPC_TAGS": "trace_grpc_tags", + "DD_TRACE_SERVICE_MAPPING": "trace_service_mappings", + "service_mapping": "trace_service_mappings", + "serviceMapping.": "trace_service_mappings", + "logger.": "logger_configs", + "sampler.rules.": "sampler_rules", + "sampler.spansamplingrules.": "sampler_span_sampling_rules", + "appsec.rules.rules": "appsec_rules", + "installSignature": "install_signature" +} diff --git a/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json b/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json new file mode 100644 index 00000000000..d4014e8b839 --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json @@ -0,0 +1,808 @@ +{ + "AWS_LAMBDA_INITIALIZATION_TYPE": "aws_lambda_initialization_type", + "COMPUTERNAME": "aas_instance_name", + "DATADOG_TRACE_AGENT_HOSTNAME": "agent_host", + "DATADOG_TRACE_AGENT_PORT": "trace_agent_port", + "DD_AAS_DOTNET_EXTENSION_VERSION": "aas_site_extensions_version", + "DD_AAS_ENABLE_CUSTOM_METRICS": "aas_custom_metrics_enabled", + "DD_AAS_ENABLE_CUSTOM_TRACING": "aas_custom_tracing_enabled", + "DD_AGENT_TRANSPORT": "agent_transport", + "DD_API_SECURITY_ENABLED": "api_security_enabled", + "DD_API_SECURITY_MAX_CONCURRENT_REQUESTS": "api_security_max_concurrent_requests", + "DD_API_SECURITY_REQUEST_SAMPLE_RATE": "api_security_request_sample_rate", + "DD_API_SECURITY_SAMPLE_DELAY": "api_security_sample_delay", + "DD_APM_ENABLE_RARE_SAMPLER": "trace_rare_sampler_enabled", + "DD_APM_RECEIVER_PORT": "trace_agent_port", + "DD_APM_RECEIVER_SOCKET": "trace_agent_socket", + "DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING": "appsec_auto_user_events_tracking", + "DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": "appsec_auto_user_instrumentation_mode", + "DD_APPSEC_ENABLED": "appsec_enabled", + "DD_APPSEC_EXTRA_HEADERS": "appsec_extra_headers", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML": "appsec_blocked_template_html", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON": "appsec_blocked_template_json", + "DD_APPSEC_IPHEADER": "appsec_ip_header", + "DD_APPSEC_KEEP_TRACES": "appsec_force_keep_traces_enabled", + "DD_APPSEC_MAX_STACK_TRACES": "appsec_max_stack_traces", + "DD_APPSEC_MAX_STACK_TRACE_DEPTH": "appsec_max_stack_trace_depth", + "DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT": "appsec_max_stack_trace_depth_top_percent", + "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP": "appsec_obfuscation_parameter_key_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP": "appsec_obfuscation_parameter_value_regexp", + "DD_APPSEC_RASP_ENABLED": "appsec_rasp_enabled", + "DD_APPSEC_RULES": "appsec_rules", + "DD_APPSEC_SCA_ENABLED": "appsec_sca_enabled", + "DD_APPSEC_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", + "DD_APPSEC_TRACE_RATE_LIMIT": "appsec_trace_rate_limit", + "DD_APPSEC_WAF_DEBUG": "appsec_waf_debug_enabled", + "DD_APPSEC_WAF_TIMEOUT": "appsec_waf_timeout", + "DD_AZURE_APP_SERVICES": "aas_enabled", + "DD_CALL_BASIC_CONFIG": "dd_call_basic_config", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "ci_visibility_agentless_enabled", + "DD_CIVISIBILITY_AGENTLESS_URL": "ci_visibility_agentless_url", + "DD_CIVISIBILITY_CODE_COVERAGE_COLLECTORPATH": "ci_visibility_code_coverage_collectorpath", + "DD_CIVISIBILITY_CODE_COVERAGE_ENABLED": "ci_visibility_code_coverage_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_ENABLE_JIT_OPTIMIZATIONS": "ci_visibility_code_coverage_jit_optimisations_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_MODE": "ci_visibility_code_coverage_mode", + "DD_CIVISIBILITY_CODE_COVERAGE_PATH": "ci_visibility_code_coverage_path", + "DD_CIVISIBILITY_CODE_COVERAGE_SNK_FILEPATH": "ci_visibility_code_coverage_snk_path", + "DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED": "ci_visibility_early_flake_detection_enabled", + "DD_CIVISIBILITY_ENABLED": "ci_visibility_enabled", + "DD_CIVISIBILITY_EXTERNAL_CODE_COVERAGE_PATH": "ci_visibility_code_coverage_external_path", + "DD_CIVISIBILITY_FLAKY_RETRY_COUNT": "ci_visibility_flaky_retry_count", + "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "ci_visibility_flaky_retry_enabled", + "DD_CIVISIBILITY_FORCE_AGENT_EVP_PROXY": "ci_visibility_force_agent_evp_proxy_enabled", + "DD_CIVISIBILITY_GAC_INSTALL_ENABLED": "ci_visibility_gac_install_enabled", + "DD_CIVISIBILITY_GIT_UPLOAD_ENABLED": "ci_visibility_git_upload_enabled", + "DD_CIVISIBILITY_ITR_ENABLED": "ci_visibility_intelligent_test_runner_enabled", + "DD_CIVISIBILITY_LOGS_ENABLED": "ci_visibility_logs_enabled", + "DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS": "ci_visibility_rum_flush_wait_millis", + "DD_CIVISIBILITY_TESTSSKIPPING_ENABLED": "ci_visibility_test_skipping_enabled", + "DD_CIVISIBILITY_TOTAL_FLAKY_RETRY_COUNT": "ci_visibility_total_flaky_retry_count", + "DD_CODE_ORIGIN_FOR_SPANS_ENABLED": "code_origin_for_spans_enabled", + "DD_CODE_ORIGIN_FOR_SPANS_MAX_USER_FRAMES": "code_origin_for_spans_max_user_frames", + "DD_DATA_STREAMS_ENABLED": "data_streams_enabled", + "DD_DATA_STREAMS_LEGACY_HEADERS": "data_streams_legacy_headers", + "DD_DBM_PROPAGATION_MODE": "dbm_propagation_mode", + "DD_DEBUGGER_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", + "DD_DEBUGGER_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", + "DD_DEBUGGER_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", + "DD_DEBUGGER_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", + "DD_DEBUGGER_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", + "DD_DIAGNOSTIC_SOURCE_ENABLED": "trace_diagnostic_source_enabled", + "DD_DISABLED_INTEGRATIONS": "trace_disabled_integrations", + "DD_DOGSTATSD_ARGS": "agent_dogstatsd_executable_args", + "DD_DOGSTATSD_PATH": "agent_dogstatsd_executable_path", + "DD_DOGSTATSD_PIPE_NAME": "dogstatsd_named_pipe", + "DD_DOGSTATSD_PORT": "dogstatsd_port", + "DD_DOGSTATSD_SOCKET": "dogstatsd_socket", + "DD_DOGSTATSD_URL": "dogstatsd_url", + "DD_DOTNET_TRACER_CONFIG_FILE": "trace_config_file", + "DD_DYNAMIC_INSTRUMENTATION_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", + "DD_DYNAMIC_INSTRUMENTATION_ENABLED": "dynamic_instrumentation_enabled", + "DD_DYNAMIC_INSTRUMENTATION_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", + "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", + "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS": "dynamic_instrumentation_redacted_identifiers", + "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES": "dynamic_instrumentation_redacted_types", + "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "dynamic_instrumentation_symbol_database_batch_size_bytes", + "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_UPLOAD_ENABLED": "dynamic_instrumentation_symbol_database_upload_enabled", + "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", + "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", + "DD_ENV": "env", + "DD_EXCEPTION_DEBUGGING_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_debugging_capture_full_callstack_enabled", + "DD_EXCEPTION_DEBUGGING_ENABLED": "dd_exception_debugging_enabled", + "DD_EXCEPTION_DEBUGGING_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_debugging_max_exception_analysis_limit", + "DD_EXCEPTION_DEBUGGING_MAX_FRAMES_TO_CAPTURE": "dd_exception_debugging_max_frames_to_capture", + "DD_EXCEPTION_DEBUGGING_RATE_LIMIT_SECONDS": "dd_exception_debugging_rate_limit_seconds", + "DD_EXCEPTION_REPLAY_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_replay_capture_full_callstack_enabled", + "DD_EXCEPTION_REPLAY_ENABLED": "dd_exception_replay_enabled", + "DD_EXCEPTION_REPLAY_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_replay_max_exception_analysis_limit", + "DD_EXCEPTION_REPLAY_MAX_FRAMES_TO_CAPTURE": "dd_exception_replay_max_frames_to_capture", + "DD_EXCEPTION_REPLAY_RATE_LIMIT_SECONDS": "dd_exception_replay_rate_limit_seconds", + "DD_EXPERIMENTAL_API_SECURITY_ENABLED": "experimental_api_security_enabled", + "DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED": "experimental_appsec_standalone_enabled", + "DD_EXPERIMENTAL_APPSEC_USE_UNSAFE_ENCODER": "appsec_use_unsafe_encoder", + "DD_GIT_COMMIT_SHA": "commit_sha", + "DD_GIT_REPOSITORY_URL": "repository_url", + "DD_GRPC_CLIENT_ERROR_STATUSES": "trace_grpc_client_error_statuses", + "DD_GRPC_SERVER_ERROR_STATUSES": "trace_grpc_server_error_statuses", + "DD_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", + "DD_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", + "DD_HTTP_SERVER_TAG_QUERY_STRING": "trace_http_server_tag_query_string_enabled", + "DD_HTTP_SERVER_TAG_QUERY_STRING_SIZE": "trace_http_server_tag_query_string_size", + "DD_IAST_COOKIE_FILTER_PATTERN": "iast_cookie_filter_pattern", + "DD_IAST_DB_ROWS_TO_TAINT": "iast_db_rows_to_taint", + "DD_IAST_DEDUPLICATION_ENABLED": "iast_deduplication_enabled", + "DD_IAST_ENABLED": "iast_enabled", + "DD_IAST_MAX_CONCURRENT_REQUESTS": "iast_max_concurrent_requests", + "DD_IAST_MAX_RANGE_COUNT": "iast_max_range_count", + "DD_IAST_REDACTION_ENABLED": "iast_redaction_enabled", + "DD_IAST_REDACTION_KEYS_REGEXP": "iast_redaction_keys_regexp", + "DD_IAST_REDACTION_NAME_PATTERN": "iast_redaction_name_pattern", + "DD_IAST_REDACTION_REGEXP_TIMEOUT": "iast_redaction_regexp_timeout", + "DD_IAST_REDACTION_VALUES_REGEXP": "iast_redaction_values_regexp", + "DD_IAST_REDACTION_VALUE_PATTERN": "iast_redaction_value_pattern", + "DD_IAST_REGEXP_TIMEOUT": "iast_regexp_timeout", + "DD_IAST_REQUEST_SAMPLING": "iast_request_sampling_percentage", + "DD_IAST_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", + "DD_IAST_TELEMETRY_VERBOSITY": "iast_telemetry_verbosity", + "DD_IAST_TRUNCATION_MAX_VALUE_LENGTH": "iast_truncation_max_value_length", + "DD_IAST_VULNERABILITIES_PER_REQUEST": "iast_vulnerability_per_request", + "DD_IAST_WEAK_CIPHER_ALGORITHMS": "iast_weak_cipher_algorithms", + "DD_IAST_WEAK_HASH_ALGORITHMS": "iast_weak_hash_algorithms", + "DD_INJECTION_ENABLED": "ssi_injection_enabled", + "DD_INJECT_FORCE": "ssi_forced_injection_enabled", + "DD_INJECT_FORCED": "dd_lib_injection_forced", + "DD_INSTRUMENTATION_TELEMETRY_AGENTLESS_ENABLED": "instrumentation_telemetry_agentless_enabled", + "DD_INSTRUMENTATION_TELEMETRY_AGENT_PROXY_ENABLED": "instrumentation_telemetry_agent_proxy_enabled", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "instrumentation_telemetry_enabled", + "DD_INSTRUMENTATION_TELEMETRY_URL": "instrumentation_telemetry_agentless_url", + "DD_INTERAL_FORCE_SYMBOL_DATABASE_UPLOAD": "internal_force_symbol_database_upload", + "DD_INTERNAL_RCM_POLL_INTERVAL": "remote_config_poll_interval", + "DD_INTERNAL_TELEMETRY_DEBUG_ENABLED": "instrumentation_telemetry_debug_enabled", + "DD_INTERNAL_TELEMETRY_V2_ENABLED": "instrumentation_telemetry_v2_enabled", + "DD_INTERNAL_WAIT_FOR_DEBUGGER_ATTACH": "internal_wait_for_debugger_attach_enabled", + "DD_INTERNAL_WAIT_FOR_NATIVE_DEBUGGER_ATTACH": "internal_wait_for_native_debugger_attach_enabled", + "DD_LIB_INJECTED": "dd_lib_injected", + "DD_LIB_INJECTION_ATTEMPTED": "dd_lib_injection_attempted", + "DD_LOGS_DIRECT_SUBMISSION_BATCH_PERIOD_SECONDS": "logs_direct_submission_batch_period_seconds", + "DD_LOGS_DIRECT_SUBMISSION_HOST": "logs_direct_submission_host", + "DD_LOGS_DIRECT_SUBMISSION_INTEGRATIONS": "logs_direct_submission_integrations", + "DD_LOGS_DIRECT_SUBMISSION_MAX_BATCH_SIZE": "logs_direct_submission_max_batch_size", + "DD_LOGS_DIRECT_SUBMISSION_MAX_QUEUE_SIZE": "logs_direct_submission_max_queue_size", + "DD_LOGS_DIRECT_SUBMISSION_MINIMUM_LEVEL": "logs_direct_submission_minimum_level", + "DD_LOGS_DIRECT_SUBMISSION_SOURCE": "logs_direct_submission_source", + "DD_LOGS_DIRECT_SUBMISSION_TAGS": "logs_direct_submission_tags", + "DD_LOGS_DIRECT_SUBMISSION_URL": "logs_direct_submission_url", + "DD_LOGS_INJECTION": "logs_injection_enabled", + "DD_LOG_INJECTION": "logs_injection_enabled", + "DD_LOG_LEVEL": "agent_log_level", + "DD_MAX_LOGFILE_SIZE": "trace_log_file_max_size", + "DD_MAX_TRACES_PER_SECOND": "trace_rate_limit", + "DD_PROFILING_CODEHOTSPOTS_ENABLED": "profiling_codehotspots_enabled", + "DD_PROFILING_ENABLED": "profiling_enabled", + "DD_PROFILING_ENDPOINT_COLLECTION_ENABLED": "profiling_endpoint_collection_enabled", + "DD_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", + "DD_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", + "DD_PROXY_HTTPS": "proxy_https", + "DD_PROXY_NO_PROXY": "proxy_no_proxy", + "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": "remote_config_poll_interval", + "DD_RUNTIME_METRICS_ENABLED": "runtime_metrics_enabled", + "DD_SERVICE": "service", + "DD_SERVICE_MAPPING": "dd_service_mapping", + "DD_SERVICE_NAME": "service", + "DD_SITE": "site", + "DD_SPAN_SAMPLING_RULES": "span_sample_rules", + "DD_SPAN_SAMPLING_RULES_FILE": "dd_span_sampling_rules_file", + "DD_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "symbol_database_batch_size_bytes", + "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_EXCLUDES": "symbol_database_third_party_detection_excludes", + "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_INCLUDES": "symbol_database_third_party_detection_includes", + "DD_SYMBOL_DATABASE_UPLOAD_ENABLED": "symbol_database_upload_enabled", + "DD_SYMBOL_DATABASE_COMPRESSION_ENABLED": "symbol_database_compression_enabled", + "DD_TAGS": "agent_tags", + "DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED": "instrumentation_telemetry_dependency_collection_enabled", + "DD_TELEMETRY_HEARTBEAT_INTERVAL": "instrumentation_telemetry_heartbeat_interval", + "DD_TELEMETRY_LOG_COLLECTION_ENABLED": "instrumentation_telemetry_log_collection_enabled", + "DD_TELEMETRY_METRICS_ENABLED": "instrumentation_telemetry_metrics_enabled", + "DD_TEST_SESSION_NAME": "test_session_name", + "DD_THIRD_PARTY_DETECTION_EXCLUDES": "third_party_detection_excludes", + "DD_THIRD_PARTY_DETECTION_INCLUDES": "third_party_detection_includes", + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": "trace_128_bits_id_enabled", + "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": "trace_128_bits_id_logging_enabled", + "DD_TRACE_ACTIVITY_LISTENER_ENABLED": "trace_activity_listener_enabled", + "DD_TRACE_AGENT_ARGS": "agent_trace_agent_excecutable_args", + "DD_TRACE_AGENT_HOSTNAME": "agent_host", + "DD_TRACE_AGENT_PATH": "agent_trace_agent_excecutable_path", + "DD_TRACE_AGENT_PORT": "trace_agent_port", + "DD_TRACE_AGENT_URL": "trace_agent_url", + "DD_TRACE_ANALYTICS_ENABLED": "trace_analytics_enabled", + "DD_TRACE_BAGGAGE_MAX_BYTES": "trace_baggage_max_bytes", + "DD_TRACE_BAGGAGE_MAX_ITEMS": "trace_baggage_max_items", + "DD_TRACE_BATCH_INTERVAL": "trace_serialization_batch_interval", + "DD_TRACE_BUFFER_SIZE": "trace_serialization_buffer_size", + "DD_TRACE_CLIENT_IP_ENABLED": "trace_client_ip_enabled", + "DD_TRACE_CLIENT_IP_HEADER": "trace_client_ip_header", + "DD_TRACE_COMMANDS_COLLECTION_ENABLED": "trace_commands_collection_enabled", + "DD_TRACE_COMPUTE_STATS": "dd_trace_compute_stats", + "DD_TRACE_CONFIG_FILE": "trace_config_file", + "DD_TRACE_DEBUG": "trace_debug_enabled", + "DD_TRACE_DEBUG_LOOKUP_FALLBACK": "trace_lookup_fallback_enabled", + "DD_TRACE_DEBUG_LOOKUP_MDTOKEN": "trace_lookup_mdtoken_enabled", + "DD_TRACE_DELAY_WCF_INSTRUMENTATION_ENABLED": "trace_delay_wcf_instrumentation_enabled", + "DD_TRACE_DELEGATE_SAMPLING": "trace_sample_delegation", + "DD_TRACE_DISABLED_ADONET_COMMAND_TYPES": "trace_disabled_adonet_command_types", + "DD_TRACE_ENABLED": "trace_enabled", + "DD_TRACE_EXPAND_ROUTE_TEMPLATES_ENABLED": "trace_route_template_expansion_enabled", + "DD_TRACE_GIT_METADATA_ENABLED": "git_metadata_enabled", + "DD_TRACE_GLOBAL_TAGS": "trace_tags", + "DD_TRACE_HEADER_TAGS": "trace_header_tags", + "DD_TRACE_HEADER_TAG_NORMALIZATION_FIX_ENABLED": "trace_header_tag_normalization_fix_enabled", + "DD_TRACE_HEALTH_METRICS_ENABLED": "dd_trace_health_metrics_enabled", + "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", + "DD_TRACE_HTTP_CLIENT_EXCLUDED_URL_SUBSTRINGS": "trace_http_client_excluded_urls", + "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING": "trace_http_client_tag_query_string", + "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", + "DD_TRACE_KAFKA_CREATE_CONSUMER_SCOPE_ENABLED": "trace_kafka_create_consumer_scope_enabled", + "DD_TRACE_LOGFILE_RETENTION_DAYS": "trace_log_file_retention_days", + "DD_TRACE_LOGGING_RATE": "trace_log_rate", + "DD_TRACE_LOG_DIRECTORY": "trace_log_directory", + "DD_TRACE_LOG_PATH": "trace_log_path", + "DD_TRACE_LOG_SINKS": "trace_log_sinks", + "DD_TRACE_METHODS": "trace_methods", + "DD_TRACE_METRICS_ENABLED": "trace_metrics_enabled", + "DD_TRACE_OBFUSCATION_QUERY_STRING_PATTERN": "dd_trace_obfuscation_query_string_pattern", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP": "trace_obfuscation_query_string_regexp", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP_TIMEOUT": "trace_obfuscation_query_string_regexp_timeout", + "DD_TRACE_OTEL_ENABLED": "trace_otel_enabled", + "DD_TRACE_OTEL_LEGACY_OPERATION_NAME_ENABLED": "trace_otel_legacy_operation_name_enabled", + "DD_TRACE_PARTIAL_FLUSH_ENABLED": "trace_partial_flush_enabled", + "DD_TRACE_PARTIAL_FLUSH_MIN_SPANS": "trace_partial_flush_min_spans", + "DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED": "trace_peer_service_defaults_enabled", + "DD_TRACE_PEER_SERVICE_MAPPING": "trace_peer_service_mapping", + "DD_TRACE_PIPE_NAME": "trace_agent_named_pipe", + "DD_TRACE_PIPE_TIMEOUT_MS": "trace_agent_named_pipe_timeout_ms", + "DD_TRACE_PROPAGATION_EXTRACT_FIRST": "trace_propagation_extract_first", + "DD_TRACE_PROPAGATION_STYLE": "trace_propagation_style", + "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", + "DD_TRACE_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", + "DD_TRACE_RATE_LIMIT": "trace_rate_limit", + "DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED": "trace_remove_integration_service_names_enabled", + "DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED": "trace_route_template_resource_names_enabled", + "DD_TRACE_SAMPLING_RULES": "trace_sample_rules", + "DD_TRACE_SAMPLING_RULES_FORMAT": "trace_sampling_rules_format", + "DD_TRACE_SPAN_ATTRIBUTE_SCHEMA": "trace_span_attribute_schema", + "DD_TRACE_STARTUP_LOGS": "trace_startup_logs_enabled", + "DD_TRACE_STATS_COMPUTATION_ENABLED": "trace_stats_computation_enabled", + "DD_TRACE_WCF_RESOURCE_OBFUSCATION_ENABLED": "trace_wcf_obfuscation_enabled", + "DD_TRACE_WCF_WEB_HTTP_RESOURCE_NAMES_ENABLED": "trace_wcf_web_http_resource_names_enabled", + "DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH": "trace_x_datadog_tags_max_length", + "DD_VERSION": "application_version", + "FUNCTIONS_EXTENSION_VERSION": "aas_functions_runtime_version", + "FUNCTIONS_WORKER_RUNTIME": "aas_functions_worker_runtime", + "FUNCTION_NAME": "gcp_deprecated_function_name", + "FUNCTION_TARGET": "gcp_function_target", + "GCP_PROJECT": "gcp_deprecated_project", + "K_SERVICE": "gcp_function_name", + "OTEL_LOGS_EXPORTER": "otel_logs_exporter", + "OTEL_LOG_LEVEL": "otel_log_level", + "OTEL_METRICS_EXPORTER": "otel_metrics_exporter", + "OTEL_PROPAGATORS": "otel_propagators", + "OTEL_RESOURCE_ATTRIBUTES": "otel_resource_attributes", + "OTEL_SDK_DISABLED": "otel_sdk_disabled", + "OTEL_SERVICE_NAME": "otel_service_name", + "OTEL_TRACES_EXPORTER": "otel_traces_exporter", + "OTEL_TRACES_SAMPLER": "otel_traces_sampler", + "OTEL_TRACES_SAMPLER_ARG": "otel_traces_sampler_arg", + "WEBSITE_INSTANCE_ID": "aas_website_instance_id", + "WEBSITE_OS": "aas_website_os", + "WEBSITE_OWNER_NAME": "aas_website_owner_name", + "WEBSITE_RESOURCE_GROUP": "aas_website_resource_group", + "WEBSITE_SITE_NAME": "aas_website_site_name", + "WEBSITE_SKU": "aas_website_sku", + "_DD_TRACE_STATS_COMPUTATION_INTERVAL": "trace_stats_computation_interval", + "_dd_appsec_deduplication_enabled": "appsec_deduplication_enabled", + "_dd_iast_debug": "iast_debug_enabled", + "_dd_iast_lazy_taint": "iast_lazy_taint", + "_dd_iast_propagation_debug": "iast_propagation_debug", + "_dd_inject_was_attempted": "trace_inject_was_attempted", + "_dd_llmobs_evaluator_sampling_rules": "llmobs_evaluator_sampling_rules", + "aas_app_type": "aas_app_type", + "aas_configuration_error": "aas_configuration_error", + "aas_functions_runtime_version": "aas_functions_runtime_version", + "aas_siteextensions_version": "aas_site_extensions_version", + "activity_listener_enabled": "activity_listener_enabled", + "agent_feature_drop_p0s": "agent_feature_drop_p0s", + "agent_transport": "agent_transport", + "agent_url": "trace_agent_url", + "analytics_enabled": "analytics_enabled", + "appsec.apiSecurity.enabled": "api_security_enabled", + "appsec.apiSecurity.requestSampling": "api_security_request_sample_rate", + "appsec.apiSecurity.sampleDelay": "api_security_sample_delay", + "appsec.blockedTemplateGraphql": "appsec_blocked_template_graphql", + "appsec.blockedTemplateHtml": "appsec_blocked_template_html", + "appsec.blockedTemplateJson": "appsec_blocked_template_json", + "appsec.customRulesProvided": "appsec_rules_custom_provided", + "appsec.enabled": "appsec_enabled", + "appsec.eventTracking": "appsec_auto_user_events_tracking", + "appsec.eventTracking.mode": "appsec_auto_user_events_tracking", + "appsec.obfuscatorKeyRegex": "appsec_obfuscation_parameter_key_regexp", + "appsec.obfuscatorValueRegex": "appsec_obfuscation_parameter_value_regexp", + "appsec.rasp.enabled": "appsec_rasp_enabled", + "appsec.rateLimit": "appsec_rate_limit", + "appsec.rules": "appsec_rules", + "appsec.rules.metadata.rules_version": "appsec_rules_metadata_rules_version", + "appsec.rules.version": "appsec_rules_version", + "appsec.sca.enabled": "appsec_sca_enabled", + "appsec.sca_enabled": "appsec_sca_enabled", + "appsec.stackTrace.enabled": "appsec_stack_trace_enabled", + "appsec.stackTrace.maxDepth": "appsec_max_stack_trace_depth", + "appsec.stackTrace.maxStackTraces": "appsec_max_stack_traces", + "appsec.standalone.enabled": "experimental_appsec_standalone_enabled", + "appsec.testing": "appsec_testing", + "appsec.trace.rate.limit": "appsec_trace_rate_limit", + "appsec.waf.timeout": "appsec_waf_timeout", + "appsec.wafTimeout": "appsec_waf_timeout", + "autofinish_spans": "trace_auto_finish_spans_enabled", + "autoload_no_compile": "autoload_no_compile", + "aws.dynamoDb.tablePrimaryKeys": "aws_dynamodb_table_primary_keys", + "baggageMaxBytes": "trace_baggage_max_bytes", + "baggageMaxItems": "trace_baggage_max_items", + "ciVisAgentlessLogSubmissionEnabled": "ci_visibility_agentless_enabled", + "ciVisibilityTestSessionName": "test_session_name", + "civisibility.agentless.enabled": "ci_visibility_agentless_enabled", + "civisibility.enabled": "ci_visibility_enabled", + "clientIpEnabled": "trace_client_ip_enabled", + "clientIpHeader": "trace_client_ip_header", + "clientIpHeaderDisabled": "client_ip_header_disabled", + "cloudPayloadTagging.maxDepth": "cloud_payload_tagging_max_depth", + "cloudPayloadTagging.requestsEnabled": "cloud_payload_tagging_requests_enabled", + "cloudPayloadTagging.responsesEnabled": "cloud_payload_tagging_responses_enabled", + "cloudPayloadTagging.rules.aws.eventbridge.expand": "cloud_payload_tagging_rules_aws_eventbridge_expand", + "cloudPayloadTagging.rules.aws.eventbridge.request": "cloud_payload_tagging_rules_aws_eventbridge_request", + "cloudPayloadTagging.rules.aws.eventbridge.response": "cloud_payload_tagging_rules_aws_eventbridge_response", + "cloudPayloadTagging.rules.aws.kinesis.expand": "cloud_payload_tagging_rules_aws_kinesis_expand", + "cloudPayloadTagging.rules.aws.kinesis.request": "cloud_payload_tagging_rules_aws_kinesis_request", + "cloudPayloadTagging.rules.aws.kinesis.response": "cloud_payload_tagging_rules_aws_kinesis_response", + "cloudPayloadTagging.rules.aws.s3.expand": "cloud_payload_tagging_rules_aws_s3_expand", + "cloudPayloadTagging.rules.aws.s3.request": "cloud_payload_tagging_rules_aws_s3_request", + "cloudPayloadTagging.rules.aws.s3.response": "cloud_payload_tagging_rules_aws_s3_response", + "cloudPayloadTagging.rules.aws.sns.expand": "cloud_payload_tagging_rules_aws_sns_expand", + "cloudPayloadTagging.rules.aws.sns.request": "cloud_payload_tagging_rules_aws_sns_request", + "cloudPayloadTagging.rules.aws.sns.response": "cloud_payload_tagging_rules_aws_sns_response", + "cloudPayloadTagging.rules.aws.sqs.expand": "cloud_payload_tagging_rules_aws_sqs_expand", + "cloudPayloadTagging.rules.aws.sqs.request": "cloud_payload_tagging_rules_aws_sqs_request", + "cloudPayloadTagging.rules.aws.sqs.response": "cloud_payload_tagging_rules_aws_sqs_response", + "cloud_hosting": "cloud_hosting_provider", + "codeOriginForSpans.enabled": "code_origin_for_spans_enabled", + "code_hotspots_enabled": "code_hotspots_enabled", + "commitSHA": "commit_sha", + "crashtracking.enabled": "crashtracking_enabled", + "crashtracking_alt_stack": "crashtracking_alt_stack", + "crashtracking_available": "crashtracking_available", + "crashtracking_debug_url": "crashtracking_debug_url", + "crashtracking_enabled": "crashtracking_enabled", + "crashtracking_stacktrace_resolver": "crashtracking_stacktrace_resolver", + "crashtracking_started": "crashtracking_started", + "crashtracking_stderr_filename": "crashtracking_stderr_filename", + "crashtracking_stdout_filename": "crashtracking_stdout_filename", + "cws.enabled": "cws_enabled", + "data.streams.enabled": "data_streams_enabled", + "data_streams_enabled": "data_streams_enabled", + "dbmPropagationMode": "dbm_propagation_mode", + "dbm_propagation_mode": "dbm_propagation_mode", + "dd.trace.debug": "trace_debug_enabled", + "dd_agent_host": "agent_host", + "dd_agent_port": "trace_agent_port", + "dd_analytics_enabled": "analytics_enabled", + "dd_api_security_parse_response_body": "appsec_parse_response_body", + "dd_appsec_automated_user_events_tracking_enabled": "appsec_auto_user_events_tracking_enabled", + "dd_civisibility_log_level": "ci_visibility_log_level", + "dd_crashtracking_create_alt_stack": "crashtracking_create_alt_stack", + "dd_crashtracking_debug_url": "crashtracking_debug_url", + "dd_crashtracking_enabled": "crashtracking_enabled", + "dd_crashtracking_stacktrace_resolver": "crashtracking_stacktrace_resolver", + "dd_crashtracking_stderr_filename": "crashtracking_stderr_filename", + "dd_crashtracking_stdout_filename": "crashtracking_stdout_filename", + "dd_crashtracking_tags": "crashtracking_tags", + "dd_crashtracking_use_alt_stack": "crashtracking_alt_stack", + "dd_crashtracking_wait_for_receiver": "crashtracking_wait_for_receiver", + "dd_dynamic_instrumentation_max_payload_size": "dynamic_instrumentation_max_payload_size", + "dd_dynamic_instrumentation_metrics_enabled": "dynamic_instrumentation_metrics_enabled", + "dd_dynamic_instrumentation_upload_timeout": "dynamic_instrumentation_upload_timeout", + "dd_http_client_tag_query_string": "trace_http_client_tag_query_string", + "dd_iast_redaction_value_numeral": "iast_redaction_value_numeral", + "dd_instrumentation_install_id": "instrumentation_install_id", + "dd_instrumentation_install_type": "instrumentation_install_type", + "dd_llmobs_agentless_enabled": "llmobs_agentless_enabled", + "dd_llmobs_enabled": "llmobs_enabled", + "dd_llmobs_ml_app": "llmobs_ml_app", + "dd_llmobs_sample_rate": "llmobs_sample_rate", + "dd_priority_sampling": "trace_priority_sampling_enabled", + "dd_profiling_agentless": "profiling_agentless", + "dd_profiling_api_timeout": "profiling_api_timeout", + "dd_profiling_capture_pct": "profiling_capture_pct", + "dd_profiling_enable_asserts": "profiling_enable_asserts", + "dd_profiling_enable_code_provenance": "profiling_enable_code_provenance", + "dd_profiling_export_libdd_enabled": "profiling_export_libdd_enabled", + "dd_profiling_export_py_enabled": "profiling_export_py_enabled", + "dd_profiling_force_legacy_exporter": "profiling_force_legacy_exporter", + "dd_profiling_heap_enabled": "profiling_heap_enabled", + "dd_profiling_heap_sample_size": "profiling_heap_sample_size", + "dd_profiling_ignore_profiler": "profiling_ignore_profiler", + "dd_profiling_lock_enabled": "profiling_lock_enabled", + "dd_profiling_lock_name_inspect_dir": "profiling_lock_name_inspect_dir", + "dd_profiling_max_events": "profiling_max_events", + "dd_profiling_max_frames": "profiling_max_frames", + "dd_profiling_max_time_usage_pct": "profiling_max_time_usage_pct", + "dd_profiling_memory_enabled": "profiling_memory_enabled", + "dd_profiling_memory_events_buffer": "profiling_memory_events_buffer", + "dd_profiling_output_pprof": "profiling_output_pprof", + "dd_profiling_sample_pool_capacity": "profiling_sample_pool_capacity", + "dd_profiling_stack_enabled": "profiling_stack_enabled", + "dd_profiling_stack_v2_enabled": "profiling_stack_v2_enabled", + "dd_profiling_tags": "profiling_tags", + "dd_profiling_timeline_enabled": "profiling_timeline_enabled", + "dd_profiling_upload_interval": "profiling_upload_interval", + "dd_remote_configuration_enabled": "remote_config_enabled", + "dd_remoteconfig_poll_seconds": "remote_config_poll_interval", + "dd_symbol_database_includes": "symbol_database_includes", + "dd_testing_raise": "testing_raise", + "dd_trace_agent_timeout_seconds": "trace_agent_timeout", + "dd_trace_api_version": "trace_api_version", + "dd_trace_propagation_http_baggage_enabled": "trace_propagation_http_baggage_enabled", + "dd_trace_report_hostname": "trace_report_hostname", + "dd_trace_sample_rate": "trace_sample_rate", + "dd_trace_span_links_enabled": "trace_span_links_enabled", + "dd_trace_span_traceback_max_size": "trace_span_traceback_max_size", + "dd_trace_writer_buffer_size_bytes": "trace_serialization_buffer_size", + "dd_trace_writer_interval_seconds": "trace_agent_flush_interval", + "dd_trace_writer_max_payload_size_bytes": "trace_agent_max_payload_size", + "dd_trace_writer_reuse_connections": "trace_agent_reuse_connections", + "ddtrace_auto_used": "ddtrace_auto_used", + "ddtrace_bootstrapped": "ddtrace_bootstrapped", + "debug": "trace_debug_enabled", + "debug_stack_enabled": "debug_stack_enabled", + "discovery": "agent_discovery_enabled", + "distributed_tracing": "trace_distributed_trace_enabled", + "dogstatsd.hostname": "dogstatsd_hostname", + "dogstatsd.port": "dogstatsd_port", + "dogstatsd.start-delay": "dogstatsd_start_delay", + "dogstatsd_addr": "dogstatsd_url", + "dogstatsd_url": "dogstatsd_url", + "dsmEnabled": "data_streams_enabled", + "dynamic.instrumentation.classfile.dump.enabled": "dynamic_instrumentation_classfile_dump_enabled", + "dynamic.instrumentation.enabled": "dynamic_instrumentation_enabled", + "dynamic.instrumentation.metrics.enabled": "dynamic_instrumentation_metrics_enabled", + "dynamicInstrumentationEnabled": "dynamic_instrumentation_enabled", + "dynamic_instrumentation.enabled": "dynamic_instrumentation_enabled", + "dynamic_instrumentation.redacted_identifiers": "dynamic_instrumentation_redacted_identifiers", + "dynamic_instrumentation.redacted_types": "dynamic_instrumentation_redacted_types", + "enabled": "trace_enabled", + "env": "env", + "environment_fulltrust_appdomain": "environment_fulltrust_appdomain_enabled", + "exception_replay_capture_interval_seconds": "dd_exception_replay_capture_interval_seconds", + "exception_replay_capture_max_frames": "dd_exception_replay_capture_max_frames", + "exception_replay_enabled": "dd_exception_replay_enabled", + "experimental.b3": "experimental_b3", + "experimental.enableGetRumData": "experimental_enable_get_rum_data", + "experimental.exporter": "experimental_exporter", + "experimental.runtimeId": "experimental_runtime_id", + "experimental.sampler.rateLimit": "experimental_sampler_rate_limit", + "experimental.sampler.sampleRate": "experimental_sampler_sample_rate", + "experimental.traceparent": "experimental_traceparent", + "flakyTestRetriesCount": "ci_visibility_flaky_retry_count", + "flushInterval": "flush_interval", + "flushMinSpans": "flush_min_spans", + "gitMetadataEnabled": "git_metadata_enabled", + "git_commit_sha": "commit_sha", + "git_repository_url": "repository_url", + "global_tag_version": "version", + "grpc.client.error.statuses": "trace_grpc_client_error_statuses", + "grpc.server.error.statuses": "trace_grpc_server_error_statuses", + "headerTags": "trace_header_tags", + "hostname": "agent_hostname", + "http.client.tag.query-string": "trace_http_client_tag_query_string", + "http.server.route-based-naming": "trace_http_server_route_based_naming_enabled", + "http.server.tag.query-string": "trace_http_server_tag_query_string", + "http_server_route_based_naming": "http_server_route_based_naming", + "hystrix.measured.enabled": "hystrix_measured_enabled", + "hystrix.tags.enabled": "hystrix_tags_enabled", + "iast.cookieFilterPattern": "iast_cookie_filter_pattern", + "iast.dbRowsToTaint": "iast_db_rows_to_taint", + "iast.debug.enabled": "iast_debug_enabled", + "iast.deduplication.enabled": "iast_deduplication_enabled", + "iast.deduplicationEnabled": "iast_deduplication_enabled", + "iast.enabled": "iast_enabled", + "iast.max-concurrent-requests": "iast_max_concurrent_requests", + "iast.maxConcurrentRequests": "iast_max_concurrent_requests", + "iast.maxContextOperations": "iast_max_context_operations", + "iast.redactionEnabled": "iast_redaction_enabled", + "iast.redactionNamePattern": "iast_redaction_name_pattern", + "iast.redactionValuePattern": "iast_redaction_value_pattern", + "iast.request-sampling": "iast_request_sampling", + "iast.requestSampling": "iast_request_sampling", + "iast.telemetryVerbosity": "iast_telemetry_verbosity", + "iast.vulnerabilities-per-request": "iast_vulnerability_per_request", + "ignite.cache.include_keys": "ignite_cache_include_keys_enabled", + "inferredProxyServicesEnabled": "inferred_proxy_services_enabled", + "inject_force": "ssi_forced_injection_enabled", + "injectionEnabled": "ssi_injection_enabled", + "instrumentation.telemetry.enabled": "instrumentation_telemetry_enabled", + "instrumentation_config_id": "instrumentation_config_id", + "integration_metrics_enabled": "integration_metrics_enabled", + "integrations.enabled": "trace_integrations_enabled", + "integrations_disabled": "trace_disabled_integrations", + "isAzureFunction": "azure_function", + "isCiVisibility": "ci_visibility_enabled", + "isEarlyFlakeDetectionEnabled": "ci_visibility_early_flake_detection_enabled", + "isFlakyTestRetriesEnabled": "ci_visibility_flaky_retry_enabled", + "isGCPFunction": "is_gcp_function", + "isGitUploadEnabled": "git_upload_enabled", + "isIntelligentTestRunnerEnabled": "intelligent_test_runner_enabled", + "isManualApiEnabled": "ci_visibility_manual_api_enabled", + "isTestDynamicInstrumentationEnabled": "ci_visibility_test_dynamic_instrumentation_enabled", + "jmxfetch.check-period": "jmxfetch_check_period", + "jmxfetch.enabled": "jmxfetch_enabled", + "jmxfetch.initial-refresh-beans-period": "jmxfetch_initial_refresh_beans_period", + "jmxfetch.multiple-runtime-services.enabled": "jmxfetch_multiple_runtime_services_enabled", + "jmxfetch.refresh-beans-period": "jmxfetch_initial_refresh_beans_period", + "jmxfetch.statsd.port": "jmxfetch_statsd_port", + "kafka.client.base64.decoding.enabled": "trace_kafka_client_base64_decoding_enabled", + "lambda_mode": "lambda_mode", + "langchain.spanCharLimit": "open_ai_span_char_limit", + "langchain.spanPromptCompletionSampleRate": "open_ai_span_prompt_completion_sample_rate", + "legacy.installer.enabled": "legacy_installer_enabled", + "legacyBaggageEnabled": "trace_legacy_baggage_enabled", + "llmobs.agentlessEnabled": "open_ai_agentless_enabled", + "llmobs.enabled": "open_ai_enabled", + "llmobs.mlApp": "open_ai_ml_app", + "logInjection": "logs_injection_enabled", + "logInjection_enabled": "logs_injection_enabled", + "logLevel": "trace_log_level", + "log_backtrace": "trace_log_backtrace_enabled", + "logger": "logger", + "logs.injection": "logs_injection_enabled", + "logs.mdc.tags.injection": "logs_mdc_tags_injection_enabled", + "lookup": "lookup", + "managed_tracer_framework": "managed_tracer_framework", + "memcachedCommandEnabled": "memchached_command_enabled", + "message.broker.split-by-destination": "message_broker_split_by_destination", + "native_tracer_version": "native_tracer_version", + "openAiLogsEnabled": "open_ai_logs_enabled", + "openaiSpanCharLimit": "open_ai_span_char_limit", + "openai_log_prompt_completion_sample_rate": "open_ai_log_prompt_completion_sample_rate", + "openai_logs_enabled": "open_ai_logs_enabled", + "openai_metrics_enabled": "open_ai_metrics_enabled", + "openai_service": "open_ai_service", + "openai_span_char_limit": "open_ai_span_char_limit", + "openai_span_prompt_completion_sample_rate": "open_ai_span_prompt_completion_sample_rate", + "orchestrion_enabled": "orchestrion_enabled", + "orchestrion_version": "orchestrion_version", + "os.name": "os_name", + "otel_enabled": "trace_otel_enabled", + "partialflush_enabled": "trace_partial_flush_enabled", + "partialflush_minspans": "trace_partial_flush_min_spans", + "peerServiceMapping": "trace_peer_service_mapping", + "platform": "platform", + "plugins": "plugins", + "port": "trace_agent_port", + "priority.sampling": "trace_priority_sample_enabled", + "priority_sampling": "trace_priority_sampling_enabled", + "profiler_loaded": "profiler_loaded", + "profiling.advanced.code_provenance_enabled": "profiling_enable_code_provenance", + "profiling.advanced.endpoint.collection.enabled": "profiling_endpoint_collection_enabled", + "profiling.allocation.enabled": "profiling_allocation_enabled", + "profiling.async.alloc.enabled": "profiling_async_alloc_enabled", + "profiling.async.cpu.enabled": "profiling_async_cpu_enabled", + "profiling.async.enabled": "profiling_async_enabled", + "profiling.async.memleak.enabled": "profiling_async_memleak_enabled", + "profiling.async.wall.enabled": "profiling_async_wall_enabled", + "profiling.ddprof.alloc.enabled": "profiling_ddprof_alloc_enabled", + "profiling.ddprof.cpu.enabled": "profiling_ddprof_cpu_enabled", + "profiling.ddprof.enabled": "profiling_ddprof_enabled", + "profiling.ddprof.memleak.enabled": "profiling_ddprof_memleak_enabled", + "profiling.ddprof.wall.enabled": "profiling_ddprof_wall_enabled", + "profiling.directallocation.enabled": "profiling_direct_allocation_enabled", + "profiling.enabled": "profiling_enabled", + "profiling.exporters": "profiling_exporters", + "profiling.heap.enabled": "profiling_heap_enabled", + "profiling.hotspots.enabled": "profiling_hotspots_enabled", + "profiling.legacy.tracing.integration": "profiling_legacy_tracing_integration_enabled", + "profiling.longLivedThreshold": "profiling_long_lived_threshold", + "profiling.sourceMap": "profiling_source_map_enabled", + "profiling.start-delay": "profiling_start_delay", + "profiling.start-force-first": "profiling_start_force_first", + "profiling.upload.period": "profiling_upload_period", + "profiling_endpoints_enabled": "profiling_endpoints_enabled", + "protocolVersion": "trace_agent_protocol_version", + "queryStringObfuscation": "trace_obfuscation_query_string_regexp", + "rcPollingInterval": "rc_polling_interval", + "remoteConfig.enabled": "remote_config_enabled", + "remoteConfig.pollInterval": "remote_config_poll_interval", + "remote_config.enabled": "remote_config_enabled", + "remote_config_poll_interval_seconds": "remote_config_poll_interval", + "reportHostname": "trace_report_hostname", + "repositoryUrl": "repository_url", + "resolver.outline.pool.enabled": "resolver_outline_pool_enabled", + "resolver.use.loadclass": "resolver_use_loadclass", + "retry_interval": "retry_interval", + "routetemplate_expansion_enabled": "trace_route_template_expansion_enabled", + "routetemplate_resourcenames_enabled": "trace_route_template_resource_names_enabled", + "runtime.metrics.enabled": "runtime_metrics_enabled", + "runtimeMetrics": "runtime_metrics_enabled", + "runtime_metrics.enabled": "runtime_metrics_enabled", + "runtime_metrics_v2_enabled": "runtime_metrics_v2_enabled", + "runtimemetrics_enabled": "runtime_metrics_enabled", + "sampleRate": "trace_sample_rate", + "sample_rate": "trace_sample_rate", + "sampler.rateLimit": "trace_rate_limit", + "sampler.rules": "trace_sample_rules", + "sampler.sampleRate": "trace_sample_rate", + "sampler.spanSamplingRules": "span_sample_rules", + "sampling_rules": "trace_sample_rules", + "scope": "scope", + "security_enabled": "appsec_enabled", + "send_retries": "trace_send_retries", + "service": "service", + "serviceMapping": "dd_service_mapping", + "site": "site", + "spanAttributeSchema": "trace_span_attribute_schema", + "spanComputePeerService": "trace_peer_service_defaults_enabled", + "spanLeakDebug": "span_leak_debug", + "spanRemoveIntegrationFromService": "trace_remove_integration_service_names_enabled", + "span_sampling_rules": "span_sample_rules", + "span_sampling_rules_file": "span_sample_rules_file", + "ssi_forced_injection_enabled": "ssi_forced_injection_enabled", + "ssi_injection_enabled": "ssi_injection_enabled", + "startupLogs": "trace_startup_logs_enabled", + "stats.enabled": "stats_enabled", + "stats_computation_enabled": "trace_stats_computation_enabled", + "tagsHeaderMaxLength": "trace_header_tags_max_length", + "telemetry.debug": "instrumentation_telemetry_debug_enabled", + "telemetry.dependencyCollection": "instrumentation_telemetry_dependency_collection_enabled", + "telemetry.enabled": "instrumentation_telemetry_enabled", + "telemetry.heartbeat.interval": "instrumentation_telemetry_heartbeat_interval", + "telemetry.heartbeatInterval": "instrumentation_telemetry_heartbeat_interval", + "telemetry.logCollection": "instrumentation_telemetry_log_collection_enabled", + "telemetry.metrics": "instrumentation_telemetry_metrics_enabled", + "telemetry.metricsInterval": "instrumentation_telemetry_metrics_interval", + "telemetryEnabled": "instrumentation_telemetry_enabled", + "telemetry_heartbeat_interval": "instrumentation_telemetry_heartbeat_interval", + "trace.128_bit_traceid_generation_enabled": "trace_128_bits_id_enabled", + "trace.128_bit_traceid_logging_enabled": "trace_128_bits_id_logging_enabled", + "trace.agent.port": "trace_agent_port", + "trace.agent.timeout": "trace_agent_timeout", + "trace.agent.v0.5.enabled": "trace_agent_v0.5_enabled", + "trace.agent_attempt_retry_time_msec": "trace_agent_attempt_retry_time_msec", + "trace.agent_connect_timeout": "trace_agent_connect_timeout", + "trace.agent_debug_verbose_curl": "trace_agent_debug_verbose_curl_enabled", + "trace.agent_flush_after_n_requests": "trace_agent_flush_after_n_requests", + "trace.agent_flush_interval": "trace_agent_flush_interval", + "trace.agent_max_consecutive_failures": "trace_send_retries", + "trace.agent_max_payload_size": "trace_agent_max_payload_size", + "trace.agent_port": "trace_agent_port", + "trace.agent_retries": "trace_send_retries", + "trace.agent_stack_backlog": "trace_agent_stack_backlog", + "trace.agent_stack_initial_size": "trace_agent_stack_initial_size", + "trace.agent_test_session_token": "trace_agent_test_session_token", + "trace.agent_timeout": "trace_agent_timeout", + "trace.agent_url": "trace_agent_url", + "trace.agentless": "trace_agentless", + "trace.analytics.enabled": "trace_analytics_enabled", + "trace.analytics_enabled": "trace_analytics_enabled", + "trace.append_trace_ids_to_logs": "trace_append_trace_ids_to_logs", + "trace.auto_flush_enabled": "trace_auto_flush_enabled", + "trace.aws-sdk.legacy.tracing.enabled": "trace_aws_sdk_legacy_tracing_enabled", + "trace.aws-sdk.propagation.enabled": "trace_aws_sdk_propagation_enabled", + "trace.beta_high_memory_pressure_percent": "trace_beta_high_memory_pressure_percent", + "trace.bgs_connect_timeout": "trace_bgs_connect_timeout", + "trace.bgs_timeout": "trace_bgs_timeout", + "trace.buffer_size": "trace_serialization_buffer_size", + "trace.cli_enabled": "trace_cli_enabled", + "trace.client-ip.enabled": "trace_client_ip_enabled", + "trace.client-ip.resolver.enabled": "trace_client_ip_resolver_enabled", + "trace.client_ip_enabled": "trace_client_ip_enabled", + "trace.client_ip_header": "client_ip_header", + "trace.db.client.split-by-instance": "trace_db_client_split_by_instance", + "trace.db.client.split-by-instance.type.suffix": "trace_db_client_split_by_instance_type_suffix", + "trace.db_client_split_by_instance": "trace_db_client_split_by_instance", + "trace.debug": "trace_debug_enabled", + "trace.debug_curl_output": "trace_debug_curl_output_enabled", + "trace.debug_prng_seed": "trace_debug_prng_seed", + "trace.enabled": "trace_enabled", + "trace.flush_collect_cycles": "trace_flush_collect_cycles_enabled", + "trace.forked_process": "trace_forked_process_enabled", + "trace.generate_root_span": "trace_generate_root_span_enabled", + "trace.git_metadata_enabled": "git_metadata_enabled", + "trace.grpc.server.trim-package-resource": "trace_grpc_server_trim_package_resource_enabled", + "trace.header.tags.legacy.parsing.enabled": "trace_header_tags_legacy_parsing_enabled", + "trace.health.metrics.enabled": "trace_health_metrics_enabled", + "trace.health.metrics.statsd.port": "trace_health_metrics_statsd_port", + "trace.health_metrics_enabled": "trace_health_metrics_enabled", + "trace.health_metrics_heartbeat_sample_rate": "trace_health_metrics_heartbeat_sample_rate", + "trace.hook_limit": "trace_hook_limit", + "trace.http.client.split-by-domain": "trace_http_client_split_by_domain", + "trace.http_client_split_by_domain": "trace_http_client_split_by_domain", + "trace.http_post_data_param_allowed": "trace_http_post_data_param_allowed", + "trace.http_url_query_param_allowed": "trace_http_url_query_param_allowed", + "trace.jms.propagation.enabled": "trace_jms_propagation_enabled", + "trace.jmxfetch.kafka.enabled": "trace_jmxfetch_kafka_enabled", + "trace.jmxfetch.tomcat.enabled": "trace_jmxfetch_tomcat_enabled", + "trace.kafka.client.propagation.enabled": "trace_kafka_client_propagation_enabled", + "trace.laravel_queue_distributed_tracing": "trace_laravel_queue_distributed_tracing", + "trace.log_file": "trace_log_file", + "trace.log_level": "trace_log_level", + "trace.measure_compile_time": "trace_measure_compile_time_enabled", + "trace.measure_peak_memory_usage": "trace_measure_peak_memory_usage_enabled", + "trace.memcached_obfuscation": "trace_memcached_obfuscation_enabled", + "trace.memory_limit": "trace_memory_limit", + "trace.obfuscation_query_string_regexp": "trace_obfuscation_query_string_regexp", + "trace.once_logs": "trace_once_logs", + "trace.otel.enabled": "trace_otel_enabled", + "trace.otel_enabled": "trace_otel_enabled", + "trace.partial.flush.min.spans": "trace_partial_flush_min_spans", + "trace.peer.service.defaults.enabled": "trace_peer_service_defaults_enabled", + "trace.peer.service.mapping": "trace_peer_service_mapping", + "trace.peer_service_defaults_enabled": "trace_peer_service_defaults_enabled", + "trace.peer_service_mapping": "trace_peer_service_mapping", + "trace.peerservicetaginterceptor.enabled": "trace_peer_service_tag_interceptor_enabled", + "trace.perf.metrics.enabled": "trace_perf_metrics_enabled", + "trace.play.report-http-status": "trace_play_report_http_status", + "trace.propagate_service": "trace_propagate_service", + "trace.propagate_user_id_default": "trace_propagate_user_id_default_enabled", + "trace.propagation_extract_first": "trace_propagation_extract_first", + "trace.propagation_style": "trace_propagation_style", + "trace.propagation_style_extract": "trace_propagation_style_extract", + "trace.propagation_style_inject": "trace_propagation_style_inject", + "trace.rabbit.propagation.enabled": "trace_rabbit_propagation_enabled", + "trace.rate.limit": "trace_rate_limit", + "trace.rate_limit": "trace_rate_limit", + "trace.redis_client_split_by_host": "trace_redis_client_split_by_host_enabled", + "trace.remove.integration-service-names.enabled": "trace_remove_integration_service_names_enabled", + "trace.remove_autoinstrumentation_orphans": "trace_remove_auto_instrumentation_orphans_enabled", + "trace.remove_integration_service_names_enabled": "trace_remove_integration_service_names_enabled", + "trace.remove_root_span_laravel_queue": "trace_remove_root_span_laravel_queue_enabled", + "trace.remove_root_span_symfony_messenger": "trace_remove_root_span_symfony_messenger_enabled", + "trace.report-hostname": "trace_report_hostname", + "trace.report_hostname": "trace_report_hostname", + "trace.request_init_hook": "trace_request_init_hook", + "trace.resource_uri_fragment_regex": "trace_resource_uri_fragment_regex", + "trace.resource_uri_mapping_incoming": "trace_resource_uri_mapping_incoming", + "trace.resource_uri_mapping_outgoing": "trace_resource_uri_mapping_outgoing", + "trace.resource_uri_query_param_allowed": "trace_resource_uri_query_param_allowed", + "trace.retain_thread_capabilities": "trace_retain_thread_capabilities_enabled", + "trace.sample.rate": "trace_sample_rate", + "trace.sample_rate": "trace_sample_rate", + "trace.sampling_rules": "trace_sample_rules", + "trace.sampling_rules_format": "trace_sampling_rules_format", + "trace.scope.depth.limit": "trace_scope_depth_limit", + "trace.servlet.async-timeout.error": "trace_servlet_async_timeout_error_enabled", + "trace.servlet.principal.enabled": "trace_servlet_principal_enabled", + "trace.shutdown_timeout": "trace_shutdown_timeout", + "trace.sidecar_trace_sender": "trace_sidecar_trace_sender", + "trace.sources_path": "trace_sources_path", + "trace.span.attribute.schema": "trace_span_attribute_schema", + "trace.spans_limit": "trace_spans_limit", + "trace.sqs.propagation.enabled": "trace_sqs_propagation_enabled", + "trace.startup_logs": "trace_startup_logs", + "trace.status404decorator.enabled": "trace_status_404_decorator_enabled", + "trace.status404rule.enabled": "trace_status_404_rule_enabled", + "trace.symfony_messenger_distributed_tracing": "trace_symfony_messenger_distributed_tracing", + "trace.symfony_messenger_middlewares": "trace_symfony_messenger_middlewares", + "trace.telemetry_enabled": "instrumentation_telemetry_enabled", + "trace.traced_internal_functions": "trace_traced_internal_functions", + "trace.tracer.metrics.enabled": "trace_metrics_enabled", + "trace.url_as_resource_names_enabled": "trace_url_as_resource_names_enabled", + "trace.warn_legacy_dd_trace": "trace_warn_legacy_dd_trace_enabled", + "trace.wordpress_additional_actions": "trace_wordpress_additional_actions", + "trace.wordpress_callbacks": "trace_wordpress_callbacks", + "trace.wordpress_enhanced_integration": "trace_wordpress_enhanced_integration", + "trace.x-datadog-tags.max.length": "trace_x_datadog_tags_max_length", + "trace.x_datadog_tags_max_length": "trace_x_datadog_tags_max_length", + "traceEnabled": "trace_enabled", + "traceId128BitGenerationEnabled": "trace_128_bits_id_enabled", + "traceId128BitLoggingEnabled": "trace_128_bits_id_logging_enabled", + "tracePropagationExtractFirst": "trace_propagation_extract_first", + "tracePropagationStyle,otelPropagators": "trace_propagation_style_otel_propagators", + "tracePropagationStyle.extract": "trace_propagation_style_extract", + "tracePropagationStyle.inject": "trace_propagation_style_inject", + "tracePropagationStyle.otelPropagators": "trace_propagation_style_otel_propagators", + "trace_methods": "trace_methods", + "tracer_instance_count": "trace_instance_count", + "tracing": "trace_enabled", + "tracing.auto_instrument.enabled": "trace_auto_instrument_enabled", + "tracing.distributed_tracing.propagation_extract_style": "trace_propagation_style_extract", + "tracing.distributed_tracing.propagation_inject_style": "trace_propagation_style_inject", + "tracing.enabled": "trace_enabled", + "tracing.log_injection": "logs_injection_enabled", + "tracing.opentelemetry.enabled": "trace_otel_enabled", + "tracing.partial_flush.enabled": "trace_partial_flush_enabled", + "tracing.partial_flush.min_spans_threshold": "trace_partial_flush_min_spans", + "tracing.propagation_style_extract": "trace_propagation_style_extract", + "tracing.propagation_style_inject": "trace_propagation_style_inject", + "tracing.report_hostname": "trace_report_hostname", + "tracing.sampling.rate_limit": "trace_sample_rate", + "tracing_enabled": "trace_enabled", + "universal_version": "universal_version_enabled", + "url": "trace_agent_url", + "version": "application_version", + "wcf_obfuscation_enabled": "trace_wcf_obfuscation_enabled" +} diff --git a/packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json b/packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json new file mode 100644 index 00000000000..fc5188f2c2b --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json @@ -0,0 +1,243 @@ +[ + "apiKey", + "appsec.eventTracking.enabled", + "trace.integration.", + "global_tag_runtime-id", + "tracePropagationStyle.inject.", + "DD_PROFILING_API_KEY", + "dd_profiling_apikey", + "N/A", + "DD_API_KEY", + "DD_APPLICATION_KEY", + "DD_TRACE_HttpMessageHandler_", + "DD_HttpMessageHandler_", + "DD_TRACE_HttpSocketsHandler_", + "DD_HttpSocketsHandler_", + "DD_TRACE_WinHttpHandler_", + "DD_WinHttpHandler_", + "DD_TRACE_CurlHandler_", + "DD_CurlHandler_", + "DD_TRACE_AspNetCore_", + "DD_AspNetCore_", + "DD_TRACE_AdoNet_", + "DD_AdoNet_", + "DD_TRACE_AspNet_", + "DD_AspNet_", + "DD_TRACE_AspNetMvc_", + "DD_AspNetMvc_", + "DD_TRACE_AspNetWebApi2_", + "DD_AspNetWebApi2_", + "DD_TRACE_GraphQL_", + "DD_GraphQL_", + "DD_TRACE_HotChocolate_", + "DD_HotChocolate_", + "DD_TRACE_MongoDb_", + "DD_MongoDb_", + "DD_TRACE_XUnit_", + "DD_XUnit_", + "DD_TRACE_NUnit_", + "DD_NUnit_", + "DD_TRACE_MsTestV2_", + "DD_MsTestV2_", + "DD_TRACE_Wcf_", + "DD_Wcf_", + "DD_TRACE_WebRequest_", + "DD_WebRequest_", + "DD_TRACE_ElasticsearchNet_", + "DD_ElasticsearchNet_", + "DD_TRACE_ServiceStackRedis_", + "DD_ServiceStackRedis_", + "DD_TRACE_StackExchangeRedis_", + "DD_StackExchangeRedis_", + "DD_TRACE_ServiceRemoting_", + "DD_ServiceRemoting_", + "DD_TRACE_RabbitMQ_", + "DD_RabbitMQ_", + "DD_TRACE_Msmq_", + "DD_Msmq_", + "DD_TRACE_Kafka_", + "DD_Kafka_", + "DD_TRACE_CosmosDb_", + "DD_CosmosDb_", + "DD_TRACE_AwsLambda_", + "DD_AwsLambda_", + "DD_TRACE_AwsSdk_", + "DD_AwsSdk_", + "DD_TRACE_AwsSqs_", + "DD_AwsSqs_", + "DD_TRACE_AwsSns_", + "DD_AwsSns_", + "DD_TRACE_ILogger_", + "DD_ILogger_", + "DD_TRACE_Aerospike_", + "DD_Aerospike_", + "DD_TRACE_AzureFunctions_", + "DD_AzureFunctions_", + "DD_TRACE_Couchbase_", + "DD_Couchbase_", + "DD_TRACE_MySql_", + "DD_MySql_", + "DD_TRACE_Npgsql_", + "DD_Npgsql_", + "DD_TRACE_Oracle_", + "DD_Oracle_", + "DD_TRACE_SqlClient_", + "DD_SqlClient_", + "DD_TRACE_Sqlite_", + "DD_Sqlite_", + "DD_TRACE_Serilog_", + "DD_Serilog_", + "DD_TRACE_Log4Net_", + "DD_Log4Net_", + "DD_TRACE_NLog_", + "DD_NLog_", + "DD_TRACE_TraceAnnotations_", + "DD_TraceAnnotations_", + "DD_TRACE_Grpc_", + "DD_Grpc_", + "DD_TRACE_Process_", + "DD_Process_", + "DD_TRACE_HashAlgorithm_", + "DD_HashAlgorithm_", + "DD_TRACE_SymmetricAlgorithm_", + "DD_SymmetricAlgorithm_", + "DD_TRACE_OpenTelemetry_", + "DD_OpenTelemetry_", + "DD_TRACE_PathTraversal_", + "DD_PathTraversal_", + "DD_TRACE_Ssrf_", + "DD_Ssrf_", + "DD_TRACE_Ldap_", + "DD_Ldap_", + "DD_TRACE_AwsKinesis_", + "DD_AwsKinesis_", + "DD_TRACE_AzureServiceBus_", + "DD_AzureServiceBus_", + "DD_TRACE_SystemRandom_", + "DD_SystemRandom_", + "DD_TRACE_AwsDynamoDb_", + "DD_AwsDynamoDb_", + "DD_TRACE_HardcodedSecret_", + "DD_HarcodedSecret_", + "DD_TRACE_IbmMq_", + "DD_IbmMq_", + "DD_TRACE_Remoting_", + "DD_Remoting_", + "trace.amqp_enabled", + "trace.amqp_analytics_enabled", + "trace.amqp_analytics_sample_rate", + "trace.cakephp_enabled", + "trace.cakephp_analytics_enabled", + "trace.cakephp_analytics_sample_rate", + "trace.codeigniter_enabled", + "trace.codeigniter_analytics_enabled", + "trace.codeigniter_analytics_sample_rate", + "trace.curl_enabled", + "trace.curl_analytics_enabled", + "trace.curl_analytics_sample_rate", + "trace.elasticsearch_enabled", + "trace.elasticsearch_analytics_enabled", + "trace.elasticsearch_analytics_sample_rate", + "trace.eloquent_enabled", + "trace.eloquent_analytics_enabled", + "trace.eloquent_analytics_sample_rate", + "trace.frankenphp_enabled", + "trace.frankenphp_analytics_enabled", + "trace.frankenphp_analytics_sample_rate", + "trace.googlespanner_enabled", + "trace.googlespanner_analytics_enabled", + "trace.googlespanner_analytics_sample_rate", + "trace.guzzle_enabled", + "trace.guzzle_analytics_enabled", + "trace.guzzle_analytics_sample_rate", + "trace.laminas_enabled", + "trace.laminas_analytics_enabled", + "trace.laminas_analytics_sample_rate", + "trace.laravel_enabled", + "trace.laravel_analytics_enabled", + "trace.laravel_analytics_sample_rate", + "trace.laravelqueue_enabled", + "trace.laravelqueue_analytics_enabled", + "trace.laravelqueue_analytics_sample_rate", + "trace.logs_enabled", + "trace.logs_analytics_enabled", + "trace.logs_analytics_sample_rate", + "trace.lumen_enabled", + "trace.lumen_analytics_enabled", + "trace.lumen_analytics_sample_rate", + "trace.memcache_enabled", + "trace.memcache_analytics_enabled", + "trace.memcache_analytics_sample_rate", + "trace.memcached_enabled", + "trace.memcached_analytics_enabled", + "trace.memcached_analytics_sample_rate", + "trace.mongo_enabled", + "trace.mongo_analytics_enabled", + "trace.mongo_analytics_sample_rate", + "trace.mongodb_enabled", + "trace.mongodb_analytics_enabled", + "trace.mongodb_analytics_sample_rate", + "trace.mysqli_enabled", + "trace.mysqli_analytics_enabled", + "trace.mysqli_analytics_sample_rate", + "trace.nette_enabled", + "trace.nette_analytics_enabled", + "trace.nette_analytics_sample_rate", + "trace.openai_enabled", + "trace.openai_analytics_enabled", + "trace.openai_analytics_sample_rate", + "trace.pcntl_enabled", + "trace.pcntl_analytics_enabled", + "trace.pcntl_analytics_sample_rate", + "trace.pdo_enabled", + "trace.pdo_analytics_enabled", + "trace.pdo_analytics_sample_rate", + "trace.phpredis_enabled", + "trace.phpredis_analytics_enabled", + "trace.phpredis_analytics_sample_rate", + "trace.predis_enabled", + "trace.predis_analytics_enabled", + "trace.predis_analytics_sample_rate", + "trace.psr18_enabled", + "trace.psr18_analytics_enabled", + "trace.psr18_analytics_sample_rate", + "trace.roadrunner_enabled", + "trace.roadrunner_analytics_enabled", + "trace.roadrunner_analytics_sample_rate", + "trace.sqlsrv_enabled", + "trace.sqlsrv_analytics_enabled", + "trace.sqlsrv_analytics_sample_rate", + "trace.slim_enabled", + "trace.slim_analytics_enabled", + "trace.slim_analytics_sample_rate", + "trace.swoole_enabled", + "trace.swoole_analytics_enabled", + "trace.swoole_analytics_sample_rate", + "trace.symfonymessenger_enabled", + "trace.symfonymessenger_analytics_enabled", + "trace.symfonymessenger_analytics_sample_rate", + "trace.symfony_enabled", + "trace.symfony_analytics_enabled", + "trace.symfony_analytics_sample_rate", + "trace.web_enabled", + "trace.web_analytics_enabled", + "trace.web_analytics_sample_rate", + "trace.wordpress_enabled", + "trace.wordpress_analytics_enabled", + "trace.wordpress_analytics_sample_rate", + "trace.yii_enabled", + "trace.yii_analytics_enabled", + "trace.yii_analytics_sample_rate", + "trace.zendframework_enabled", + "trace.zendframework_analytics_enabled", + "trace.zendframework_analytics_sample_rate", + "trace.drupal_enabled", + "trace.drupal_analytics_enabled", + "trace.drupal_analytics_sample_rate", + "trace.magento_enabled", + "trace.magento_analytics_enabled", + "trace.magento_analytics_sample_rate", + "trace.exec_enabled", + "trace.exec_analytics_enabled", + "trace.exec_analytics_sample_rate" +] diff --git a/packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json b/packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json new file mode 100644 index 00000000000..b96a6ab5d15 --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json @@ -0,0 +1,175 @@ +{ + "normalization_rules" : + { + "HOSTNAME" : "agent_hostname", + "hostname" : "agent_hostname", + "appsec.blockedTemplateHtml" : "appsec_blocked_template_html", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML" : "appsec_blocked_template_html", + "appsec.blockedTemplateJson" : "appsec_blocked_template_json", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON" : "appsec_blocked_template_json", + "security_enabled" : "appsec_enabled", + "appsec.enabled" : "appsec_enabled", + "DD_APPSEC_ENABLED" : "appsec_enabled", + "appsec.obfuscatorKeyRegex" : "appsec_obfuscation_parameter_key_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP" : "appsec_obfuscation_parameter_key_regexp", + "appsec.obfuscatorValueRegex" : "appsec_obfuscation_parameter_value_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP" : "appsec_obfuscation_parameter_value_regexp", + "appsec.rateLimit" : "appsec_rate_limit", + "appsec.rules" : "appsec_rules", + "DD_APPSEC_RULES" : "appsec_rules", + "appsec.customRulesProvided" : "appsec_rules_custom_provided", + "appsec.rules.metadata.rules_version" : "appsec_rules_metadata_rules_version", + "appsec.rules.version" : "appsec_rules_version", + "appsec.wafTimeout" : "appsec_waf_timeout", + "appsec.waf.timeout" : "appsec_waf_timeout", + "DD_APPSEC_WAF_TIMEOUT" : "appsec_waf_timeout", + "civisibility.enabled" : "ci_visibility_enabled", + "isCiVisibility" : "ci_visibility_enabled", + "DD_CIVISIBILITY_ENABLED" : "ci_visibility_enabled", + "clientIpHeaderDisabled" : "client_ip_header_disabled", + "dbmPropagationMode" : "dbm_propagation_mode", + "dbm_propagation_mode" : "dbm_propagation_mode", + "DD_DBM_PROPAGATION_MODE" : "dbm_propagation_mode", + "dogstatsd.hostname" : "dogstatsd_hostname", + "dogstatsd.port" : "dogstatsd_port", + "DD_DOGSTATSD_PORT" : "dogstatsd_port", + "env" : "env", + "DD_ENV" : "env", + "experimental.b3" : "experimental_b3", + "experimental.enableGetRumData" : "experimental_enable_get_rum_data", + "experimental.exporter" : "experimental_exporter", + "experimental.runtimeId" : "experimental_runtime_id", + "experimental.sampler.rateLimit" : "experimental_sampler_rate_limit", + "experimental.sampler.sampleRate" : "experimental_sampler_sample_rate", + "experimental.traceparent" : "experimental_traceparent", + "flushInterval" : "flush_interval", + "flushMinSpans" : "flush_min_spans", + "isGitUploadEnabled" : "git_upload_enabled", + "iast.deduplication.enabled" : "iast_deduplication_enabled", + "iast.deduplicationEnabled" : "iast_deduplication_enabled", + "DD_IAST_DEDUPLICATION_ENABLED" : "iast_deduplication_enabled", + "iast.enabled" : "iast_enabled", + "DD_IAST_ENABLED" : "iast_enabled", + "iast.maxConcurrentRequests" : "iast_max_concurrent_requests", + "iast.max-concurrent-requests" : "iast_max_concurrent_requests", + "DD_IAST_MAX_CONCURRENT_REQUESTS" : "iast_max_concurrent_requests", + "iast.maxContextOperations" : "iast_max_context_operations", + "iast.requestSampling" : "iast_request_sampling", + "iast.request-sampling" : "iast_request_sampling", + "telemetry.debug" : "instrumentation_telemetry_debug_enabled", + "DD_INTERNAL_TELEMETRY_DEBUG_ENABLED" : "instrumentation_telemetry_debug_enabled", + "instrumentation.telemetry.enabled" : "instrumentation_telemetry_enabled", + "telemetryEnabled" : "instrumentation_telemetry_enabled", + "telemetry.enabled" : "instrumentation_telemetry_enabled", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED" : "instrumentation_telemetry_enabled", + "trace.telemetry_enabled" : "instrumentation_telemetry_enabled", + "telemetry.logCollection" : "instrumentation_telemetry_log_collection_enabled", + "telemetry.metrics" : "instrumentation_telemetry_metrics_enabled", + "DD_TELEMETRY_METRICS_ENABLED" : "instrumentation_telemetry_metrics_enabled", + "isIntelligentTestRunnerEnabled" : "intelligent_test_runner_enabled", + "logger" : "logger", + "logInjection_enabled" : "logs_injection_enabled", + "logs.injection" : "logs_injection_enabled", + "logInjection" : "logs_injection_enabled", + "DD_LOGS_INJECTION" : "logs_injection_enabled", + "lookup" : "lookup", + "plugins" : "plugins", + "profiling.enabled" : "profiling_enabled", + "DD_PROFILING_ENABLED" : "profiling_enabled", + "profiling.exporters" : "profiling_exporters", + "profiling.sourceMap" : "profiling_source_map_enabled", + "remote_config.enabled" : "remote_config_enabled", + "remoteConfig.enabled" : "remote_config_enabled", + "remoteConfig.pollInterval" : "remote_config_poll_interval", + "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS" : "remote_config_poll_interval", + "DD_INTERNAL_RCM_POLL_INTERVAL" : "remote_config_poll_interval", + "runtimemetrics_enabled" : "runtime_metrics_enabled", + "runtime.metrics.enabled" : "runtime_metrics_enabled", + "runtimeMetrics" : "runtime_metrics_enabled", + "DD_RUNTIME_METRICS_ENABLED" : "runtime_metrics_enabled", + "scope" : "scope", + "service" : "service", + "DD_SERVICE" : "service", + "DD_SERVICE_NAME" : "service", + "site" : "site", + "DD_SITE" : "site", + "stats.enabled" : "stats_enabled", + "traceId128BitGenerationEnabled" : "trace_128_bits_id_enabled", + "trace.128_bit_traceid_generation_enabled" : "trace_128_bits_id_enabled", + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED" : "trace_128_bits_id_enabled", + "traceId128BitLoggingEnabled" : "trace_128_bits_id_logging_enabled", + "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED" : "trace_128_bits_id_logging_enabled", + "trace.128_bit_traceid_logging_enabled" : "trace_128_bits_id_logging_enabled", + "trace.agent.port" : "trace_agent_port", + "port" : "trace_agent_port", + "DD_TRACE_AGENT_PORT" : "trace_agent_port", + "DATADOG_TRACE_AGENT_PORT" : "trace_agent_port", + "DD_APM_RECEIVER_PORT" : "trace_agent_port", + "trace.agent_port" : "trace_agent_port", + "protocolVersion" : "trace_agent_protocol_version", + "agent_url" : "trace_agent_url", + "url" : "trace_agent_url", + "DD_TRACE_AGENT_URL" : "trace_agent_url", + "trace.agent_url" : "trace_agent_url", + "trace.client-ip.enabled" : "trace_client_ip_enabled", + "clientIpEnabled" : "trace_client_ip_enabled", + "DD_TRACE_CLIENT_IP_ENABLED" : "trace_client_ip_enabled", + "trace.client_ip_enabled" : "trace_client_ip_enabled", + "clientIpHeader" : "trace_client_ip_header", + "DD_TRACE_CLIENT_IP_HEADER" : "trace_client_ip_header", + "debug" : "trace_debug_enabled", + "dd.trace.debug" : "trace_debug_enabled", + "DD_TRACE_DEBUG" : "trace_debug_enabled", + "trace.debug" : "trace_debug_enabled", + "enabled" : "trace_enabled", + "trace.enabled" : "trace_enabled", + "tracing" : "trace_enabled", + "DD_TRACE_ENABLED" : "trace_enabled", + "tagsHeaderMaxLength" : "trace_header_tags_max_length", + "logLevel" : "trace_log_level", + "querystringObfuscation" : "trace_obfuscation_query_string_regexp", + "queryStringObfuscation" : "trace_obfuscation_query_string_regexp", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP" : "trace_obfuscation_query_string_regexp", + "trace.obfuscation_query_string_regexp" : "trace_obfuscation_query_string_regexp", + "DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED" : "trace_peer_service_defaults_enabled", + "trace.peer_service_defaults_enabled" : "trace_peer_service_defaults_enabled", + "spanComputePeerService" : "trace_peer_service_defaults_enabled", + "trace.peer.service.defaults.enabled" : "trace_peer_service_defaults_enabled", + "DD_TRACE_PEER_SERVICE_MAPPING" : "trace_peer_service_mapping", + "peerServiceMapping" : "trace_peer_service_mapping", + "trace.peer.service.mapping" : "trace_peer_service_mapping", + "trace.peer_service_mapping" : "trace_peer_service_mapping", + "sampler.rateLimit" : "trace_rate_limit", + "trace.rate.limit" : "trace_rate_limit", + "DD_TRACE_RATE_LIMIT" : "trace_rate_limit", + "DD_MAX_TRACES_PER_SECOND" : "trace_rate_limit", + "trace.rate_limit" : "trace_rate_limit", + "DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED" : "trace_remove_integration_service_names_enabled", + "trace.remove_integration_service_names_enabled" : "trace_remove_integration_service_names_enabled", + "spanRemoveIntegrationFromService" : "trace_remove_integration_service_names_enabled", + "trace.remove.integration-service-names.enabled" : "trace_remove_integration_service_names_enabled", + "reportHostname" : "trace_report_hostname", + "trace.report-hostname" : "trace_report_hostname", + "trace.report_hostname" : "trace_report_hostname", + "sample_rate" : "trace_sample_rate", + "trace.sample.rate" : "trace_sample_rate", + "dd_trace_sample_rate" : "trace_sample_rate", + "sampler.sampleRate" : "trace_sample_rate", + "sampleRate" : "trace_sample_rate", + "DD_TRACE_SAMPLE_RATE" : "trace_sample_rate", + "trace.sample_rate" : "trace_sample_rate", + "spanattributeschema" : "trace_span_attribute_schema", + "DD_TRACE_SPAN_ATTRIBUTE_SCHEMA" : "trace_span_attribute_schema", + "spanAttributeSchema" : "trace_span_attribute_schema", + "trace.span.attribute.schema" : "trace_span_attribute_schema", + "startupLogs" : "trace_startup_logs_enabled", + "DD_TRACE_STARTUP_LOGS" : "trace_startup_logs_enabled", + "global_tag_version" : "version" + }, + "prefix_block_list" : [ + ], + "redaction_list" :[ + ], + "reduce_rules" : { + } +} diff --git a/packages/dd-trace/test/llmobs/writers/base.spec.js b/packages/dd-trace/test/llmobs/writers/base.spec.js index 8b971b2748a..a2880251f4c 100644 --- a/packages/dd-trace/test/llmobs/writers/base.spec.js +++ b/packages/dd-trace/test/llmobs/writers/base.spec.js @@ -138,14 +138,16 @@ describe('BaseLLMObsWriter', () => { writer.append({ foo: 'bar' }) const error = new Error('boom') + let reqUrl request.callsFake((url, options, callback) => { + reqUrl = options.url callback(error) }) writer.flush() expect(logger.error).to.have.been.calledWith( - 'Error sending 1 LLMObs undefined events to https://llmobs-intake.datadoghq.com/api/v2/llmobs: boom' + 'Error sending %d LLMObs %s events to %s: %s', 1, undefined, reqUrl, 'boom', error ) }) diff --git a/packages/dd-trace/test/log.spec.js b/packages/dd-trace/test/log.spec.js index f2ec9a02a1f..a035c864f71 100644 --- a/packages/dd-trace/test/log.spec.js +++ b/packages/dd-trace/test/log.spec.js @@ -160,6 +160,7 @@ describe('log', () => { expect(console.error.firstCall.args[0]).to.have.property('message', 'error') }) + // NOTE: There is no usage for this case. should we continue supporting it? it('should convert empty values to errors', () => { log.error() @@ -191,6 +192,34 @@ describe('log', () => { expect(console.error.firstCall.args[0]).to.be.instanceof(Error) expect(console.error.firstCall.args[0]).to.have.property('message', 'error') }) + + it('should allow a message + Error', () => { + log.error('this is an error', new Error('cause')) + + expect(console.error).to.have.been.called + expect(console.error.firstCall.args[0]).to.be.instanceof(Error) + expect(console.error.firstCall.args[0]).to.have.property('message', 'this is an error') + expect(console.error.secondCall.args[0]).to.be.instanceof(Error) + expect(console.error.secondCall.args[0]).to.have.property('message', 'cause') + }) + + it('should allow a templated message', () => { + log.error('this is an error of type: %s code: %i', 'ERR', 42) + + expect(console.error).to.have.been.called + expect(console.error.firstCall.args[0]).to.be.instanceof(Error) + expect(console.error.firstCall.args[0]).to.have.property('message', 'this is an error of type: ERR code: 42') + }) + + it('should allow a templated message + Error', () => { + log.error('this is an error of type: %s code: %i', 'ERR', 42, new Error('cause')) + + expect(console.error).to.have.been.called + expect(console.error.firstCall.args[0]).to.be.instanceof(Error) + expect(console.error.firstCall.args[0]).to.have.property('message', 'this is an error of type: ERR code: 42') + expect(console.error.secondCall.args[0]).to.be.instanceof(Error) + expect(console.error.secondCall.args[0]).to.have.property('message', 'cause') + }) }) describe('toggle', () => { diff --git a/packages/dd-trace/test/msgpack/encoder.spec.js b/packages/dd-trace/test/msgpack/encoder.spec.js new file mode 100644 index 00000000000..cfda0a9e7d7 --- /dev/null +++ b/packages/dd-trace/test/msgpack/encoder.spec.js @@ -0,0 +1,88 @@ +'use strict' + +require('../setup/tap') + +const { expect } = require('chai') +const msgpack = require('msgpack-lite') +const codec = msgpack.createCodec({ int64: true }) +const { MsgpackEncoder } = require('../../src/msgpack/encoder') + +function randString (length) { + return Array.from({ length }, () => { + return String.fromCharCode(Math.floor(Math.random() * 256)) + }).join('') +} + +describe('msgpack/encoder', () => { + let encoder + + beforeEach(() => { + encoder = new MsgpackEncoder() + }) + + it('should encode to msgpack', () => { + const data = [ + { first: 'test' }, + { + fixstr: 'foo', + str: randString(1000), + fixuint: 127, + fixint: -31, + uint8: 255, + uint16: 65535, + uint32: 4294967295, + uint53: 9007199254740991, + int8: -15, + int16: -32767, + int32: -2147483647, + int53: -9007199254740991, + float: 12345.6789, + biguint: BigInt('9223372036854775807'), + bigint: BigInt('-9223372036854775807'), + buffer: Buffer.from('test'), + uint8array: new Uint8Array([1, 2, 3, 4]), + uint32array: new Uint32Array([1, 2]) + } + ] + + const buffer = encoder.encode(data) + const decoded = msgpack.decode(buffer, { codec }) + + expect(decoded).to.be.an('array') + expect(decoded[0]).to.be.an('object') + expect(decoded[0]).to.have.property('first', 'test') + expect(decoded[1]).to.be.an('object') + expect(decoded[1]).to.have.property('fixstr', 'foo') + expect(decoded[1]).to.have.property('str') + expect(decoded[1].str).to.have.length(1000) + expect(decoded[1]).to.have.property('fixuint', 127) + expect(decoded[1]).to.have.property('fixint', -31) + expect(decoded[1]).to.have.property('uint8', 255) + expect(decoded[1]).to.have.property('uint16', 65535) + expect(decoded[1]).to.have.property('uint32', 4294967295) + expect(decoded[1]).to.have.property('uint53') + expect(decoded[1].uint53.toString()).to.equal('9007199254740991') + expect(decoded[1]).to.have.property('int8', -15) + expect(decoded[1]).to.have.property('int16', -32767) + expect(decoded[1]).to.have.property('int32', -2147483647) + expect(decoded[1]).to.have.property('int53') + expect(decoded[1].int53.toString()).to.equal('-9007199254740991') + expect(decoded[1]).to.have.property('float', 12345.6789) + expect(decoded[1]).to.have.property('biguint') + expect(decoded[1].biguint.toString()).to.equal('9223372036854775807') + expect(decoded[1]).to.have.property('bigint') + expect(decoded[1].bigint.toString()).to.equal('-9223372036854775807') + expect(decoded[1]).to.have.property('buffer') + expect(decoded[1].buffer.toString('utf8')).to.equal('test') + expect(decoded[1]).to.have.property('buffer') + expect(decoded[1].buffer.toString('utf8')).to.equal('test') + expect(decoded[1]).to.have.property('uint8array') + expect(decoded[1].uint8array[0]).to.equal(1) + expect(decoded[1].uint8array[1]).to.equal(2) + expect(decoded[1].uint8array[2]).to.equal(3) + expect(decoded[1].uint8array[3]).to.equal(4) + expect(decoded[1]).to.have.property('uint32array') + expect(decoded[1].uint32array[0]).to.equal(1) + expect(decoded[1].uint32array[4]).to.equal(2) + }) +}) diff --git a/packages/dd-trace/test/opentelemetry/span.spec.js b/packages/dd-trace/test/opentelemetry/span.spec.js index 578d92a6224..9250b701225 100644 --- a/packages/dd-trace/test/opentelemetry/span.spec.js +++ b/packages/dd-trace/test/opentelemetry/span.spec.js @@ -325,6 +325,33 @@ describe('OTel Span', () => { expect(_links).to.have.lengthOf(2) }) + it('should add span pointers', () => { + const span = makeSpan('name') + const { _links } = span._ddSpan + + span.addSpanPointer('pointer_kind', 'd', 'abc123') + expect(_links).to.have.lengthOf(1) + expect(_links[0].attributes).to.deep.equal({ + 'ptr.kind': 'pointer_kind', + 'ptr.dir': 'd', + 'ptr.hash': 'abc123', + 'link.kind': 'span-pointer' + }) + expect(_links[0].context.toTraceId()).to.equal('0') + expect(_links[0].context.toSpanId()).to.equal('0') + + span.addSpanPointer('another_kind', 'd', '1234567') + expect(_links).to.have.lengthOf(2) + expect(_links[1].attributes).to.deep.equal({ + 'ptr.kind': 'another_kind', + 'ptr.dir': 'd', + 'ptr.hash': '1234567', + 'link.kind': 'span-pointer' + }) + expect(_links[1].context.toTraceId()).to.equal('0') + expect(_links[1].context.toSpanId()).to.equal('0') + }) + it('should set status', () => { const unset = makeSpan('name') const unsetCtx = unset._ddSpan.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 45ddc905ee4..3e4f6aed3e8 100644 --- a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js @@ -108,7 +108,7 @@ describe('TextMapPropagator', () => { const spanContext = createContext({ baggageItems }) propagator.inject(spanContext, carrier) - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len expect(carrier.baggage).to.be.equal('%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D%F0%9F%90%B6%C3%A9%E6%88%91=%22%2C%3B%5C%F0%9F%90%B6%C3%A9%E6%88%91') }) @@ -406,7 +406,6 @@ describe('TextMapPropagator', () => { }) it('should extract otel baggage items with special characters', () => { - process.env.DD_TRACE_BAGGAGE_ENABLED = true config = new Config() propagator = new TextMapPropagator(config) const carrier = { @@ -452,6 +451,20 @@ describe('TextMapPropagator', () => { expect(spanContextD._baggageItems).to.deep.equal({}) }) + it('should extract baggage when it is the only propagation style', () => { + config = new Config({ + tracePropagationStyle: { + extract: ['baggage'] + } + }) + propagator = new TextMapPropagator(config) + const carrier = { + baggage: 'foo=bar' + } + const spanContext = propagator.extract(carrier) + expect(spanContext._baggageItems).to.deep.equal({ foo: 'bar' }) + }) + it('should convert signed IDs to unsigned', () => { textMap['x-datadog-trace-id'] = '-123' textMap['x-datadog-parent-id'] = '-456' @@ -692,6 +705,23 @@ describe('TextMapPropagator', () => { } }) + it('should create span links when traces have inconsistent traceids', () => { + // Add a traceparent header and it will prioritize it + const traceId = '1111aaaa2222bbbb3333cccc4444dddd' + const spanId = '5555eeee6666ffff' + textMap.traceparent = `00-${traceId}-${spanId}-01` + + config.tracePropagationStyle.extract = ['tracecontext', 'datadog'] + + const first = propagator.extract(textMap) + + expect(first._links.length).to.equal(1) + expect(first._links[0].context.toTraceId()).to.equal(textMap['x-datadog-trace-id']) + expect(first._links[0].context.toSpanId()).to.equal(textMap['x-datadog-parent-id']) + expect(first._links[0].attributes.reason).to.equal('terminated_context') + expect(first._links[0].attributes.context_headers).to.equal('datadog') + }) + describe('with B3 propagation as multiple headers', () => { beforeEach(() => { config.tracePropagationStyle.extract = ['b3multi'] diff --git a/packages/dd-trace/test/opentracing/span.spec.js b/packages/dd-trace/test/opentracing/span.spec.js index 87d22114aa1..7fa3348a251 100644 --- a/packages/dd-trace/test/opentracing/span.spec.js +++ b/packages/dd-trace/test/opentracing/span.spec.js @@ -300,6 +300,37 @@ describe('Span', () => { }) }) + describe('span pointers', () => { + it('should add a span pointer with a zero context', () => { + // Override id stub for this test to return '0' when called with '0' + id.withArgs('0').returns('0') + + span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' }) + + span.addSpanPointer('pointer_kind', 'd', 'abc123') + expect(span._links).to.have.lengthOf(1) + expect(span._links[0].context.toTraceId()).to.equal('0') + expect(span._links[0].context.toSpanId()).to.equal('0') + expect(span._links[0].attributes).to.deep.equal({ + 'ptr.kind': 'pointer_kind', + 'ptr.dir': 'd', + 'ptr.hash': 'abc123', + 'link.kind': 'span-pointer' + }) + }) + + span.addSpanPointer('another_kind', 'd', '1234567') + expect(span._links).to.have.lengthOf(2) + expect(span._links[1].attributes).to.deep.equal({ + 'ptr.kind': 'another_kind', + 'ptr.dir': 'd', + 'ptr.hash': '1234567', + 'link.kind': 'span-pointer' + }) + expect(span._links[1].context.toTraceId()).to.equal('0') + expect(span._links[1].context.toSpanId()).to.equal('0') + }) + describe('events', () => { it('should add span events', () => { span = new Span(tracer, processor, prioritySampler, { operationName: 'operation' }) diff --git a/packages/dd-trace/test/opentracing/span_context.spec.js b/packages/dd-trace/test/opentracing/span_context.spec.js index cfa184d433b..b590d9074f5 100644 --- a/packages/dd-trace/test/opentracing/span_context.spec.js +++ b/packages/dd-trace/test/opentracing/span_context.spec.js @@ -48,6 +48,7 @@ describe('SpanContext', () => { _tags: {}, _sampling: { priority: 2 }, _spanSampling: undefined, + _links: [], _baggageItems: { foo: 'bar' }, _noop: noop, _trace: { @@ -77,6 +78,7 @@ describe('SpanContext', () => { _tags: {}, _sampling: {}, _spanSampling: undefined, + _links: [], _baggageItems: {}, _noop: null, _trace: { diff --git a/packages/dd-trace/test/plugins/agent.js b/packages/dd-trace/test/plugins/agent.js index cb6f241e7d3..041cbf73967 100644 --- a/packages/dd-trace/test/plugins/agent.js +++ b/packages/dd-trace/test/plugins/agent.js @@ -69,6 +69,24 @@ function dsmStatsExist (agent, expectedHash, expectedEdgeTags) { return hashFound } +function dsmStatsExistWithParentHash (agent, expectedParentHash) { + const dsmStats = agent.getDsmStats() + let hashFound = false + if (dsmStats.length !== 0) { + for (const statsTimeBucket of dsmStats) { + for (const statsBucket of statsTimeBucket.Stats) { + for (const stats of statsBucket.Stats) { + if (stats.ParentHash.toString() === expectedParentHash) { + hashFound = true + return hashFound + } + } + } + } + } + return hashFound +} + function addEnvironmentVariablesToHeaders (headers) { // get all environment variables that start with "DD_" const ddEnvVars = new Map( @@ -424,5 +442,6 @@ module.exports = { tracer, testedPlugins, getDsmStats, - dsmStatsExist + dsmStatsExist, + dsmStatsExistWithParentHash } diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index 5b00aa6061c..c3fc12fb176 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -98,7 +98,7 @@ }, { "name": "express", - "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.3.0"] + "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.0.0 <5.0.0", ">=4.3.0 <5.0.0"] }, { "name": "body-parser", @@ -271,6 +271,12 @@ "versions": ["6.1.0"] } ], + "langchain": [ + { + "name": "@langchain/anthropic", + "versions": [">=0.1"] + } + ], "ldapjs": [ { "name": "ldapjs", @@ -326,7 +332,7 @@ }, { "name": "express", - "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.3.0"] + "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.0.0 <5.0.0", ">=4.3.0 <5.0.0"] }, { "name": "body-parser", @@ -410,6 +416,10 @@ { "name": "express", "versions": [">=4"] + }, + { + "name": "sqlite3", + "versions": ["^5.0.8"] } ] } diff --git a/packages/dd-trace/test/profiling/exporters/agent.spec.js b/packages/dd-trace/test/profiling/exporters/agent.spec.js index 93ff52468f1..4009b70fb13 100644 --- a/packages/dd-trace/test/profiling/exporters/agent.spec.js +++ b/packages/dd-trace/test/profiling/exporters/agent.spec.js @@ -316,7 +316,7 @@ describe('exporters/agent', function () { } let index = 0 - const exporter = newAgentExporter({ url, logger: { debug: onMessage, error: onMessage } }) + const exporter = newAgentExporter({ url, logger: { debug: onMessage, warn: onMessage } }) const start = new Date() const end = new Date() const tags = { foo: 'bar' } @@ -353,7 +353,7 @@ describe('exporters/agent', function () { }) it('should not retry on 4xx errors', async function () { - const exporter = newAgentExporter({ url, logger: { debug: () => {}, error: () => {} } }) + const exporter = newAgentExporter({ url, logger: { debug: () => {}, warn: () => {} } }) const start = new Date() const end = new Date() const tags = { foo: 'bar' } diff --git a/packages/dd-trace/test/profiling/exporters/file.spec.js b/packages/dd-trace/test/profiling/exporters/file.spec.js index bca561dce8b..36b0d257ece 100644 --- a/packages/dd-trace/test/profiling/exporters/file.spec.js +++ b/packages/dd-trace/test/profiling/exporters/file.spec.js @@ -25,9 +25,13 @@ describe('exporters/file', () => { const profiles = { test: buffer } - await exporter.export({ profiles, end: new Date('2023-02-10T21:03:05Z') }) + await exporter.export({ + profiles, + start: new Date('2023-02-10T21:02:05Z'), + end: new Date('2023-02-10T21:03:05Z') + }) - sinon.assert.calledOnce(fs.writeFile) + sinon.assert.calledTwice(fs.writeFile) sinon.assert.calledWith(fs.writeFile, 'test_worker_0_20230210T210305Z.pprof', buffer) }) @@ -37,9 +41,13 @@ describe('exporters/file', () => { const profiles = { test: buffer } - await exporter.export({ profiles, end: new Date('2023-02-10T21:03:05Z') }) + await exporter.export({ + profiles, + start: new Date('2023-02-10T21:02:05Z'), + end: new Date('2023-02-10T21:03:05Z') + }) - sinon.assert.calledOnce(fs.writeFile) + sinon.assert.calledTwice(fs.writeFile) sinon.assert.calledWith(fs.writeFile, 'myprefix_test_worker_0_20230210T210305Z.pprof', buffer) }) }) diff --git a/packages/dd-trace/test/profiling/profiler.spec.js b/packages/dd-trace/test/profiling/profiler.spec.js index d99eb6135ea..d1ad3be734f 100644 --- a/packages/dd-trace/test/profiling/profiler.spec.js +++ b/packages/dd-trace/test/profiling/profiler.spec.js @@ -272,7 +272,7 @@ describe('profiler', function () { await waitForExport() - sinon.assert.calledOnce(consoleLogger.error) + sinon.assert.calledOnce(consoleLogger.warn) }) it('should log encoded profile', async () => { diff --git a/packages/dd-trace/test/proxy.spec.js b/packages/dd-trace/test/proxy.spec.js index 4836e99787f..dd145390245 100644 --- a/packages/dd-trace/test/proxy.spec.js +++ b/packages/dd-trace/test/proxy.spec.js @@ -520,8 +520,9 @@ describe('TracerProxy', () => { const profilerImportFailureProxy = new ProfilerImportFailureProxy() profilerImportFailureProxy.init() + sinon.assert.calledOnce(log.error) const expectedErr = sinon.match.instanceOf(Error).and(sinon.match.has('code', 'MODULE_NOT_FOUND')) - sinon.assert.calledWith(log.error, sinon.match(expectedErr)) + sinon.assert.match(log.error.firstCall.lastArg, sinon.match(expectedErr)) }) it('should start telemetry', () => { diff --git a/packages/dd-trace/test/sampling_rule.spec.js b/packages/dd-trace/test/sampling_rule.spec.js index 49ce1153d2e..609afe385ec 100644 --- a/packages/dd-trace/test/sampling_rule.spec.js +++ b/packages/dd-trace/test/sampling_rule.spec.js @@ -120,6 +120,30 @@ describe('sampling rule', () => { expect(rule.match(spans[10])).to.equal(false) }) + it('should match with case-insensitive strings', () => { + const lowerCaseRule = new SamplingRule({ + service: 'test', + name: 'operation' + }) + + const mixedCaseRule = new SamplingRule({ + service: 'teSt', + name: 'oPeration' + }) + + expect(lowerCaseRule.match(spans[0])).to.equal(mixedCaseRule.match(spans[0])) + expect(lowerCaseRule.match(spans[1])).to.equal(mixedCaseRule.match(spans[1])) + expect(lowerCaseRule.match(spans[2])).to.equal(mixedCaseRule.match(spans[2])) + expect(lowerCaseRule.match(spans[3])).to.equal(mixedCaseRule.match(spans[3])) + expect(lowerCaseRule.match(spans[4])).to.equal(mixedCaseRule.match(spans[4])) + expect(lowerCaseRule.match(spans[5])).to.equal(mixedCaseRule.match(spans[5])) + expect(lowerCaseRule.match(spans[6])).to.equal(mixedCaseRule.match(spans[6])) + expect(lowerCaseRule.match(spans[7])).to.equal(mixedCaseRule.match(spans[7])) + expect(lowerCaseRule.match(spans[8])).to.equal(mixedCaseRule.match(spans[8])) + expect(lowerCaseRule.match(spans[9])).to.equal(mixedCaseRule.match(spans[9])) + expect(lowerCaseRule.match(spans[10])).to.equal(mixedCaseRule.match(spans[10])) + }) + it('should match with regexp', () => { rule = new SamplingRule({ service: /test/, diff --git a/packages/dd-trace/test/setup/helpers/load-inst.js b/packages/dd-trace/test/setup/helpers/load-inst.js new file mode 100644 index 00000000000..91abd8baa77 --- /dev/null +++ b/packages/dd-trace/test/setup/helpers/load-inst.js @@ -0,0 +1,62 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const proxyquire = require('proxyquire') + +function loadInstFile (file, instrumentations) { + const instrument = { + addHook (instrumentation) { + instrumentations.push(instrumentation) + } + } + + const instPath = path.join(__dirname, `../../../../datadog-instrumentations/src/${file}`) + + proxyquire.noPreserveCache()(instPath, { + './helpers/instrument': instrument, + '../helpers/instrument': instrument + }) +} + +function loadOneInst (name) { + const instrumentations = [] + + try { + loadInstFile(`${name}/server.js`, instrumentations) + loadInstFile(`${name}/client.js`, instrumentations) + } catch (e) { + try { + loadInstFile(`${name}/main.js`, instrumentations) + } catch (e) { + loadInstFile(`${name}.js`, instrumentations) + } + } + + return instrumentations +} + +function getAllInstrumentations () { + const names = fs.readdirSync(path.join(__dirname, '../../../../', 'datadog-instrumentations', 'src')) + .filter(file => file.endsWith('.js')) + .map(file => file.slice(0, -3)) + + const instrumentations = names.reduce((acc, key) => { + const name = key + let instrumentations = loadOneInst(name) + + instrumentations = instrumentations.filter(i => i.versions) + if (instrumentations.length) { + acc[key] = instrumentations + } + + return acc + }, {}) + + return instrumentations +} + +module.exports = { + getInstrumentation: loadOneInst, + getAllInstrumentations +} diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index d3520c3fe1c..53a2c95897a 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -11,6 +11,7 @@ const agent = require('../plugins/agent') const Nomenclature = require('../../src/service-naming') const { storage } = require('../../../datadog-core') const { schemaDefinitions } = require('../../src/service-naming/schemas') +const { getInstrumentation } = require('./helpers/load-inst') global.withVersions = withVersions global.withExports = withExports @@ -19,38 +20,6 @@ global.withPeerService = withPeerService const testedPlugins = agent.testedPlugins -function loadInst (plugin) { - const instrumentations = [] - - try { - loadInstFile(`${plugin}/server.js`, instrumentations) - loadInstFile(`${plugin}/client.js`, instrumentations) - } catch (e) { - try { - loadInstFile(`${plugin}/main.js`, instrumentations) - } catch (e) { - loadInstFile(`${plugin}.js`, instrumentations) - } - } - - return instrumentations -} - -function loadInstFile (file, instrumentations) { - const instrument = { - addHook (instrumentation) { - instrumentations.push(instrumentation) - } - } - - const instPath = path.join(__dirname, `../../../datadog-instrumentations/src/${file}`) - - proxyquire.noPreserveCache()(instPath, { - './helpers/instrument': instrument, - '../helpers/instrument': instrument - }) -} - function withNamingSchema ( spanProducerFn, expected, @@ -174,7 +143,7 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service } function withVersions (plugin, modules, range, cb) { - const instrumentations = typeof plugin === 'string' ? loadInst(plugin) : [].concat(plugin) + const instrumentations = typeof plugin === 'string' ? getInstrumentation(plugin) : [].concat(plugin) const names = instrumentations.map(instrumentation => instrumentation.name) modules = [].concat(modules) diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index 306d7a16c30..0263f395e9f 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -409,7 +409,7 @@ describe('Telemetry extended heartbeat', () => { { name: 'DD_TRACE_SAMPLING_RULES', value: - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len '[{"service":"*","sampling_rate":1},{"service":"svc*","resource":"*abc","name":"op-??","tags":{"tag-a":"ta-v*","tag-b":"tb-v?","tag-c":"tc-v"},"sample_rate":0.5}]', origin: 'code' } diff --git a/packages/dd-trace/test/telemetry/logs/index.spec.js b/packages/dd-trace/test/telemetry/logs/index.spec.js index f00c8f17655..e865644e960 100644 --- a/packages/dd-trace/test/telemetry/logs/index.spec.js +++ b/packages/dd-trace/test/telemetry/logs/index.spec.js @@ -4,6 +4,7 @@ require('../../setup/tap') const { match } = require('sinon') const proxyquire = require('proxyquire') +const { Log } = require('../../../src/log/log') describe('telemetry logs', () => { let defaultConfig @@ -141,13 +142,19 @@ describe('telemetry logs', () => { it('should be called when an Error object is published to datadog:log:error', () => { const error = new Error('message') const stack = error.stack - errorLog.publish(error) - - expect(logCollectorAdd).to.be.calledOnceWith(match({ message: 'message', level: 'ERROR', stack_trace: stack })) + errorLog.publish({ cause: error }) + + expect(logCollectorAdd) + .to.be.calledOnceWith(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('custom error message') + errorLog.publish({ message: 'custom error message' }) expect(logCollectorAdd).to.be.calledOnceWith(match({ message: 'custom error message', @@ -161,6 +168,12 @@ describe('telemetry logs', () => { expect(logCollectorAdd).not.to.be.called }) + + it('should not be called when an object without message and stack is published to datadog:log:error', () => { + errorLog.publish(Log.parse(() => new Error('error'))) + + expect(logCollectorAdd).not.to.be.called + }) }) }) 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 168378a2251..57600dcb441 100644 --- a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js +++ b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js @@ -43,9 +43,18 @@ describe('telemetry log collector', () => { expect(logCollector.add({ message: 'Error 1', level: 'DEBUG', stack_trace: `stack 1\n${ddFrame}` })).to.be.true }) + it('should not store logs with empty stack and \'Generic Error\' message', () => { + expect(logCollector.add({ + message: 'Generic Error', + level: 'ERROR', + stack_trace: 'stack 1\n/not/a/dd/frame' + }) + ).to.be.false + }) + it('should include original message and dd frames', () => { const ddFrame = `at T (${ddBasePath}packages/dd-trace/test/telemetry/logs/log_collector.spec.js:29:21)` - const stack = new Error('Error 1') + const stack = new TypeError('Error 1') .stack.replace(`Error 1${EOL}`, `Error 1${EOL}${ddFrame}${EOL}`) const ddFrames = stack @@ -54,30 +63,43 @@ describe('telemetry log collector', () => { .map(line => line.replace(ddBasePath, '')) .join(EOL) - expect(logCollector.add({ message: 'Error 1', level: 'ERROR', stack_trace: stack })).to.be.true + expect(logCollector.add({ + message: 'Error 1', + level: 'ERROR', + stack_trace: stack, + errorType: 'TypeError' + })).to.be.true expect(logCollector.hasEntry({ message: 'Error 1', level: 'ERROR', - stack_trace: `Error: Error 1${EOL}${ddFrames}` + stack_trace: `TypeError: Error 1${EOL}${ddFrames}` })).to.be.true }) - it('should not include original message if first frame is not a dd frame', () => { + it('should redact stack message if first frame is not a dd frame', () => { const thirdPartyFrame = `at callFn (/this/is/not/a/dd/frame/runnable.js:366:21) at T (${ddBasePath}packages/dd-trace/test/telemetry/logs/log_collector.spec.js:29:21)` - const stack = new Error('Error 1') + const stack = new TypeError('Error 1') .stack.replace(`Error 1${EOL}`, `Error 1${EOL}${thirdPartyFrame}${EOL}`) - const ddFrames = stack - .split(EOL) - .filter(line => line.includes(ddBasePath)) - .map(line => line.replace(ddBasePath, '')) - .join(EOL) + const ddFrames = [ + 'TypeError: redacted', + ...stack + .split(EOL) + .filter(line => line.includes(ddBasePath)) + .map(line => line.replace(ddBasePath, '')) + ].join(EOL) + + expect(logCollector.add({ + message: 'Error 1', + level: 'ERROR', + stack_trace: stack, + errorType: 'TypeError' + })).to.be.true - expect(logCollector.add({ message: 'Error 1', level: 'ERROR', stack_trace: stack })).to.be.true expect(logCollector.hasEntry({ - message: 'omitted', + message: 'Error 1', level: 'ERROR', stack_trace: ddFrames })).to.be.true @@ -104,5 +126,22 @@ describe('telemetry log collector', () => { expect(logs.length).to.be.equal(4) expect(logs[3]).to.deep.eq({ message: 'Omitted 2 entries due to overflowing', level: 'ERROR' }) }) + + it('duplicated errors should send incremented count values', () => { + const err1 = { message: 'oh no', level: 'ERROR', count: 1 } + + const err2 = { message: 'foo buzz', level: 'ERROR', count: 1 } + + logCollector.add(err1) + logCollector.add(err2) + logCollector.add(err1) + logCollector.add(err2) + logCollector.add(err1) + + const drainedErrors = logCollector.drain() + expect(drainedErrors.length).to.be.equal(2) + expect(drainedErrors[0].count).to.be.equal(3) + expect(drainedErrors[1].count).to.be.equal(2) + }) }) }) diff --git a/packages/dd-trace/test/util.spec.js b/packages/dd-trace/test/util.spec.js index f32b47c0cee..40b209a96cf 100644 --- a/packages/dd-trace/test/util.spec.js +++ b/packages/dd-trace/test/util.spec.js @@ -3,6 +3,7 @@ require('./setup/tap') const { isTrue, isFalse, globMatch } = require('../src/util') +const { generatePointerHash } = require('../src/util') const TRUES = [ 1, @@ -68,3 +69,20 @@ describe('util', () => { }) }) }) + +describe('generatePointerHash', () => { + it('should generate a valid hash for a basic S3 object', () => { + const hash = generatePointerHash(['some-bucket', 'some-key.data', 'ab12ef34']) + expect(hash).to.equal('e721375466d4116ab551213fdea08413') + }) + + it('should generate a valid hash for an S3 object with a non-ascii key', () => { + const hash1 = generatePointerHash(['some-bucket', 'some-key.你好', 'ab12ef34']) + expect(hash1).to.equal('d1333a04b9928ab462b5c6cadfa401f4') + }) + + it('should generate a valid hash for multipart-uploaded S3 object', () => { + const hash1 = generatePointerHash(['some-bucket', 'some-key.data', 'ab12ef34-5']) + expect(hash1).to.equal('2b90dffc37ebc7bc610152c3dc72af9f') + }) +}) diff --git a/repository.datadog.yml b/repository.datadog.yml new file mode 100644 index 00000000000..ded5018823b --- /dev/null +++ b/repository.datadog.yml @@ -0,0 +1,4 @@ +--- +schema-version: v1 +kind: mergequeue +enable: false diff --git a/requirements.json b/requirements.json index 85fc7c33894..9ba115f8a6a 100644 --- a/requirements.json +++ b/requirements.json @@ -46,40 +46,49 @@ "id": "npm", "description": "Ignore the npm CLI", "os": null, - "cmds": [ - "**/node", - "**/nodejs", - "**/ts-node", - "**/ts-node-*" - ], + "cmds": [], "args": [{ "args": ["*/npm-cli.js"], "position": 1}], "envars": null }, + { + "id": "npm_symlink", + "description": "Ignore the npm CLI (symlink)", + "os": null, + "cmds": [], + "args": [{ "args": ["*/npm"], "position": 1}], + "envars": null + }, { "id": "yarn", "description": "Ignore the yarn CLI", "os": null, - "cmds": [ - "**/node", - "**/nodejs", - "**/ts-node", - "**/ts-node-*" - ], + "cmds": [], "args": [{ "args": ["*/yarn.js"], "position": 1}], "envars": null }, + { + "id": "yarn_symlink", + "description": "Ignore the yarn CLI (symlink)", + "os": null, + "cmds": [], + "args": [{ "args": ["*/yarn"], "position": 1}], + "envars": null + }, { "id": "pnpm", "description": "Ignore the pnpm CLI", "os": null, - "cmds": [ - "**/node", - "**/nodejs", - "**/ts-node", - "**/ts-node-*" - ], + "cmds": [], "args": [{ "args": ["*/pnpm.cjs"], "position": 1}], "envars": null + }, + { + "id": "pnpm_symlink", + "description": "Ignore the pnpm CLI (symlink)", + "os": null, + "cmds": [], + "args": [{ "args": ["*/pnpm"], "position": 1}], + "envars": null } ] } diff --git a/scripts/install_plugin_modules.js b/scripts/install_plugin_modules.js index 682e2d3c5ad..212dc5928ed 100644 --- a/scripts/install_plugin_modules.js +++ b/scripts/install_plugin_modules.js @@ -5,10 +5,10 @@ const os = require('os') const path = require('path') const crypto = require('crypto') const semver = require('semver') -const proxyquire = require('proxyquire') const exec = require('./helpers/exec') const childProcess = require('child_process') const externals = require('../packages/dd-trace/test/plugins/externals') +const { getInstrumentation } = require('../packages/dd-trace/test/setup/helpers/load-inst') const requirePackageJsonPath = require.resolve('../packages/dd-trace/src/require-package-json') @@ -47,19 +47,7 @@ async function run () { async function assertVersions () { const internals = names - .map(key => { - const instrumentations = [] - const name = key - - try { - loadInstFile(`${name}/server.js`, instrumentations) - loadInstFile(`${name}/client.js`, instrumentations) - } catch (e) { - loadInstFile(`${name}.js`, instrumentations) - } - - return instrumentations - }) + .map(getInstrumentation) .reduce((prev, next) => prev.concat(next), []) for (const inst of internals) { @@ -117,10 +105,10 @@ function assertFolder (name, version) { } } -async function assertPackage (name, version, dependency, external) { - const dependencies = { [name]: dependency } +async function assertPackage (name, version, dependencyVersionRange, external) { + const dependencies = { [name]: dependencyVersionRange } if (deps[name]) { - await addDependencies(dependencies, name, dependency) + await addDependencies(dependencies, name, dependencyVersionRange) } const pkg = { name: [name, sha1(name).substr(0, 8), sha1(version)].filter(val => val).join('-'), @@ -151,7 +139,13 @@ async function addDependencies (dependencies, name, versionRange) { for (const dep of deps[name]) { for (const section of ['devDependencies', 'peerDependencies']) { if (pkgJson[section] && dep in pkgJson[section]) { - dependencies[dep] = pkgJson[section][dep] + if (pkgJson[section][dep].includes('||')) { + // Use the first version in the list (as npm does by default) + dependencies[dep] = pkgJson[section][dep].split('||')[0].trim() + } else { + // Only one version available so use that. + dependencies[dep] = pkgJson[section][dep] + } break } } @@ -234,18 +228,3 @@ function sha1 (str) { shasum.update(str) return shasum.digest('hex') } - -function loadInstFile (file, instrumentations) { - const instrument = { - addHook (instrumentation) { - instrumentations.push(instrumentation) - } - } - - const instPath = path.join(__dirname, `../packages/datadog-instrumentations/src/${file}`) - - proxyquire.noPreserveCache()(instPath, { - './helpers/instrument': instrument, - '../helpers/instrument': instrument - }) -} diff --git a/scripts/release/helpers/requirements.js b/scripts/release/helpers/requirements.js new file mode 100644 index 00000000000..a2da9f924bb --- /dev/null +++ b/scripts/release/helpers/requirements.js @@ -0,0 +1,85 @@ +'use strict' + +/* eslint-disable @stylistic/js/max-len */ + +const { capture, fatal, run } = require('./terminal') + +const requiredScopes = ['public_repo', 'read:org'] + +// Check that the `git` CLI is installed. +function checkGit () { + try { + run('git --version') + } catch (e) { + fatal( + 'The "git" CLI could not be found.', + 'Please visit https://git-scm.com/downloads for instructions to install.' + ) + } +} + +// Check that the `branch-diff` CLI is installed. +function checkBranchDiff () { + try { + run('branch-diff --version') + } catch (e) { + const link = [ + 'https://datadoghq.atlassian.net/wiki/spaces/DL/pages/3125511269/Node.js+Tracer+Release+Process', + '#Install-and-Configure-branch-diff-to-automate-some-operations' + ].join('') + fatal( + 'The "branch-diff" CLI could not be found.', + `Please visit ${link} for instructions to install.` + ) + } +} + +// Check that the `gh` CLI is installed and authenticated. +function checkGitHub () { + if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) { + const link = 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic' + + fatal( + 'The GITHUB_TOKEN environment variable is missing.', + `Please visit ${link} for instructions to generate a personal access token.`, + `The following scopes are required when generating the token: ${requiredScopes.join(', ')}` + ) + } + + try { + run('gh --version') + } catch (e) { + fatal( + 'The "gh" CLI could not be found.', + 'Please visit https://github.com/cli/cli#installation for instructions to install.' + ) + } + + checkGitHubScopes() +} + +// Check that the active GITHUB_TOKEN has the required scopes. +function checkGitHubScopes () { + const url = 'https://api.github.com' + const headers = [ + 'Accept: application/vnd.github.v3+json', + `Authorization: Bearer ${process.env.GITHUB_TOKEN || process.env.GH_TOKEN}`, + 'X-GitHub-Api-Version: 2022-11-28' + ].map(h => `-H "${h}"`).join(' ') + + const lines = capture(`curl -sS -I ${headers} ${url}`).trim().split(/\r?\n/g) + const scopeLine = lines.find(line => line.startsWith('x-oauth-scopes:')) || '' + const scopes = scopeLine.replace('x-oauth-scopes:', '').trim().split(', ') + const link = 'https://github.com/settings/tokens' + + for (const req of requiredScopes) { + if (!scopes.includes(req)) { + fatal( + `Missing "${req}" scope for GITHUB_TOKEN.`, + `Please visit ${link} and make sure the following scopes are enabled: ${requiredScopes.join(' ,')}.` + ) + } + } +} + +module.exports = { checkBranchDiff, checkGitHub, checkGit } diff --git a/scripts/release/helpers/terminal.js b/scripts/release/helpers/terminal.js new file mode 100644 index 00000000000..91128f76fae --- /dev/null +++ b/scripts/release/helpers/terminal.js @@ -0,0 +1,152 @@ +'use strict' + +const { execSync, spawnSync } = require('child_process') + +const { params, flags } = parse() + +const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] + +const BOLD = '\x1b[1m' +const CYAN = '\x1b[36m' +const ERASE = '\x1b[0K' +const GRAY = '\x1b[90m' +const GREEN = '\x1b[32m' +const PREVIOUS = '\x1b[1A' +const RED = '\x1b[31m' +const RESET = '\x1b[0m' + +const print = (...msgs) => msgs.forEach(msg => process.stdout.write(msg)) +const log = (...msgs) => msgs.forEach(msg => print(`${msg}\n`)) +const fatal = (...msgs) => fail() || log(...msgs) || process.exit(1) + +let timer +let current + +// Output a command to the terminal and execute it. +function run (cmd) { + capture(cmd) +} + +// Ask a question in terminal and return the response. +function prompt (question) { + print(`${BOLD}${CYAN}?${RESET} ${BOLD}${question}${RESET} `) + + const child = spawnSync('bash', ['-c', 'read answer && echo $answer'], { + stdio: ['inherit'] + }) + + return child.stdout.toString() +} + +// Ask whether to continue and otherwise exit the process. +function checkpoint (question) { + const answer = prompt(`${question} [Y/n]`).trim() + const prefix = `\r${PREVIOUS}${BOLD}${CYAN}?${RESET}` + + question = `${BOLD}${question}${RESET}` + + if (answer && answer.toLowerCase() !== 'y') { + print(`\r${prefix} ${question} ${BOLD}${CYAN}No${RESET}${ERASE}\n`) + process.exit(0) + } else { + print(`\r${prefix} ${question} ${BOLD}${CYAN}Yes${RESET}${ERASE}\n`) + } +} + +// Run a command and capture its output to return it to the caller. +function capture (cmd) { + if (flags.debug) { + log(`${GRAY}> ${cmd}${RESET}`) + } + + const output = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }).toString().trim() + + if (flags.debug) { + log(output) + } + + return output +} + +// Start an operation and show a spinner until it reports as passing or failing. +function start (title) { + current = title + + spin(0) +} + +// Show a spinner for the current operation. +function spin (index) { + if (flags.debug) return + + print(`\r${CYAN}${frames[index]}${RESET} ${BOLD}${current}${RESET}`) + + timer = setTimeout(spin, 80, index === frames.length - 1 ? 0 : index + 1) +} + +// Finish the current operation as passing. +function pass (result) { + if (!current) return + + clearTimeout(timer) + + if (!flags.debug) { + print(`\r${GREEN}✔${RESET} ${BOLD}${current}${RESET}`) + + if (result) { + print(`: ${BOLD}${CYAN}${result}${RESET}`) + } + + print('\n') + } + + current = undefined +} + +// Finish the current operation as failing. +function fail (err) { + if (!current) return + + clearTimeout(timer) + + if (!flags.debug) { + print(`\r${RED}✘${RESET} ${BOLD}${current}${RESET}\n`) + } + + current = undefined + + if (err) { + throw err + } +} + +// Parse CLI arguments into parameters and flags. +function parse () { + const args = process.argv.slice(2) + const params = [] + const flags = {} + + for (const arg of args) { + if (arg.startsWith('-')) { + const name = arg.replace(/^-+/, '') + flags[name] = true + } else { + params.push(arg) + } + } + + return { params, flags } +} + +module.exports = { + capture, + checkpoint, + fail, + fatal, + flags, + log, + params, + pass, + run, + start +} diff --git a/scripts/release/notes.js b/scripts/release/notes.js new file mode 100644 index 00000000000..b083839dd24 --- /dev/null +++ b/scripts/release/notes.js @@ -0,0 +1,28 @@ +'use strict' + +const fs = require('fs') +const os = require('os') +const path = require('path') +const { capture, run } = require('./helpers/terminal') +const pkg = require('../../package.json') + +const version = pkg.version +const tag = `v${version}` +const major = version.split('.')[0] +const body = capture(`gh pr view ${tag}-proposal --json body --jq '.body'`) +const args = process.argv.slice(2) +const flags = [] +const folder = path.join(os.tmpdir(), 'release_notes') +const file = path.join(folder, `${tag}.md`) + +// Default is to determine this automatically, so set it explicitly instead. +flags.push(args.includes('--latest') ? '--latest' : '--latest=false') + +if (version.includes('-')) { + flags.push('--prerelease') +} + +fs.mkdirSync(folder, { recursive: true }) +fs.writeFileSync(file, body) + +run(`gh release create ${tag} --target v${major}.x --title ${version} -F ${file} ${flags.join(' ')}`) diff --git a/scripts/release/proposal.js b/scripts/release/proposal.js index b5c16de4c0e..1a50bbcaf49 100644 --- a/scripts/release/proposal.js +++ b/scripts/release/proposal.js @@ -1,128 +1,163 @@ 'use strict' -/* eslint-disable no-console */ - // TODO: Support major versions. -const { execSync } = require('child_process') const fs = require('fs') +const os = require('os') const path = require('path') - -// Helpers for colored output. -const log = msg => console.log(msg) -const success = msg => console.log(`\x1b[32m${msg}\x1b[0m`) -const error = msg => console.log(`\x1b[31m${msg}\x1b[0m`) -const whisper = msg => console.log(`\x1b[90m${msg}\x1b[0m`) - -const currentBranch = capture('git branch --show-current') -const releaseLine = process.argv[2] +const { + capture, + checkpoint, + fail, + fatal, + flags, + log, + params, + pass, + start, + run +} = require('./helpers/terminal') +const { checkBranchDiff, checkGitHub, checkGit } = require('./helpers/requirements') + +const releaseLine = params[0] // Validate release line argument. -if (!releaseLine || releaseLine === 'help' || releaseLine === '--help') { - log('Usage: node scripts/release/proposal [release-type]') +if (!releaseLine || releaseLine === 'help' || flags.help) { + log( + 'Usage: node scripts/release/proposal \n', + 'Options:', + ' --debug Print raw commands and their outputs.', + ' --help Show this help.', + ' --minor Force a minor release.', + ' --patch Force a patch release.' + ) process.exit(0) } else if (!releaseLine?.match(/^\d+$/)) { - error('Invalid release line. Must be a whole number.') - process.exit(1) + fatal('Invalid release line. Must be a whole number.') } -// Make sure the release branch is up to date to prepare for new proposal. -// The main branch is not automatically pulled to avoid inconsistencies between -// release lines if new commits are added to it during a release. -run(`git checkout v${releaseLine}.x`) -run('git pull') - -const diffCmd = [ - 'branch-diff', - '--user DataDog', - '--repo dd-trace-js', - isActivePatch() - ? `--exclude-label=semver-major,semver-minor,dont-land-on-v${releaseLine}.x` - : `--exclude-label=semver-major,dont-land-on-v${releaseLine}.x` -].join(' ') - -// Determine the new version. -const [lastMajor, lastMinor, lastPatch] = require('../../package.json').version.split('.').map(Number) -const lineDiff = capture(`${diffCmd} v${releaseLine}.x master`) -const newVersion = lineDiff.includes('SEMVER-MINOR') - ? `${releaseLine}.${lastMinor + 1}.0` - : `${releaseLine}.${lastMinor}.${lastPatch + 1}` - -// Checkout new branch and output new changes. -run(`git checkout v${newVersion}-proposal || git checkout -b v${newVersion}-proposal`) - -// Get the hashes of the last version and the commits to add. -const lastCommit = capture('git log -1 --pretty=%B').trim() -const proposalDiff = capture(`${diffCmd} --format=sha --reverse v${newVersion}-proposal master`) - .replace(/\n/g, ' ').trim() - -if (proposalDiff) { - // We have new commits to add, so revert the version commit if it exists. - if (lastCommit === `v${newVersion}`) { - run('git reset --hard HEAD~1') - } +try { + start('Check for requirements') + + checkGit() + checkBranchDiff() + checkGitHub() + + pass() + + start('Pull release branch') + + // Make sure the release branch is up to date to prepare for new proposal. + // The main branch is not automatically pulled to avoid inconsistencies between + // release lines if new commits are added to it during a release. + run(`git checkout v${releaseLine}.x`) + run('git pull --ff-only') + + pass(`v${releaseLine}.x`) - // Output new changes since last commit of the proposal branch. - run(`${diffCmd} v${newVersion}-proposal master`) + const diffCmd = [ + 'branch-diff', + '--user DataDog', + '--repo dd-trace-js', + `--exclude-label=semver-major,dont-land-on-v${releaseLine}.x` + ].join(' ') + + start('Determine version increment') + + const lastVersion = require('../../package.json').version + const [, lastMinor, lastPatch] = lastVersion.split('.').map(Number) + const lineDiff = capture(`${diffCmd} --markdown=true v${releaseLine}.x master`) + const isMinor = flags.minor || (!flags.patch && lineDiff.includes('SEMVER-MINOR')) + const newVersion = isMinor + ? `${releaseLine}.${lastMinor + 1}.0` + : `${releaseLine}.${lastMinor}.${lastPatch + 1}` + const notesDir = path.join(os.tmpdir(), 'release_notes') + const notesFile = path.join(notesDir, `${newVersion}.md`) + + pass(`${isMinor ? 'minor' : 'patch'} (${lastVersion} -> ${newVersion})`) + + start('Checkout release proposal branch') + + // Checkout new or existing branch. + run(`git checkout v${newVersion}-proposal || git checkout -b v${newVersion}-proposal`) - // Cherry pick all new commits to the proposal branch. try { - run(`echo "${proposalDiff}" | xargs git cherry-pick`) - } catch (err) { - error('Cherry-pick failed. Resolve the conflicts and run `git cherry-pick --continue` to continue.') - error('When all conflicts have been resolved, run this script again.') - process.exit(1) + // Pull latest changes in case the release was started by someone else. + run(`git remote show origin | grep v${newVersion} && git pull --ff-only`) + } catch (e) { + // Either there is no remote to pull from or the local and remote branches + // have diverged. In both cases we ignore the error and will just use our + // changes. } -} -// Update package.json with new version. -run(`npm version --git-tag-version=false ${newVersion}`) -run(`git commit -uno -m v${newVersion} package.json || exit 0`) + pass(`v${newVersion}-proposal`) -ready() + start('Check for new changes') -// Check if current branch is already an active patch proposal branch to avoid -// creating a new minor proposal branch if new minor commits are added to the -// main branch during a existing patch release. -function isActivePatch () { - const currentMatch = currentBranch.match(/^(\d+)\.(\d+)\.(\d+)-proposal$/) + // Get the hashes of the last version and the commits to add. + const lastCommit = capture('git log -1 --pretty=%B').trim() + const proposalDiff = capture(`${diffCmd} --format=sha --reverse v${newVersion}-proposal master`) + .replace(/\n/g, ' ').trim() - if (currentMatch) { - const [major, minor, patch] = currentMatch.slice(1).map(Number) + if (proposalDiff) { + // Get new changes since last commit of the proposal branch. + const newChanges = capture(`${diffCmd} v${newVersion}-proposal master`) - if (major === lastMajor && minor === lastMinor && patch > lastPatch) { - return true - } - } + pass(`\n${newChanges}`) - return false -} + start('Apply changes from the main branch') -// Output a command to the terminal and execute it. -function run (cmd) { - whisper(`> ${cmd}`) + // We have new commits to add, so revert the version commit if it exists. + if (lastCommit === `v${newVersion}`) { + run('git reset --hard HEAD~1') + } - const output = execSync(cmd, {}).toString() + // Cherry pick all new commits to the proposal branch. + try { + run(`echo "${proposalDiff}" | xargs git cherry-pick`) - log(output) -} + pass() + } catch (err) { + fatal( + 'Cherry-pick failed. Resolve the conflicts and run `git cherry-pick --continue` to continue.', + 'When all conflicts have been resolved, run this script again.' + ) + } + } else { + pass('none') + } -// Run a command and capture its output to return it to the caller. -function capture (cmd) { - return execSync(cmd, {}).toString() -} + // Update package.json with new version. + run(`npm version --allow-same-version --git-tag-version=false ${newVersion}`) + run(`git commit -uno -m v${newVersion} package.json || exit 0`) -// Write release notes to a file that can be copied to the GitHub release. -function ready () { - const notesDir = path.join(__dirname, '..', '..', '.github', 'release_notes') - const notesFile = path.join(notesDir, `${newVersion}.md`) - const lineDiff = capture(`${diffCmd} --markdown=true v${releaseLine}.x master`) + start('Save release notes draft') + // Write release notes to a file that can be copied to the GitHub release. fs.mkdirSync(notesDir, { recursive: true }) fs.writeFileSync(notesFile, lineDiff) - success('Release proposal is ready.') - success(`Changelog at .github/release_notes/${newVersion}.md`) + pass(notesFile) - process.exit(0) + // Stop and ask the user if they want to proceed with pushing everything upstream. + checkpoint('Push the release upstream and create/update PR?') + + start('Push proposal upstream') + + run(`git push -f -u origin v${newVersion}-proposal`) + + // Create or edit the PR. This will also automatically output a link to the PR. + try { + run(`gh pr create -d -B v${releaseLine}.x -t "v${newVersion} proposal" -F ${notesFile}`) + } catch (e) { + // PR already exists so update instead. + // TODO: Keep existing non-release-notes PR description if there is one. + run(`gh pr edit -F "${notesFile}"`) + } + + const pullRequestUrl = capture('gh pr view --json url --jq=".url"') + + pass(pullRequestUrl) +} catch (e) { + fail(e) } diff --git a/scripts/verify-ci-config.js b/scripts/verify-ci-config.js new file mode 100644 index 00000000000..2e16ac0f7c3 --- /dev/null +++ b/scripts/verify-ci-config.js @@ -0,0 +1,121 @@ +'use strict' +/* eslint-disable no-console */ + +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 { getAllInstrumentations } = require('../packages/dd-trace/test/setup/helpers/load-inst') + +if (!Module.isBuiltin) { + Module.isBuiltin = mod => Module.builtinModules.includes(mod) +} + +const nodeMajor = Number(process.versions.node.split('.')[0]) + +const instrumentations = getAllInstrumentations() + +const versions = {} + +function checkYaml (yamlPath) { + const yamlContent = yaml.parse(fs.readFileSync(yamlPath, 'utf8')) + + const rangesPerPluginFromYaml = {} + const rangesPerPluginFromInst = {} + for (const jobName in yamlContent.jobs) { + const job = yamlContent.jobs[jobName] + if (!job.env || !job.env.PLUGINS) continue + + const pluginName = job.env.PLUGINS + if (Module.isBuiltin(pluginName)) continue + const rangesFromYaml = getRangesFromYaml(job) + if (rangesFromYaml) { + if (!rangesPerPluginFromYaml[pluginName]) { + rangesPerPluginFromYaml[pluginName] = new Set() + } + rangesFromYaml.forEach(range => rangesPerPluginFromYaml[pluginName].add(range)) + const plugin = instrumentations[pluginName] + const allRangesForPlugin = new Set(plugin.map(x => x.versions).flat()) + rangesPerPluginFromInst[pluginName] = allRangesForPlugin + } + } + for (const pluginName in rangesPerPluginFromYaml) { + const yamlRanges = Array.from(rangesPerPluginFromYaml[pluginName]) + const instRanges = Array.from(rangesPerPluginFromInst[pluginName]) + const yamlVersions = getMatchingVersions(pluginName, yamlRanges) + const instVersions = getMatchingVersions(pluginName, instRanges) + if (!util.isDeepStrictEqual(yamlVersions, instVersions)) { + const opts = { colors: true } + const colors = x => util.inspect(x, opts) + errorMsg(pluginName, 'Mismatch', ` +Valid version ranges from YAML: ${colors(yamlRanges)} +Valid version ranges from INST: ${colors(instRanges)} +${mismatching(yamlVersions, instVersions)} +Note that versions may be dependent on Node.js version. This is Node.js v${colors(nodeMajor)} + +> These don't match the same sets of versions in npm. +> +> Please check ${yamlPath} and the instrumentations +> for ${pluginName} to see that the version ranges match.`.trim()) + } + } +} + +function getRangesFromYaml (job) { + // eslint-disable-next-line no-template-curly-in-string + if (job.env && job.env.PACKAGE_VERSION_RANGE && job.env.PACKAGE_VERSION_RANGE !== '${{ matrix.range }}') { + errorMsg(job.env.PLUGINS, 'ERROR in YAML', 'You must use matrix.range instead of env.PACKAGE_VERSION_RANGE') + process.exitCode = 1 + } + if (job.strategy && job.strategy.matrix && job.strategy.matrix.range) { + const possibilities = [job.strategy.matrix] + if (job.strategy.matrix.include) { + possibilities.push(...job.strategy.matrix.include) + } + return possibilities.map(possibility => { + if (possibility.range) { + return [possibility.range].flat() + } else { + return undefined + } + }).flat() + } + + return null +} + +function getMatchingVersions (name, ranges) { + if (!versions[name]) { + versions[name] = JSON.parse(execSync('npm show ' + name + ' versions --json').toString()) + } + return versions[name].filter(version => ranges.some(range => semver.satisfies(version, range))) +} + +checkYaml(path.join(__dirname, '..', '.github', 'workflows', 'plugins.yml')) +checkYaml(path.join(__dirname, '..', '.github', 'workflows', 'appsec.yml')) + +function mismatching (yamlVersions, instVersions) { + const yamlSet = new Set(yamlVersions) + const instSet = new Set(instVersions) + + const onlyInYaml = yamlVersions.filter(v => !instSet.has(v)) + const onlyInInst = instVersions.filter(v => !yamlSet.has(v)) + + const opts = { colors: true } + return [ + `Versions only in YAML: ${util.inspect(onlyInYaml, opts)}`, + `Versions only in INST: ${util.inspect(onlyInInst, opts)}` + ].join('\n') +} + +function errorMsg (pluginName, title, message) { + console.log('===========================================') + console.log(title + ' for ' + pluginName) + console.log('-------------------------------------------') + console.log(message) + console.log('\n') + process.exitCode = 1 +} diff --git a/yarn.lock b/yarn.lock index de5a02ece2f..a56218a0a45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -401,22 +401,22 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity "sha1-u1BFecHK6SPmV2pPXaQ9Jfl729k= sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" -"@datadog/libdatadog@^0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.2.2.tgz#ac02c76ac9a38250dca740727c7cdf00244ce3d3" - integrity sha512-rTWo96mEPTY5UbtGoFj8/wY0uKSViJhsPg/Z6aoFWBFXQ8b45Ix2e/yvf92AAwrhG+gPLTxEqTXh3kef2dP8Ow== +"@datadog/libdatadog@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.3.0.tgz#2fc1e2695872840bc8c356f66acf675da428d6f0" + integrity sha512-TbP8+WyXfh285T17FnLeLUOPl4SbkRYMqKgcmknID2mXHNrbt5XJgW9bnDgsrrtu31Q7FjWWw2WolgRLWyzLRA== -"@datadog/native-appsec@8.2.1": - version "8.2.1" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.2.1.tgz#e84f9ec7e5dddea2531970117744264a685da15a" - integrity sha512-PnSlb4DC+EngEfXvZLYVBUueMnxxQV0dTpwbRQmyC6rcIFBzBCPxUl6O0hZaxCNmT1dgllpif+P1efrSi85e0Q== +"@datadog/native-appsec@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.3.0.tgz#91afd89d18d386be4da8a1b0e04500f2f8b5eb66" + integrity sha512-RYHbSJ/MwJcJaLzaCaZvUyNLUKFbMshayIiv4ckpFpQJDiq1T8t9iM2k7008s75g1vRuXfsRNX7MaLn4aoFuWA== dependencies: node-gyp-build "^3.9.0" -"@datadog/native-iast-rewriter@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.5.0.tgz#b613defe86e78168f750d1f1662d4ffb3cf002e6" - integrity sha512-WRu34A3Wwp6oafX8KWNAbedtDaaJO+nzfYQht7pcJKjyC2ggfPeF7SoP+eDo9wTn4/nQwEOscSR4hkJqTRlpXQ== +"@datadog/native-iast-rewriter@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.6.1.tgz#5e5393628c73c57dcf08256299c0e8cf71deb14f" + integrity sha512-zv7cr/MzHg560jhAnHcO7f9pLi4qaYrBEcB+Gla0xkVouYSDsp8cGXIGG4fiGdAMHdt7SpDNS6+NcEAqD/v8Ig== dependencies: lru-cache "^7.14.0" node-gyp-build "^4.5.0" @@ -428,10 +428,10 @@ dependencies: node-gyp-build "^3.9.0" -"@datadog/native-metrics@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.0.1.tgz#dc276c93785c0377a048e316f23b7c8ff3acfa84" - integrity sha512-0GuMyYyXf+Qpb/F+Fcekz58f2mO37lit9U3jMbWY/m8kac44gCPABzL5q3gWbdH+hWgqYfQoEYsdNDGSrKfwoQ== +"@datadog/native-metrics@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.1.0.tgz#c2378841accd9fdd6866d0e49bdf6e3d76e79f22" + integrity sha512-yOBi4x0OQRaGNPZ2bx9TGvDIgEdQ8fkudLTFAe7gEM1nAlvFmbE5YfpH8WenEtTSEBwojSau06m2q7axtEEmCg== dependencies: node-addon-api "^6.1.0" node-gyp-build "^3.9.0" @@ -589,11 +589,31 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" + integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + "@eslint/js@8.57.0": version "8.57.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@^9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.11.1.tgz#8bcb37436f9854b3d9a561440daf916acd940986" + integrity sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA== + "@graphql-tools/merge@^8.4.1": version "8.4.2" resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.4.2.tgz#95778bbe26b635e8d2f60ce9856b388f11fe8288" @@ -659,6 +679,11 @@ resolve-from "^3.0.0" rimraf "^3.0.0" +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" @@ -846,6 +871,14 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== +"@stylistic/eslint-plugin-js@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.8.0.tgz#f605202c75aa17692342662231f77d413d96d940" + integrity sha512-/e7pSzVMrwBd6yzSDsKHwax3TS96+pd/xSKzELaTkOuYqUhYfj/becWdfDbFSBGQD7BBBCiiE4L8L2cUfu5h+A== + dependencies: + eslint-visitor-keys "^4.0.0" + espree "^10.1.0" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -923,10 +956,10 @@ dependencies: undici-types "~5.26.4" -"@types/node@^16.18.103": - version "16.18.103" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.103.tgz#5557c7c32a766fddbec4b933b1d5c365f89b20a4" - integrity sha512-gOAcUSik1nR/CRC3BsK8kr6tbmNIOTpvb1sT+v5Nmmys+Ho8YtnIHP90wEsVK4hTcHndOqPVIlehEGEA5y31bA== +"@types/node@^16.0.0": + version "16.18.122" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.122.tgz#54948ddbe2ddef8144ee16b37f160e3f99c32397" + integrity sha512-rF6rUBS80n4oK16EW8nE75U+9fw0SSUgoPtWSvHhPXdT7itbvmS7UjB/jyM8i3AkvI6yeSM5qCwo+xN0npGDHg== "@types/prop-types@*": version "15.7.5" @@ -1007,7 +1040,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.12.0, acorn@^8.8.2, acorn@^8.9.0: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -1728,9 +1761,9 @@ cross-argv@^1.0.0: integrity sha512-uAVe/bgNHlPdP1VE4Sk08u9pAJ7o1x/tVQtX77T5zlhYhuwOWtVkPBEtHdvF5cq48VzeCG5i1zN4dQc8pwLYrw== cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82" + integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -2214,6 +2247,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.0.0, eslint-visitor-keys@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c" + integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg== + eslint@^8.57.0: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" @@ -2263,6 +2301,15 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== +espree@^10.0.1, espree@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.2.0.tgz#f4bcead9e05b0615c968e85f83816bc386a45df6" + integrity sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g== + dependencies: + acorn "^8.12.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.1.0" + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -2321,10 +2368,10 @@ events@1.1.1: resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" integrity "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" -express@^4.17.1, express@^4.18.2: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== +express@^4.17.1, express@^4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" @@ -2345,7 +2392,7 @@ express@^4.17.1, express@^4.18.2: methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.10" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" qs "6.13.0" range-parser "~1.2.1" @@ -2663,6 +2710,16 @@ globals@^13.19.0, globals@^13.24.0: dependencies: type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^15.10.0: + version "15.10.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.10.0.tgz#a7eab3886802da248ad8b6a9ccca6573ff899c9b" + integrity sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ== + globalthis@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" @@ -3566,7 +3623,7 @@ mocha@^9: log-symbols "4.1.0" minimatch "4.2.1" ms "2.1.3" - nanoid "3.3.1" + nanoid "3.3.8" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" @@ -3624,10 +3681,10 @@ multer@^1.4.5-lts.1: type-is "^1.6.4" xtend "^4.0.0" -nanoid@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@3.3.8: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0" @@ -3955,10 +4012,10 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.10, path-to-regexp@^0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.12, path-to-regexp@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-to-regexp@^6.2.1: version "6.3.0" @@ -4525,7 +4582,7 @@ source-map@^0.6.0, source-map@^0.6.1: source-map@^0.7.4: version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== spawn-wrap@^2.0.0: @@ -5195,6 +5252,11 @@ yaml@^1.10.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" + integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz"