diff --git a/.github/actions/plugins/test-and-upstream/action.yml b/.github/actions/plugins/test-and-upstream/action.yml new file mode 100644 index 00000000000..d847de98c0e --- /dev/null +++ b/.github/actions/plugins/test-and-upstream/action.yml @@ -0,0 +1,20 @@ +name: Plugin Tests +runs: + using: composite + steps: + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/oldest + - run: yarn test:plugins:ci + shell: bash + - run: yarn test:plugins:upstream + shell: bash + - uses: ./.github/actions/node/latest + - run: yarn test:plugins:ci + shell: bash + - run: yarn test:plugins:upstream + shell: bash + - uses: codecov/codecov-action@v3 + - if: always() + uses: ./.github/actions/testagent/logs diff --git a/.github/actions/plugins/test/action.yml b/.github/actions/plugins/test/action.yml new file mode 100644 index 00000000000..f39da26b682 --- /dev/null +++ b/.github/actions/plugins/test/action.yml @@ -0,0 +1,16 @@ +name: Plugin Tests +runs: + using: composite + steps: + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/oldest + - 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 diff --git a/.github/actions/plugins/upstream/action.yml b/.github/actions/plugins/upstream/action.yml new file mode 100644 index 00000000000..e1d74b574ee --- /dev/null +++ b/.github/actions/plugins/upstream/action.yml @@ -0,0 +1,16 @@ +name: Plugin Upstream Tests +runs: + using: composite + steps: + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - uses: ./.github/actions/node/oldest + - run: yarn test:plugins:upstream + shell: bash + - uses: ./.github/actions/node/latest + - run: yarn test:plugins:upstream + shell: bash + - uses: codecov/codecov-action@v3 + - if: always() + uses: ./.github/actions/testagent/logs diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index dca7fa63ae9..9a9617f50d7 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -112,18 +112,7 @@ jobs: DD_DATA_STREAMS_ENABLED: true steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream amqplib: runs-on: ubuntu-latest @@ -137,18 +126,7 @@ jobs: SERVICES: rabbitmq steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream apollo: runs-on: ubuntu-latest @@ -156,18 +134,7 @@ jobs: PLUGINS: apollo steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream aws-sdk: strategy: @@ -227,16 +194,7 @@ jobs: PLUGINS: axios steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/upstream bluebird: runs-on: ubuntu-latest @@ -244,16 +202,7 @@ jobs: PLUGINS: bluebird steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - uses: codecov/codecov-action@v3 - - if: always() - uses: ./.github/actions/testagent/logs + - uses: ./.github/actions/plugins/test bunyan: runs-on: ubuntu-latest @@ -261,18 +210,7 @@ jobs: PLUGINS: bunyan steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream cassandra: runs-on: ubuntu-latest @@ -286,16 +224,7 @@ jobs: SERVICES: cassandra steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test child_process: runs-on: ubuntu-latest @@ -343,18 +272,7 @@ jobs: PLUGINS: connect steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream cucumber: runs-on: ubuntu-latest @@ -362,16 +280,7 @@ jobs: PLUGINS: cucumber steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test # TODO: fix performance issues and test more Node versions cypress: @@ -436,16 +345,7 @@ jobs: PLUGINS: express|body-parser|cookie-parser steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test fastify: runs-on: ubuntu-latest @@ -453,16 +353,7 @@ jobs: PLUGINS: fastify steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test fetch: runs-on: ubuntu-latest @@ -470,16 +361,7 @@ jobs: PLUGINS: fetch steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test generic-pool: runs-on: ubuntu-latest @@ -487,16 +369,7 @@ jobs: PLUGINS: generic-pool steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test google-cloud-pubsub: runs-on: ubuntu-latest @@ -510,16 +383,7 @@ jobs: SERVICES: gpubsub steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test graphql: runs-on: ubuntu-latest @@ -527,18 +391,7 @@ jobs: PLUGINS: graphql steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream grpc: runs-on: ubuntu-latest @@ -546,16 +399,7 @@ jobs: PLUGINS: grpc steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test hapi: runs-on: ubuntu-latest @@ -563,16 +407,7 @@ jobs: PLUGINS: hapi steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test http: strategy: @@ -653,16 +488,7 @@ jobs: SERVICES: kafka steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test knex: runs-on: ubuntu-latest @@ -670,16 +496,7 @@ jobs: PLUGINS: knex steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test koa: runs-on: ubuntu-latest @@ -687,18 +504,7 @@ jobs: PLUGINS: koa steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream limitd-client: runs-on: ubuntu-latest @@ -716,16 +522,7 @@ jobs: SERVICES: limitd steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test memcached: runs-on: ubuntu-latest @@ -739,16 +536,7 @@ jobs: SERVICES: memcached steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test microgateway-core: runs-on: ubuntu-latest @@ -756,16 +544,7 @@ jobs: PLUGINS: microgateway-core steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test mocha: runs-on: ubuntu-latest @@ -773,16 +552,7 @@ jobs: PLUGINS: mocha steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - uses: codecov/codecov-action@v3 - - if: always() - uses: ./.github/actions/testagent/logs + - uses: ./.github/actions/plugins/test moleculer: runs-on: ubuntu-latest @@ -790,16 +560,7 @@ jobs: PLUGINS: moleculer steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test mongodb: runs-on: ubuntu-latest @@ -814,16 +575,7 @@ jobs: SERVICES: mongo steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test mongodb-core: runs-on: ubuntu-latest @@ -838,16 +590,7 @@ jobs: SERVICES: mongo steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test mongoose: runs-on: ubuntu-latest @@ -861,16 +604,7 @@ jobs: SERVICES: mongo steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test mysql: runs-on: ubuntu-latest @@ -887,16 +621,7 @@ jobs: SERVICES: mysql steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test net: runs-on: ubuntu-latest @@ -945,16 +670,7 @@ jobs: PLUGINS: openai steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test opensearch: runs-on: ubuntu-latest @@ -971,16 +687,7 @@ jobs: SERVICES: opensearch steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test # TODO: Install the Oracle client on the host and test Node >=16. # TODO: Figure out why nyc stopped working with EACCESS errors. @@ -1071,16 +778,7 @@ jobs: SERVICES: postgres steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test promise: runs-on: ubuntu-latest @@ -1088,18 +786,7 @@ jobs: PLUGINS: promise steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - run: yarn test:plugins:upstream - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test-and-upstream promise-js: runs-on: ubuntu-latest @@ -1107,16 +794,7 @@ jobs: PLUGINS: promise-js steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test q: runs-on: ubuntu-latest @@ -1124,16 +802,7 @@ jobs: PLUGINS: q steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test redis: runs-on: ubuntu-latest @@ -1147,16 +816,7 @@ jobs: SERVICES: redis steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test restify: runs-on: ubuntu-latest @@ -1164,16 +824,7 @@ jobs: PLUGINS: restify steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test router: runs-on: ubuntu-latest @@ -1181,16 +832,7 @@ jobs: PLUGINS: router steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test sharedb: runs-on: ubuntu-latest @@ -1239,16 +881,7 @@ jobs: PLUGINS: undici steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test when: runs-on: ubuntu-latest @@ -1256,16 +889,7 @@ jobs: PLUGINS: when steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 + - uses: ./.github/actions/plugins/test winston: runs-on: ubuntu-latest @@ -1273,12 +897,4 @@ jobs: PLUGINS: winston steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - uses: ./.github/actions/install - - uses: ./.github/actions/node/oldest - - run: yarn test:plugins:ci - - uses: ./.github/actions/node/latest - - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs + - uses: ./.github/actions/plugins/test diff --git a/.github/workflows/prepare-release-proposal.yml b/.github/workflows/prepare-release-proposal.yml new file mode 100644 index 00000000000..46e472e4e33 --- /dev/null +++ b/.github/workflows/prepare-release-proposal.yml @@ -0,0 +1,101 @@ +name: Prepare release proposal + +on: + workflow_dispatch: + +jobs: + create-proposal: + strategy: + matrix: + base-branch: + - v4.x + - v5.x + runs-on: ubuntu-latest + + permissions: write-all + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ matrix.base-branch }} + token: ${{ secrets.GH_ACCESS_TOKEN_RELEASE }} + + - name: Set up Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Pull master branch + run: | + git checkout master + git pull + cp -r scripts _scripts + git checkout ${{ matrix.base-branch }} + + + - name: Configure node + uses: actions/setup-node@v3 + + - name: Install dependencies + run: | + yarn + git checkout yarn.lock + + - name: Install branch-diff + run: | + npm i -g @bengl/branch-diff + + - name: Configure branch-diff + run: | + mkdir -p ~/.config/changelog-maker + echo "{\"token\":\"${{secrets.GITHUB_TOKEN}}\",\"user\":\"${{github.actor}}\"}" > ~/.config/changelog-maker/config.json + + - name: Commit branch diffs + id: commit_branch_diffs + run: | + node _scripts/prepare-release-proposal.js commit-branch-diffs ${{ matrix.base-branch }} > branch-diffs.txt + + - name: Calculate release type + id: calc-release-type + run: | + release_type=`grep -q "(SEMVER-MINOR)" branch-diffs.txt && echo "minor" || echo "patch"` + echo "release-type=$release_type" >> $GITHUB_OUTPUT + + - name: Create proposal branch + id: create_branch + run: | + branch_name=`node _scripts/prepare-release-proposal.js create-branch ${{ steps.calc-release-type.outputs.release-type }}` + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT + + - name: Push proposal branch + run: | + git push origin ${{steps.create_branch.outputs.branch_name}} + + - name: Update package.json + id: pkg + run: | + content=`node _scripts/prepare-release-proposal.js update-package-json ${{ steps.calc-release-type.outputs.release-type }}` + echo "version=$content" >> $GITHUB_OUTPUT + + - name: Create PR + run: | + gh pr create --draft --base ${{ matrix.base-branch }} --title "v${{ steps.pkg.outputs.version }}" -F branch-diffs.txt + rm branch-diffs.txt + env: + GH_TOKEN: ${{ github.token }} + + # Commit package.json and push to proposal branch after the PR is created to force CI execution + - name: Commit package.json + run: | + git add package.json + git commit -m "v${{ steps.pkg.outputs.version }}" + + - name: Push package.json update + run: | + git push origin ${{steps.create_branch.outputs.branch_name}} + + - name: Clean _scripts + run: | + rm -rf _scripts diff --git a/.github/workflows/rebase-release-proposal.yml b/.github/workflows/rebase-release-proposal.yml new file mode 100644 index 00000000000..01ae84222b3 --- /dev/null +++ b/.github/workflows/rebase-release-proposal.yml @@ -0,0 +1,86 @@ +name: Rebase release proposal + +on: + workflow_dispatch: + inputs: + base-branch: + description: 'Branch to rebase onto' + required: true + type: choice + options: + - v4.x + - v5.x + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Get PR details + id: get_pr + run: | + pr_number=$(gh pr list --head ${{ github.ref_name }} --json number --jq '.[0].number') + echo "PR_NUMBER=$pr_number" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ github.token }} + + - name: Check PR approval + id: check_approval + run: | + approvals=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER/reviews" \ + | jq '[.[] | select(.state == "APPROVED")] | length') + if [ "$approvals" -eq 0 ]; then + exit 1 + fi + + - name: Check CI status + id: check_ci_status + run: | + status=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/commits/${{ github.sha }}/status" \ + | jq -r '.state') + if [ "$status" != "success" ]; then + exit 1 + fi + + release: + needs: check + + runs-on: ubuntu-latest + + permissions: write-all + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_ACCESS_TOKEN_RELEASE }} + + - name: Set up Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Checkout base branch + run: | + git checkout ${{ github.event.inputs.base-branch }} + + - name: Rebase branch + run: | + git rebase ${{ github.ref_name }} + + - name: Push rebased branch + run: | + git push diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54dbd9e5dab..714eb493581 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,6 +19,7 @@ onboarding_tests_installer: parallel: matrix: - ONBOARDING_FILTER_WEBLOG: [test-app-nodejs,test-app-nodejs-container] + SCENARIO: [ INSTALLER_AUTO_INJECTION, SIMPLE_AUTO_INJECTION_PROFILING ] onboarding_tests_k8s_injection: variables: diff --git a/index.d.ts b/index.d.ts index 999921df993..6d2b495d5da 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1837,9 +1837,10 @@ declare namespace tracer { /** * Construct a new TracerProvider to register with @opentelemetry/api * + * @param config Configuration object for the TracerProvider * @returns TracerProvider A TracerProvider instance */ - new(): TracerProvider; + new(config?: Record): TracerProvider; /** * Returns a Tracer, creating one if one with the given name and version is diff --git a/integration-tests/ci-visibility/vitest-tests/coverage-test.mjs b/integration-tests/ci-visibility/vitest-tests/coverage-test.mjs new file mode 100644 index 00000000000..63ee3600ef9 --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/coverage-test.mjs @@ -0,0 +1,8 @@ +import { describe, test, expect } from 'vitest' +import { sum } from './sum' + +describe('code coverage', () => { + test('passes', () => { + expect(sum(1, 2)).to.equal(3) + }) +}) diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index ca338d12a39..6c836ee32d6 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -64,7 +64,7 @@ versions.forEach(version => { }) => { // TODO: add esm tests describe(`cucumber@${version} ${type}`, () => { - let sandbox, cwd, receiver, childProcess + let sandbox, cwd, receiver, childProcess, testOutput before(async function () { // add an explicit timeout to make tests less flaky @@ -87,6 +87,7 @@ versions.forEach(version => { }) afterEach(async () => { + testOutput = '' childProcess.kill() await receiver.stop() }) @@ -271,7 +272,6 @@ versions.forEach(version => { ) }) it('can report code coverage', (done) => { - let testOutput const libraryConfigRequestPromise = receiver.payloadReceived( ({ url }) => url.endsWith('/api/v2/libraries/tests/services/setting') ) @@ -1118,6 +1118,108 @@ versions.forEach(version => { }).catch(done) }) }) + + it('takes into account untested files if "all" is passed to nyc', (done) => { + const linesPctMatchRegex = /Lines\s*:\s*([\d.]+)%/ + let linesPctMatch + let linesPctFromNyc = 0 + let codeCoverageWithUntestedFiles = 0 + let codeCoverageWithoutUntestedFiles = 0 + + let eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + codeCoverageWithUntestedFiles = testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] + }) + + childProcess = exec( + './node_modules/nyc/bin/nyc.js --all -r=text-summary --nycrc-path ./my-nyc.config.js ' + + 'node ./node_modules/.bin/cucumber-js ci-visibility/features/*.feature', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NYC_INCLUDE: JSON.stringify( + [ + 'ci-visibility/features/**', + 'ci-visibility/features-esm/**' + ] + ) + }, + stdio: 'inherit' + } + ) + + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + linesPctMatch = testOutput.match(linesPctMatchRegex) + linesPctFromNyc = linesPctMatch ? Number(linesPctMatch[1]) : null + + assert.equal( + linesPctFromNyc, + codeCoverageWithUntestedFiles, + 'nyc --all output does not match the reported coverage' + ) + + // reset test output for next test session + testOutput = '' + // we run the same tests without the all flag + childProcess = exec( + './node_modules/nyc/bin/nyc.js -r=text-summary --nycrc-path ./my-nyc.config.js ' + + 'node ./node_modules/.bin/cucumber-js ci-visibility/features/*.feature', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NYC_INCLUDE: JSON.stringify( + [ + 'ci-visibility/features/**', + 'ci-visibility/features-esm/**' + ] + ) + }, + stdio: 'inherit' + } + ) + + eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + codeCoverageWithoutUntestedFiles = testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] + }) + + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + linesPctMatch = testOutput.match(linesPctMatchRegex) + linesPctFromNyc = linesPctMatch ? Number(linesPctMatch[1]) : null + + assert.equal( + linesPctFromNyc, + codeCoverageWithoutUntestedFiles, + 'nyc output does not match the reported coverage (no --all flag)' + ) + + eventsPromise.then(() => { + assert.isAbove(codeCoverageWithoutUntestedFiles, codeCoverageWithUntestedFiles) + done() + }).catch(done) + }) + }) + }) }) }) }) diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index f516d8b40d8..571179276e1 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -21,6 +21,11 @@ const { engines } = require('../package.json') const supportedRange = engines.node const currentVersionIsSupported = semver.satisfies(process.versions.node, supportedRange) +// These are on by default in release tests, so we'll turn them off for +// more fine-grained control of these variables in these tests. +delete process.env.DD_INJECTION_ENABLED +delete process.env.DD_INJECT_FORCE + function testInjectionScenarios (arg, filename, esmWorks = false) { if (!currentVersionIsSupported) return const doTest = (file, ...args) => testFile(file, ...args) diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index 1c22eafcb28..1b005b21eab 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -1634,4 +1634,90 @@ describe('mocha CommonJS', function () { }) }) }) + + it('takes into account untested files if "all" is passed to nyc', (done) => { + const linePctMatchRegex = /Lines\s*:\s*(\d+)%/ + let linePctMatch + let linesPctFromNyc = 0 + let codeCoverageWithUntestedFiles = 0 + let codeCoverageWithoutUntestedFiles = 0 + + let eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + codeCoverageWithUntestedFiles = testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] + }) + + childProcess = exec( + './node_modules/nyc/bin/nyc.js -r=text-summary --all --nycrc-path ./my-nyc.config.js ' + + 'node ./node_modules/mocha/bin/mocha.js ./ci-visibility/test/ci-visibility-test.js', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + stdio: 'inherit' + } + ) + + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + linePctMatch = testOutput.match(linePctMatchRegex) + linesPctFromNyc = linePctMatch ? Number(linePctMatch[1]) : null + + assert.equal( + linesPctFromNyc, + codeCoverageWithUntestedFiles, + 'nyc --all output does not match the reported coverage' + ) + + // reset test output for next test session + testOutput = '' + // we run the same tests without the all flag + childProcess = exec( + './node_modules/nyc/bin/nyc.js -r=text-summary --nycrc-path ./my-nyc.config.js ' + + 'node ./node_modules/mocha/bin/mocha.js ./ci-visibility/test/ci-visibility-test.js', + { + cwd, + env: getCiVisAgentlessConfig(receiver.port), + stdio: 'inherit' + } + ) + + eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + const testSession = events.find(event => event.type === 'test_session_end').content + codeCoverageWithoutUntestedFiles = testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] + }) + + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + linePctMatch = testOutput.match(linePctMatchRegex) + linesPctFromNyc = linePctMatch ? Number(linePctMatch[1]) : null + + assert.equal( + linesPctFromNyc, + codeCoverageWithoutUntestedFiles, + 'nyc output does not match the reported coverage (no --all flag)' + ) + + eventsPromise.then(() => { + assert.isAbove(codeCoverageWithoutUntestedFiles, codeCoverageWithUntestedFiles) + done() + }).catch(done) + }) + }) + }) }) diff --git a/integration-tests/my-nyc.config.js b/integration-tests/my-nyc.config.js new file mode 100644 index 00000000000..b0d1235ecd2 --- /dev/null +++ b/integration-tests/my-nyc.config.js @@ -0,0 +1,5 @@ +// non default name so that it only gets picked up intentionally +module.exports = { + exclude: ['node_modules/**'], + include: process.env.NYC_INCLUDE ? JSON.parse(process.env.NYC_INCLUDE) : ['ci-visibility/test/**'] +} diff --git a/integration-tests/opentelemetry.spec.js b/integration-tests/opentelemetry.spec.js index 23071caae22..73adf812360 100644 --- a/integration-tests/opentelemetry.spec.js +++ b/integration-tests/opentelemetry.spec.js @@ -5,6 +5,7 @@ const { fork } = require('child_process') const { join } = require('path') const { assert } = require('chai') const { satisfies } = require('semver') +const axios = require('axios') function check (agent, proc, timeout, onMessage = () => { }, isMetrics) { const messageReceiver = isMetrics @@ -56,7 +57,11 @@ describe('opentelemetry', () => { before(async () => { const dependencies = [ - '@opentelemetry/api@1.8.0' + '@opentelemetry/api@1.8.0', + '@opentelemetry/instrumentation', + '@opentelemetry/instrumentation-http', + '@opentelemetry/instrumentation-express', + 'express' ] if (satisfies(process.version.slice(1), '>=14')) { dependencies.push('@opentelemetry/sdk-node') @@ -371,6 +376,56 @@ describe('opentelemetry', () => { }) }) + it('should work with otel express & http auto instrumentation', async () => { + const SERVER_PORT = 6666 + proc = fork(join(cwd, 'opentelemetry/auto-instrumentation.js'), { + cwd, + env: { + DD_TRACE_AGENT_PORT: agent.port, + DD_TRACE_OTEL_ENABLED: 1, + SERVER_PORT, + DD_TRACE_DISABLED_INSTRUMENTATIONS: 'http,dns,express,net' + } + }) + await new Promise(resolve => setTimeout(resolve, 1000)) // Adjust the delay as necessary + await axios.get(`http://localhost:${SERVER_PORT}/first-endpoint`) + + return check(agent, proc, 10000, ({ payload }) => { + assert.strictEqual(payload.length, 2) + // combine the traces + const trace = payload.flat() + assert.strictEqual(trace.length, 9) + + // Should have expected span names and ordering + assert.isTrue(eachEqual(trace, [ + 'GET /second-endpoint', + 'middleware - query', + 'middleware - expressInit', + 'request handler - /second-endpoint', + 'GET /first-endpoint', + 'middleware - query', + 'middleware - expressInit', + 'request handler - /first-endpoint', + 'GET' + ], + (span) => span.name)) + + assert.isTrue(allEqual(trace, (span) => { + span.trace_id.toString() + })) + + const [get3, query2, init2, handler2, get1, query1, init1, handler1, get2] = trace + isChildOf(query1, get1) + isChildOf(init1, get1) + isChildOf(handler1, get1) + isChildOf(get2, get1) + isChildOf(get3, get2) + isChildOf(query2, get3) + isChildOf(init2, get3) + isChildOf(handler2, get3) + }) + }) + if (satisfies(process.version.slice(1), '>=14')) { it('should auto-instrument @opentelemetry/sdk-node', async () => { proc = fork(join(cwd, 'opentelemetry/env-var.js'), { @@ -392,3 +447,9 @@ describe('opentelemetry', () => { }) } }) + +function isChildOf (childSpan, parentSpan) { + assert.strictEqual(childSpan.trace_id.toString(), parentSpan.trace_id.toString()) + assert.notStrictEqual(childSpan.span_id.toString(), parentSpan.span_id.toString()) + assert.strictEqual(childSpan.parent_id.toString(), parentSpan.span_id.toString()) +} diff --git a/integration-tests/opentelemetry/auto-instrumentation.js b/integration-tests/opentelemetry/auto-instrumentation.js new file mode 100644 index 00000000000..8a1ba5c2c77 --- /dev/null +++ b/integration-tests/opentelemetry/auto-instrumentation.js @@ -0,0 +1,55 @@ +const tracer = require('dd-trace').init() +const { TracerProvider } = tracer +const provider = new TracerProvider() +provider.register() + +const { registerInstrumentations } = require('@opentelemetry/instrumentation') +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http') +const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express') + +registerInstrumentations({ + instrumentations: [ + new HttpInstrumentation({ + ignoreIncomingRequestHook (req) { + // Ignore spans from static assets. + return req.path === '/v0.4/traces' || req.path === '/v0.7/config' || + req.path === '/telemetry/proxy/api/v2/apmtelemetry' + }, + ignoreOutgoingRequestHook (req) { + // Ignore spans from static assets. + return req.path === '/v0.4/traces' || req.path === '/v0.7/config' || + req.path === '/telemetry/proxy/api/v2/apmtelemetry' + } + }), + new ExpressInstrumentation() + ], + tracerProvider: provider +}) + +const express = require('express') +const http = require('http') +const app = express() +const PORT = process.env.SERVER_PORT + +app.get('/second-endpoint', (req, res) => { + res.send('Response from second endpoint') + server.close(() => { + }) +}) + +app.get('/first-endpoint', async (req, res) => { + try { + const response = await new Promise((resolve, reject) => { + http.get(`http://localhost:${PORT}/second-endpoint`).on('finish', (response) => { + resolve(response) + }).on('error', (error) => { + reject(error) + }) + }) + res.send(`First endpoint received: ${response}`) + } catch (error) { + res.status(500).send(`Error occurred while making nested call ${error}`) + } +}) + +const server = app.listen(PORT, () => {}) diff --git a/integration-tests/package-guardrails.spec.js b/integration-tests/package-guardrails.spec.js index c16280acf6b..f560a4ab2a3 100644 --- a/integration-tests/package-guardrails.spec.js +++ b/integration-tests/package-guardrails.spec.js @@ -13,6 +13,11 @@ const DD_TRACE_DEBUG = 'true' const DD_INJECTION_ENABLED = 'tracing' const DD_LOG_LEVEL = 'error' +// These are on by default in release tests, so we'll turn them off for +// more fine-grained control of these variables in these tests. +delete process.env.DD_INJECTION_ENABLED +delete process.env.DD_INJECT_FORCE + describe('package guardrails', () => { useEnv({ NODE_OPTIONS }) const runTest = (...args) => diff --git a/integration-tests/vitest.config.mjs b/integration-tests/vitest.config.mjs index ae1d5aefaea..9a1572fb499 100644 --- a/integration-tests/vitest.config.mjs +++ b/integration-tests/vitest.config.mjs @@ -1,9 +1,19 @@ import { defineConfig } from 'vite' -export default defineConfig({ +const config = { test: { include: [ process.env.TEST_DIR || 'ci-visibility/vitest-tests/test-visibility*' ] } -}) +} + +if (process.env.COVERAGE_PROVIDER) { + config.test.coverage = { + provider: process.env.COVERAGE_PROVIDER || 'v8', + include: ['ci-visibility/vitest-tests/**'], + reporter: ['text-summary'] + } +} + +export default defineConfig(config) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index ffcd267b5fa..e4492b79fe1 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -13,18 +13,24 @@ const { TEST_STATUS, TEST_TYPE, TEST_IS_RETRY, - TEST_CODE_OWNERS + TEST_CODE_OWNERS, + TEST_CODE_COVERAGE_LINES_PCT } = require('../../packages/dd-trace/src/plugins/util/test') -// tested with 1.6.0 const versions = ['1.6.0', 'latest'] +const linePctMatchRegex = /Lines\s+:\s+([\d.]+)%/ + versions.forEach((version) => { describe(`vitest@${version}`, () => { - let sandbox, cwd, receiver, childProcess + let sandbox, cwd, receiver, childProcess, testOutput before(async function () { - sandbox = await createSandbox([`vitest@${version}`], true) + sandbox = await createSandbox([ + `vitest@${version}`, + `@vitest/coverage-istanbul@${version}`, + `@vitest/coverage-v8@${version}` + ], true) cwd = sandbox.folder }) @@ -37,6 +43,7 @@ versions.forEach((version) => { }) afterEach(async () => { + testOutput = '' childProcess.kill() await receiver.stop() }) @@ -233,5 +240,59 @@ versions.forEach((version) => { }).catch(done) }) }) + + // only works for >=2.0.0 + if (version === 'latest') { + const coverageProviders = ['v8', 'istanbul'] + + coverageProviders.forEach((coverageProvider) => { + it(`reports code coverage for ${coverageProvider} provider`, (done) => { + let codeCoverageExtracted + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const testSession = events.find(event => event.type === 'test_session_end').content + + codeCoverageExtracted = testSession.metrics[TEST_CODE_COVERAGE_LINES_PCT] + }) + + childProcess = exec( + './node_modules/.bin/vitest run --coverage', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + COVERAGE_PROVIDER: coverageProvider, + TEST_DIR: 'ci-visibility/vitest-tests/coverage-test*' + }, + stdio: 'inherit' + } + ) + + childProcess.stdout.on('data', (chunk) => { + testOutput += chunk.toString() + }) + childProcess.stderr.on('data', (chunk) => { + testOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + const linePctMatch = testOutput.match(linePctMatchRegex) + const linesPctFromNyc = linePctMatch ? Number(linePctMatch[1]) : null + + assert.equal( + linesPctFromNyc, + codeCoverageExtracted, + 'coverage reported by vitest does not match extracted coverage' + ) + done() + }).catch(done) + }) + }) + }) + } }) }) diff --git a/package.json b/package.json index 7ca35eaaab5..5b1232283f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dd-trace", - "version": "5.20.0", + "version": "5.21.0", "description": "Datadog APM tracing client for JavaScript", "main": "index.js", "typings": "index.d.ts", @@ -73,8 +73,8 @@ }, "dependencies": { "@datadog/native-appsec": "8.0.1", - "@datadog/native-iast-rewriter": "2.4.0", - "@datadog/native-iast-taint-tracking": "3.0.0", + "@datadog/native-iast-rewriter": "2.4.1", + "@datadog/native-iast-taint-tracking": "3.1.0", "@datadog/native-metrics": "^2.0.0", "@datadog/pprof": "5.3.0", "@datadog/sketches-js": "^2.1.0", @@ -103,7 +103,7 @@ "tlhunter-sorted-set": "^0.1.0" }, "devDependencies": { - "@types/node": ">=18", + "@types/node": "^16.18.103", "autocannon": "^4.5.2", "aws-sdk": "^2.1446.0", "axios": "^1.6.7", diff --git a/packages/datadog-instrumentations/src/body-parser.js b/packages/datadog-instrumentations/src/body-parser.js index 3e3d7503231..57d8e3c86c5 100644 --- a/packages/datadog-instrumentations/src/body-parser.js +++ b/packages/datadog-instrumentations/src/body-parser.js @@ -1,7 +1,7 @@ 'use strict' const shimmer = require('../../datadog-shimmer') -const { channel, addHook } = require('./helpers/instrument') +const { channel, addHook, AsyncResource } = require('./helpers/instrument') const bodyParserReadCh = channel('datadog:body-parser:read:finish') @@ -23,7 +23,19 @@ function publishRequestBodyAndNext (req, res, next) { addHook({ name: 'body-parser', file: 'lib/read.js', - versions: ['>=1.4.0'] + versions: ['>=1.4.0 <1.20.0'] +}, read => { + return shimmer.wrap(read, function (req, res, next) { + const nextResource = new AsyncResource('bound-anonymous-fn') + arguments[2] = nextResource.bind(publishRequestBodyAndNext(req, res, next)) + return read.apply(this, arguments) + }) +}) + +addHook({ + name: 'body-parser', + file: 'lib/read.js', + versions: ['>=1.20.0'] }, read => { return shimmer.wrap(read, function (req, res, next) { arguments[2] = publishRequestBodyAndNext(req, res, next) diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 3e713ad89bb..1ebaf425a1d 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -28,6 +28,8 @@ const workerReportTraceCh = channel('ci:cucumber:worker-report:trace') const itrSkippedSuitesCh = channel('ci:cucumber:itr:skipped-suites') +const getCodeCoverageCh = channel('ci:nyc:get-coverage') + const { getCoveredFilenamesFromCoverage, resetCoverage, @@ -356,10 +358,18 @@ function getWrappedStart (start, frameworkVersion, isParallel = false) { const success = await start.apply(this, arguments) + let untestedCoverage + if (getCodeCoverageCh.hasSubscribers) { + untestedCoverage = await getChannelPromise(getCodeCoverageCh) + } + let testCodeCoverageLinesTotal if (global.__coverage__) { try { + if (untestedCoverage) { + originalCoverageMap.merge(fromCoverageMapToCoverage(untestedCoverage)) + } testCodeCoverageLinesTotal = originalCoverageMap.getCoverageSummary().lines.pct } catch (e) { // ignore errors diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 94f3318fb62..284e4ed5950 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -72,7 +72,6 @@ module.exports = { 'microgateway-core': () => require('../microgateway-core'), mocha: () => require('../mocha'), 'mocha-each': () => require('../mocha'), - workerpool: () => require('../mocha'), moleculer: () => require('../moleculer'), mongodb: () => require('../mongodb'), 'mongodb-core': () => require('../mongodb-core'), @@ -89,6 +88,7 @@ module.exports = { 'node:http2': () => require('../http2'), 'node:https': () => require('../http'), 'node:net': () => require('../net'), + nyc: () => require('../nyc'), oracledb: () => require('../oracledb'), openai: () => require('../openai'), paperplane: () => require('../paperplane'), @@ -113,5 +113,6 @@ module.exports = { undici: () => require('../undici'), vitest: { esmFirst: true, fn: () => require('../vitest') }, when: () => require('../when'), - winston: () => require('../winston') + winston: () => require('../winston'), + workerpool: () => require('../mocha') } diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 8444dd93e4e..c593961be71 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -90,7 +90,14 @@ for (const packageName of names) { } if (matchesFile) { - const version = moduleVersion || getVersion(moduleBaseDir) + let version = moduleVersion + try { + version = version || getVersion(moduleBaseDir) + } catch (e) { + log.error(`Error getting version for "${name}": ${e.message}`) + log.error(e) + continue + } if (!Object.hasOwnProperty(namesAndSuccesses, name)) { namesAndSuccesses[`${name}@${version}`] = false } diff --git a/packages/datadog-instrumentations/src/mocha/main.js b/packages/datadog-instrumentations/src/mocha/main.js index cf79f95b98e..5a8a62b9aa9 100644 --- a/packages/datadog-instrumentations/src/mocha/main.js +++ b/packages/datadog-instrumentations/src/mocha/main.js @@ -47,6 +47,7 @@ const config = {} // We'll preserve the original coverage here const originalCoverageMap = createCoverageMap() +let untestedCoverage // test channels const testStartCh = channel('ci:mocha:test:start') @@ -66,6 +67,8 @@ const testSessionStartCh = channel('ci:mocha:session:start') const testSessionFinishCh = channel('ci:mocha:session:finish') const itrSkippedSuitesCh = channel('ci:mocha:itr:skipped-suites') +const getCodeCoverageCh = channel('ci:nyc:get-coverage') + function getFilteredSuites (originalSuites) { return originalSuites.reduce((acc, suite) => { const testPath = getTestSuitePath(suite.file, process.cwd()) @@ -131,6 +134,9 @@ function getOnEndHandler (isParallel) { let testCodeCoverageLinesTotal if (global.__coverage__) { try { + if (untestedCoverage) { + originalCoverageMap.merge(fromCoverageMapToCoverage(untestedCoverage)) + } testCodeCoverageLinesTotal = originalCoverageMap.getCoverageSummary().lines.pct } catch (e) { // ignore errors @@ -153,6 +159,83 @@ function getOnEndHandler (isParallel) { }) } +function getExecutionConfiguration (runner, onFinishRequest) { + const mochaRunAsyncResource = new AsyncResource('bound-anonymous-fn') + + const onReceivedSkippableSuites = ({ err, skippableSuites, itrCorrelationId: responseItrCorrelationId }) => { + if (err) { + suitesToSkip = [] + } else { + suitesToSkip = skippableSuites + itrCorrelationId = responseItrCorrelationId + } + // We remove the suites that we skip through ITR + const filteredSuites = getFilteredSuites(runner.suite.suites) + const { suitesToRun } = filteredSuites + + isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length + + log.debug( + () => `${suitesToRun.length} out of ${runner.suite.suites.length} suites are going to run.` + ) + + runner.suite.suites = suitesToRun + + skippedSuites = Array.from(filteredSuites.skippedSuites) + + onFinishRequest() + } + + const onReceivedKnownTests = ({ err, knownTests: receivedKnownTests }) => { + if (err) { + knownTests = [] + isEarlyFlakeDetectionEnabled = false + } else { + knownTests = receivedKnownTests + } + + if (isSuitesSkippingEnabled) { + skippableSuitesCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) + }) + } else { + onFinishRequest() + } + } + + const onReceivedConfiguration = ({ err, libraryConfig }) => { + if (err || !skippableSuitesCh.hasSubscribers || !knownTestsCh.hasSubscribers) { + return onFinishRequest() + } + + isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled + isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled + earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries + isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled + + config.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled + config.isSuitesSkippingEnabled = isSuitesSkippingEnabled + config.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries + config.isFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled + + if (isEarlyFlakeDetectionEnabled) { + knownTestsCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedKnownTests) + }) + } else if (isSuitesSkippingEnabled) { + skippableSuitesCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) + }) + } else { + onFinishRequest() + } + } + + libraryConfigurationCh.publish({ + onDone: mochaRunAsyncResource.bind(onReceivedConfiguration) + }) +} + // In this hook we delay the execution with options.delay to grab library configuration, // skippable and known tests. // It is called but skipped in parallel mode. @@ -161,7 +244,6 @@ addHook({ versions: ['>=5.2.0'], file: 'lib/mocha.js' }, (Mocha) => { - const mochaRunAsyncResource = new AsyncResource('bound-anonymous-fn') shimmer.wrap(Mocha.prototype, 'run', run => function () { // Workers do not need to request any data, just run the tests if (!testStartCh.hasSubscribers || process.env.MOCHA_WORKER_ID || this.options.parallel) { @@ -181,79 +263,17 @@ addHook({ } }) - const onReceivedSkippableSuites = ({ err, skippableSuites, itrCorrelationId: responseItrCorrelationId }) => { - if (err) { - suitesToSkip = [] - } else { - suitesToSkip = skippableSuites - itrCorrelationId = responseItrCorrelationId - } - // We remove the suites that we skip through ITR - const filteredSuites = getFilteredSuites(runner.suite.suites) - const { suitesToRun } = filteredSuites - - isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length - - log.debug( - () => `${suitesToRun.length} out of ${runner.suite.suites.length} suites are going to run.` - ) - - runner.suite.suites = suitesToRun - - skippedSuites = Array.from(filteredSuites.skippedSuites) - - global.run() - } - - const onReceivedKnownTests = ({ err, knownTests: receivedKnownTests }) => { - if (err) { - knownTests = [] - isEarlyFlakeDetectionEnabled = false - } else { - knownTests = receivedKnownTests - } - - if (isSuitesSkippingEnabled) { - skippableSuitesCh.publish({ - onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) + getExecutionConfiguration(runner, () => { + if (getCodeCoverageCh.hasSubscribers) { + getCodeCoverageCh.publish({ + onDone: (receivedCodeCoverage) => { + untestedCoverage = receivedCodeCoverage + global.run() + } }) } else { global.run() } - } - - const onReceivedConfiguration = ({ err, libraryConfig }) => { - if (err || !skippableSuitesCh.hasSubscribers || !knownTestsCh.hasSubscribers) { - return global.run() - } - - isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled - isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled - earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries - isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled - - config.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled - config.isSuitesSkippingEnabled = isSuitesSkippingEnabled - config.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries - config.isFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled - - if (isEarlyFlakeDetectionEnabled) { - knownTestsCh.publish({ - onDone: mochaRunAsyncResource.bind(onReceivedKnownTests) - }) - } else if (isSuitesSkippingEnabled) { - skippableSuitesCh.publish({ - onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites) - }) - } else { - global.run() - } - } - - mochaRunAsyncResource.runInAsyncScope(() => { - libraryConfigurationCh.publish({ - onDone: mochaRunAsyncResource.bind(onReceivedConfiguration) - }) }) return runner diff --git a/packages/datadog-instrumentations/src/nyc.js b/packages/datadog-instrumentations/src/nyc.js new file mode 100644 index 00000000000..34210a78f06 --- /dev/null +++ b/packages/datadog-instrumentations/src/nyc.js @@ -0,0 +1,23 @@ +const { addHook, channel } = require('./helpers/instrument') +const shimmer = require('../../datadog-shimmer') + +const codeCoverageWrapCh = channel('ci:nyc:wrap') + +addHook({ + name: 'nyc', + versions: ['>=17'] +}, (nycPackage) => { + shimmer.wrap(nycPackage.prototype, 'wrap', wrap => async function () { + // Only relevant if the config `all` is set to true + try { + if (JSON.parse(process.env.NYC_CONFIG).all) { + codeCoverageWrapCh.publish(this) + } + } catch (e) { + // ignore errors + } + + return wrap.apply(this, arguments) + }) + return nycPackage +}) diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index 3ebe2002ed1..7f14b5c28d2 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -121,6 +121,21 @@ function getSortWrapper (sort) { this.ctx.config.retry = NUM_FAILED_TEST_RETRIES } + let testCodeCoverageLinesTotal + + if (this.ctx.coverageProvider?.generateCoverage) { + shimmer.wrap(this.ctx.coverageProvider, 'generateCoverage', generateCoverage => async function () { + const totalCodeCoverage = await generateCoverage.apply(this, arguments) + + try { + testCodeCoverageLinesTotal = totalCodeCoverage.getCoverageSummary().lines.pct + } catch (e) { + // ignore errors + } + return totalCodeCoverage + }) + } + shimmer.wrap(this.ctx, 'exit', exit => async function () { let onFinish @@ -136,8 +151,9 @@ function getSortWrapper (sort) { sessionAsyncResource.runInAsyncScope(() => { testSessionFinishCh.publish({ status: getSessionStatus(this.state), - onFinish, - error + testCodeCoverageLinesTotal, + error, + onFinish }) }) diff --git a/packages/datadog-instrumentations/test/body-parser.spec.js b/packages/datadog-instrumentations/test/body-parser.spec.js index 724f584ac46..482ba5e772d 100644 --- a/packages/datadog-instrumentations/test/body-parser.spec.js +++ b/packages/datadog-instrumentations/test/body-parser.spec.js @@ -3,6 +3,7 @@ const dc = require('dc-polyfill') const axios = require('axios') const agent = require('../../dd-trace/test/plugins/agent') +const { storage } = require('../../datadog-core') withVersions('body-parser', 'body-parser', version => { describe('body parser instrumentation', () => { @@ -10,7 +11,7 @@ withVersions('body-parser', 'body-parser', version => { let port, server, middlewareProcessBodyStub before(() => { - return agent.load(['express', 'body-parser'], { client: false }) + return agent.load(['http', 'express', 'body-parser'], { client: false }) }) before((done) => { @@ -70,5 +71,27 @@ withVersions('body-parser', 'body-parser', version => { bodyParserReadCh.unsubscribe(blockRequest) }) + + it('should not lose the http async context', async () => { + let store + let payload + + function handler (data) { + store = storage.getStore() + payload = data + } + bodyParserReadCh.subscribe(handler) + + const res = await axios.post(`http://localhost:${port}/`, { key: 'value' }) + + expect(store).to.have.property('req', payload.req) + expect(store).to.have.property('res', payload.res) + expect(store).to.have.property('span') + + expect(middlewareProcessBodyStub).to.be.calledOnce + expect(res.data).to.be.equal('DONE') + + bodyParserReadCh.unsubscribe(handler) + }) }) }) diff --git a/packages/datadog-plugin-cucumber/src/index.js b/packages/datadog-plugin-cucumber/src/index.js index 2e77b59395b..98ed65cfbd4 100644 --- a/packages/datadog-plugin-cucumber/src/index.js +++ b/packages/datadog-plugin-cucumber/src/index.js @@ -37,7 +37,10 @@ const { TELEMETRY_ITR_FORCED_TO_RUN, TELEMETRY_CODE_COVERAGE_EMPTY, TELEMETRY_ITR_UNSKIPPABLE, - TELEMETRY_CODE_COVERAGE_NUM_FILES + TELEMETRY_CODE_COVERAGE_NUM_FILES, + TEST_IS_RUM_ACTIVE, + TEST_BROWSER_DRIVER, + TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/ci-visibility/telemetry') const id = require('../../dd-trace/src/id') @@ -107,6 +110,7 @@ class CucumberPlugin extends CiPlugin { this.testSessionSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') finishAllTraceSpans(this.testSessionSpan) + this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName }) this.libraryConfig = null this.tracer._exporter.flush() @@ -285,10 +289,16 @@ class CucumberPlugin extends CiPlugin { span.finish() if (!isStep) { + const spanTags = span.context()._tags this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', - { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] } + { + hasCodeOwners: !!spanTags[TEST_CODE_OWNERS], + isNew, + isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true', + browserDriver: spanTags[TEST_BROWSER_DRIVER] + } ) finishAllTraceSpans(span) // If it's a worker, flushing is cheap, as it's just sending data to the main process diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index a918344f6a9..09a75070961 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -44,7 +44,9 @@ const { TELEMETRY_ITR_UNSKIPPABLE, TELEMETRY_CODE_COVERAGE_NUM_FILES, incrementCountMetric, - distributionMetric + distributionMetric, + TELEMETRY_ITR_SKIPPED, + TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/ci-visibility/telemetry') const { @@ -179,7 +181,7 @@ class CypressPlugin { } = this.testEnvironmentMetadata this.repositoryRoot = repositoryRoot - this.isUnsupportedCIProvider = !ciProviderName + this.ciProviderName = ciProviderName this.codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot) this.testConfiguration = { @@ -321,7 +323,7 @@ class CypressPlugin { incrementCountMetric(name, { testLevel, testFramework: 'cypress', - isUnsupportedCIProvider: this.isUnsupportedCIProvider, + isUnsupportedCIProvider: !this.ciProviderName, ...tags }) } @@ -363,6 +365,7 @@ class CypressPlugin { const { skippableTests, correlationId } = skippableTestsResponse this.testsToSkip = skippableTests || [] this.itrCorrelationId = correlationId + incrementCountMetric(TELEMETRY_ITR_SKIPPED, { testLevel: 'test' }, this.testsToSkip.length) } } @@ -436,6 +439,9 @@ class CypressPlugin { this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') + incrementCountMetric(TELEMETRY_TEST_SESSION, { + provider: this.ciProviderName + }) finishAllTraceSpans(this.testSessionSpan) } @@ -668,8 +674,14 @@ class CypressPlugin { } // test spans are finished at after:spec } + this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', { + hasCodeOwners: !!this.activeTestSpan.context()._tags[TEST_CODE_OWNERS], + isNew, + isRum: isRUMActive, + browserDriver: 'cypress' + }) this.activeTestSpan = null - this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test') + return null }, 'dd:addTags': (tags) => { diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index b3c8386fbc4..7124f88b931 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -20,7 +20,9 @@ const { TEST_IS_RETRY, TEST_EARLY_FLAKE_ENABLED, TEST_EARLY_FLAKE_ABORT_REASON, - JEST_DISPLAY_NAME + JEST_DISPLAY_NAME, + TEST_IS_RUM_ACTIVE, + TEST_BROWSER_DRIVER } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const id = require('../../dd-trace/src/id') @@ -32,7 +34,8 @@ const { TELEMETRY_ITR_FORCED_TO_RUN, TELEMETRY_CODE_COVERAGE_EMPTY, TELEMETRY_ITR_UNSKIPPABLE, - TELEMETRY_CODE_COVERAGE_NUM_FILES + TELEMETRY_CODE_COVERAGE_NUM_FILES, + TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/ci-visibility/telemetry') const isJestWorker = !!process.env.JEST_WORKER_ID @@ -129,6 +132,8 @@ class JestPlugin extends CiPlugin { this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') finishAllTraceSpans(this.testSessionSpan) + this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName }) + this.tracer._exporter.flush(() => { if (onDone) { onDone() @@ -287,12 +292,20 @@ class JestPlugin extends CiPlugin { if (testStartLine) { span.setTag(TEST_SOURCE_START, testStartLine) } - span.finish() + + const spanTags = span.context()._tags this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', - { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] } + { + hasCodeOwners: !!spanTags[TEST_CODE_OWNERS], + isNew: spanTags[TEST_IS_NEW] === 'true', + isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true', + browserDriver: spanTags[TEST_BROWSER_DRIVER] + } ) + + span.finish() finishAllTraceSpans(span) }) diff --git a/packages/datadog-plugin-mocha/src/index.js b/packages/datadog-plugin-mocha/src/index.js index 133a63e5697..79b0d14c62f 100644 --- a/packages/datadog-plugin-mocha/src/index.js +++ b/packages/datadog-plugin-mocha/src/index.js @@ -27,7 +27,9 @@ const { TEST_SUITE_ID, TEST_COMMAND, TEST_SUITE, - MOCHA_IS_PARALLEL + MOCHA_IS_PARALLEL, + TEST_IS_RUM_ACTIVE, + TEST_BROWSER_DRIVER } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -38,7 +40,8 @@ const { TELEMETRY_ITR_FORCED_TO_RUN, TELEMETRY_CODE_COVERAGE_EMPTY, TELEMETRY_ITR_UNSKIPPABLE, - TELEMETRY_CODE_COVERAGE_NUM_FILES + TELEMETRY_CODE_COVERAGE_NUM_FILES, + TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/ci-visibility/telemetry') const id = require('../../dd-trace/src/id') const log = require('../../dd-trace/src/log') @@ -184,12 +187,20 @@ class MochaPlugin extends CiPlugin { if (hasBeenRetried) { span.setTag(TEST_IS_RETRY, 'true') } - span.finish() + + const spanTags = span.context()._tags this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', - { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] } + { + hasCodeOwners: !!spanTags[TEST_CODE_OWNERS], + isNew: spanTags[TEST_IS_NEW] === 'true', + isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true', + browserDriver: spanTags[TEST_BROWSER_DRIVER] + } ) + + span.finish() finishAllTraceSpans(span) } }) @@ -226,12 +237,19 @@ class MochaPlugin extends CiPlugin { span.setTag(TEST_IS_RETRY, 'true') } - span.finish() + const spanTags = span.context()._tags this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', - { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] } + { + hasCodeOwners: !!spanTags[TEST_CODE_OWNERS], + isNew: spanTags[TEST_IS_NEW] === 'true', + isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true', + browserDriver: spanTags[TEST_BROWSER_DRIVER] + } ) + + span.finish() finishAllTraceSpans(span) } }) @@ -289,6 +307,7 @@ class MochaPlugin extends CiPlugin { this.testSessionSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') finishAllTraceSpans(this.testSessionSpan) + this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName }) } this.libraryConfig = null this.tracer._exporter.flush() diff --git a/packages/datadog-plugin-nyc/src/index.js b/packages/datadog-plugin-nyc/src/index.js new file mode 100644 index 00000000000..c407b55221c --- /dev/null +++ b/packages/datadog-plugin-nyc/src/index.js @@ -0,0 +1,35 @@ +const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin') + +class NycPlugin extends CiPlugin { + static get id () { + return 'nyc' + } + + constructor (...args) { + super(...args) + + this.addSub('ci:nyc:wrap', (nyc) => { + if (nyc?.config?.all) { + this.nyc = nyc + } + }) + + this.addSub('ci:nyc:get-coverage', ({ onDone }) => { + if (this.nyc?.getCoverageMapFromAllCoverageFiles) { + this.nyc.getCoverageMapFromAllCoverageFiles() + .then((untestedCoverageMap) => { + this.nyc = null + onDone(untestedCoverageMap) + }).catch((e) => { + this.nyc = null + onDone() + }) + } else { + this.nyc = null + onDone() + } + }) + } +} + +module.exports = NycPlugin diff --git a/packages/datadog-plugin-playwright/src/index.js b/packages/datadog-plugin-playwright/src/index.js index 4ab109b1050..482bd6f10b9 100644 --- a/packages/datadog-plugin-playwright/src/index.js +++ b/packages/datadog-plugin-playwright/src/index.js @@ -14,7 +14,8 @@ const { TEST_CONFIGURATION_BROWSER_NAME, TEST_IS_NEW, TEST_IS_RETRY, - TEST_EARLY_FLAKE_ENABLED + TEST_EARLY_FLAKE_ENABLED, + TELEMETRY_TEST_SESSION } = require('../../dd-trace/src/plugins/util/test') const { RESOURCE_NAME } = require('../../../ext/tags') const { COMPONENT } = require('../../dd-trace/src/constants') @@ -59,6 +60,7 @@ class PlaywrightPlugin extends CiPlugin { this.testSessionSpan.finish() this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') finishAllTraceSpans(this.testSessionSpan) + this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName }) appClosingTelemetry() this.tracer._exporter.flush(onDone) this.numFailedTests = 0 @@ -160,8 +162,6 @@ class PlaywrightPlugin extends CiPlugin { stepSpan.finish(stepStartTime + stepDuration) }) - span.finish() - if (testStatus === 'fail') { this.numFailedTests++ } @@ -169,8 +169,13 @@ class PlaywrightPlugin extends CiPlugin { this.telemetry.ciVisEvent( TELEMETRY_EVENT_FINISHED, 'test', - { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] } + { + hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS], + isNew, + browserDriver: 'playwright' + } ) + span.finish() finishAllTraceSpans(span) }) diff --git a/packages/datadog-plugin-vitest/src/index.js b/packages/datadog-plugin-vitest/src/index.js index b8b7e29d15b..a93eeb1ea4d 100644 --- a/packages/datadog-plugin-vitest/src/index.js +++ b/packages/datadog-plugin-vitest/src/index.js @@ -7,9 +7,16 @@ const { getTestSuitePath, getTestSuiteCommonTags, TEST_SOURCE_FILE, - TEST_IS_RETRY + TEST_IS_RETRY, + TEST_CODE_COVERAGE_LINES_PCT, + TEST_CODE_OWNERS } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') +const { + TELEMETRY_EVENT_CREATED, + TELEMETRY_EVENT_FINISHED, + TELEMETRY_TEST_SESSION +} = require('../../dd-trace/src/ci-visibility/telemetry') // Milliseconds that we subtract from the error test duration // so that they do not overlap with the following test @@ -64,6 +71,9 @@ class VitestPlugin extends CiPlugin { const span = store?.span if (span) { + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', { + hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS] + }) span.setTag(TEST_STATUS, 'pass') span.finish(this.taskToFinishTime.get(task)) finishAllTraceSpans(span) @@ -75,6 +85,9 @@ class VitestPlugin extends CiPlugin { const span = store?.span if (span) { + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', { + hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS] + }) span.setTag(TEST_STATUS, 'fail') if (error) { @@ -91,7 +104,7 @@ class VitestPlugin extends CiPlugin { this.addSub('ci:vitest:test:skip', ({ testName, testSuiteAbsolutePath }) => { const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot) - this.startTestSpan( + const testSpan = this.startTestSpan( testName, testSuite, this.testSuiteSpan, @@ -99,7 +112,11 @@ class VitestPlugin extends CiPlugin { [TEST_SOURCE_FILE]: testSuite, [TEST_STATUS]: 'skip' } - ).finish() + ) + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', { + hasCodeowners: !!testSpan.context()._tags[TEST_CODE_OWNERS] + }) + testSpan.finish() }) this.addSub('ci:vitest:test-suite:start', ({ testSuiteAbsolutePath, frameworkVersion }) => { @@ -124,6 +141,7 @@ class VitestPlugin extends CiPlugin { ...testSuiteMetadata } }) + this.telemetry.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite') const store = storage.getStore() this.enter(testSuiteSpan, store) this.testSuiteSpan = testSuiteSpan @@ -137,6 +155,7 @@ class VitestPlugin extends CiPlugin { span.finish() finishAllTraceSpans(span) } + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite') // TODO: too frequent flush - find for method in worker to decrease frequency this.tracer._exporter.flush(onFinish) }) @@ -150,16 +169,23 @@ class VitestPlugin extends CiPlugin { } }) - this.addSub('ci:vitest:session:finish', ({ status, onFinish, error }) => { + this.addSub('ci:vitest:session:finish', ({ status, onFinish, error, testCodeCoverageLinesTotal }) => { this.testSessionSpan.setTag(TEST_STATUS, status) this.testModuleSpan.setTag(TEST_STATUS, status) if (error) { this.testModuleSpan.setTag('error', error) this.testSessionSpan.setTag('error', error) } + if (testCodeCoverageLinesTotal) { + this.testModuleSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal) + this.testSessionSpan.setTag(TEST_CODE_COVERAGE_LINES_PCT, testCodeCoverageLinesTotal) + } this.testModuleSpan.finish() + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module') this.testSessionSpan.finish() + this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session') finishAllTraceSpans(this.testSessionSpan) + this.telemetry.count(TELEMETRY_TEST_SESSION, { provider: this.ciProviderName }) this.tracer._exporter.flush(onFinish) }) } diff --git a/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js b/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js index 16f7337c7ae..e7dac1607c8 100644 --- a/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +++ b/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js @@ -1,5 +1,30 @@ const request = require('../../exporters/common/request') const id = require('../../id') +const log = require('../../log') + +const { + incrementCountMetric, + distributionMetric, + TELEMETRY_KNOWN_TESTS, + TELEMETRY_KNOWN_TESTS_MS, + TELEMETRY_KNOWN_TESTS_ERRORS, + TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS, + TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES +} = require('../../ci-visibility/telemetry') + +function getNumTests (knownTests) { + let totalNumTests = 0 + + for (const testModule of Object.values(knownTests)) { + for (const testSuite of Object.values(testModule)) { + for (const testList of Object.values(testSuite)) { + totalNumTests += testList.length + } + } + } + + return totalNumTests +} function getKnownTests ({ url, @@ -64,12 +89,26 @@ function getKnownTests ({ } }) - request(data, options, (err, res) => { + incrementCountMetric(TELEMETRY_KNOWN_TESTS) + + const startTime = Date.now() + + request(data, options, (err, res, statusCode) => { + distributionMetric(TELEMETRY_KNOWN_TESTS_MS, {}, Date.now() - startTime) if (err) { + incrementCountMetric(TELEMETRY_KNOWN_TESTS_ERRORS, { statusCode }) done(err) } else { try { const { data: { attributes: { tests: knownTests } } } = JSON.parse(res) + + const numTests = getNumTests(knownTests) + + incrementCountMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS, {}, numTests) + distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES, {}, res.length) + + log.debug(() => `Number of received known tests: ${numTests}`) + done(null, knownTests) } catch (err) { done(err) 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 52001672101..98eff61a6fd 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 @@ -12,8 +12,7 @@ const { TELEMETRY_ENDPOINT_PAYLOAD_BYTES, TELEMETRY_ENDPOINT_PAYLOAD_REQUESTS_MS, TELEMETRY_ENDPOINT_PAYLOAD_REQUESTS_ERRORS, - TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, - getErrorTypeFromStatusCode + TELEMETRY_ENDPOINT_PAYLOAD_DROPPED } = require('../../../ci-visibility/telemetry') class Writer extends BaseWriter { @@ -56,10 +55,9 @@ class Writer extends BaseWriter { Date.now() - startRequestTime ) if (err) { - const errorType = getErrorTypeFromStatusCode(statusCode) incrementCountMetric( TELEMETRY_ENDPOINT_PAYLOAD_REQUESTS_ERRORS, - { endpoint: 'code_coverage', errorType } + { endpoint: 'code_coverage', statusCode } ) incrementCountMetric( TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, 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 afbc670443e..3934ec0d5b2 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js @@ -12,8 +12,7 @@ const { TELEMETRY_ENDPOINT_PAYLOAD_BYTES, TELEMETRY_ENDPOINT_PAYLOAD_REQUESTS_MS, TELEMETRY_ENDPOINT_PAYLOAD_REQUESTS_ERRORS, - TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, - getErrorTypeFromStatusCode + TELEMETRY_ENDPOINT_PAYLOAD_DROPPED } = require('../../../ci-visibility/telemetry') class Writer extends BaseWriter { @@ -57,10 +56,9 @@ class Writer extends BaseWriter { Date.now() - startRequestTime ) if (err) { - const errorType = getErrorTypeFromStatusCode(statusCode) incrementCountMetric( TELEMETRY_ENDPOINT_PAYLOAD_REQUESTS_ERRORS, - { endpoint: 'test_cycle', errorType } + { endpoint: 'test_cycle', statusCode } ) incrementCountMetric( TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, diff --git a/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js b/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js index ebc2d24771d..1585a94166f 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +++ b/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js @@ -11,7 +11,8 @@ const { generatePackFilesForCommits, getCommitsRevList, isShallowRepository, - unshallowRepository + unshallowRepository, + isGitAvailable } = require('../../../plugins/util/git') const { @@ -24,8 +25,7 @@ const { TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES, TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_MS, TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_ERRORS, - TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_BYTES, - getErrorTypeFromStatusCode + TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_BYTES } = require('../../../ci-visibility/telemetry') const isValidSha1 = (sha) => /^[0-9a-f]{40}$/.test(sha) @@ -92,8 +92,7 @@ function getCommitsToUpload ({ url, repositoryUrl, latestCommits, isEvpProxy, ev request(localCommitData, options, (err, response, statusCode) => { distributionMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_MS, {}, Date.now() - startTime) if (err) { - const errorType = getErrorTypeFromStatusCode(statusCode) - incrementCountMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_ERRORS, { errorType }) + incrementCountMetric(TELEMETRY_GIT_REQUESTS_SEARCH_COMMITS_ERRORS, { statusCode }) const error = new Error(`Error fetching commits to exclude: ${err.message}`) return callback(error) } @@ -178,8 +177,7 @@ function uploadPackFile ({ url, isEvpProxy, evpProxyPrefix, packFileToUpload, re request(form, options, (err, _, statusCode) => { distributionMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_MS, {}, Date.now() - startTime) if (err) { - const errorType = getErrorTypeFromStatusCode(statusCode) - incrementCountMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_ERRORS, { errorType }) + incrementCountMetric(TELEMETRY_GIT_REQUESTS_OBJECT_PACKFILES_ERRORS, { statusCode }) const error = new Error(`Could not upload packfiles: status code ${statusCode}: ${err.message}`) return callback(error, uploadSize) } @@ -245,6 +243,9 @@ function generateAndUploadPackFiles ({ * This function uploads git metadata to CI Visibility's backend. */ function sendGitMetadata (url, { isEvpProxy, evpProxyPrefix }, configRepositoryUrl, callback) { + if (!isGitAvailable()) { + return callback(new Error('Git is not available')) + } let repositoryUrl = configRepositoryUrl if (!repositoryUrl) { repositoryUrl = getRepositoryUrl() diff --git a/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js b/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js index fd9f1662e6a..3ff8f3afde3 100644 --- a/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +++ b/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js @@ -8,8 +8,7 @@ const { TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS, - TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, - getErrorTypeFromStatusCode + TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES } = require('../../ci-visibility/telemetry') function getSkippableSuites ({ @@ -83,8 +82,7 @@ function getSkippableSuites ({ request(data, options, (err, res, statusCode) => { distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_MS, {}, Date.now() - startTime) if (err) { - const errorType = getErrorTypeFromStatusCode(statusCode) - incrementCountMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS, { errorType }) + incrementCountMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS, { statusCode }) done(err) } else { let skippableSuites = [] 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 6a0adb0aa03..9a32efad05e 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 @@ -7,8 +7,7 @@ const { TELEMETRY_GIT_REQUESTS_SETTINGS, TELEMETRY_GIT_REQUESTS_SETTINGS_MS, TELEMETRY_GIT_REQUESTS_SETTINGS_ERRORS, - TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE, - getErrorTypeFromStatusCode + TELEMETRY_GIT_REQUESTS_SETTINGS_RESPONSE } = require('../telemetry') const DEFAULT_EARLY_FLAKE_DETECTION_NUM_RETRIES = 2 @@ -81,8 +80,7 @@ function getLibraryConfiguration ({ request(data, options, (err, res, statusCode) => { distributionMetric(TELEMETRY_GIT_REQUESTS_SETTINGS_MS, {}, Date.now() - startTime) if (err) { - const errorType = getErrorTypeFromStatusCode(statusCode) - incrementCountMetric(TELEMETRY_GIT_REQUESTS_SETTINGS_ERRORS, { errorType }) + incrementCountMetric(TELEMETRY_GIT_REQUESTS_SETTINGS_ERRORS, { statusCode }) done(err) } else { try { diff --git a/packages/dd-trace/src/ci-visibility/telemetry.js b/packages/dd-trace/src/ci-visibility/telemetry.js index 1bc01c502c9..7b24bc02096 100644 --- a/packages/dd-trace/src/ci-visibility/telemetry.js +++ b/packages/dd-trace/src/ci-visibility/telemetry.js @@ -10,13 +10,24 @@ const formattedTags = { isCodeCoverageEnabled: 'coverage_enabled', isSuitesSkippingEnabled: 'itrskip_enabled', hasCodeOwners: 'has_code_owners', - isUnsupportedCIProvider: 'is_unsupported_ci' + isUnsupportedCIProvider: 'is_unsupported_ci', + isNew: 'is_new', + isRum: 'is_rum', + browserDriver: 'browser_driver' } // Transform tags dictionary to array of strings. // If tag value is true, then only tag key is added to the array. function formatMetricTags (tagsDictionary) { return Object.keys(tagsDictionary).reduce((acc, tagKey) => { + if (tagKey === 'statusCode') { + const statusCode = tagsDictionary[tagKey] + if (isStatusCode400(statusCode)) { + acc.push(`status_code:${statusCode}`) + } + acc.push(`error_type:${getErrorTypeFromStatusCode(statusCode)}`) + return acc + } const formattedTagKey = formattedTags[tagKey] || tagKey if (tagsDictionary[tagKey] === true) { acc.push(formattedTagKey) @@ -36,6 +47,7 @@ function distributionMetric (name, tags, measure) { } // CI Visibility telemetry events +const TELEMETRY_TEST_SESSION = 'test_session' const TELEMETRY_EVENT_CREATED = 'event_created' const TELEMETRY_EVENT_FINISHED = 'event_finished' const TELEMETRY_CODE_COVERAGE_STARTED = 'code_coverage_started' @@ -74,6 +86,16 @@ const TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS = 'itr_skippable_tests.request_errors const TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES = 'itr_skippable_tests.response_suites' const TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS = 'itr_skippable_tests.response_tests' const TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES = 'itr_skippable_tests.response_bytes' +// early flake detection +const TELEMETRY_KNOWN_TESTS = 'early_flake_detection.request' +const TELEMETRY_KNOWN_TESTS_MS = 'early_flake_detection.request_ms' +const TELEMETRY_KNOWN_TESTS_ERRORS = 'early_flake_detection.request_errors' +const TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS = 'early_flake_detection.response_tests' +const TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES = 'early_flake_detection.response_bytes' + +function isStatusCode400 (statusCode) { + return statusCode >= 400 && statusCode < 500 +} function getErrorTypeFromStatusCode (statusCode) { if (statusCode >= 400 && statusCode < 500) { @@ -88,6 +110,7 @@ function getErrorTypeFromStatusCode (statusCode) { module.exports = { incrementCountMetric, distributionMetric, + TELEMETRY_TEST_SESSION, TELEMETRY_EVENT_CREATED, TELEMETRY_EVENT_FINISHED, TELEMETRY_CODE_COVERAGE_STARTED, @@ -126,5 +149,9 @@ module.exports = { TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS, TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, - getErrorTypeFromStatusCode + TELEMETRY_KNOWN_TESTS, + TELEMETRY_KNOWN_TESTS_MS, + TELEMETRY_KNOWN_TESTS_ERRORS, + TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS, + TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES } diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 2736b7527b8..1c982ef9da5 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -183,7 +183,7 @@ function remapify (input, mappings) { return output } -function propagationStyle (key, option, defaultValue) { +function propagationStyle (key, option) { // Extract by key if in object-form value if (option !== null && typeof option === 'object' && !Array.isArray(option)) { option = option[key] @@ -206,8 +206,16 @@ function propagationStyle (key, option, defaultValue) { .filter(v => v !== '') .map(v => v.trim().toLowerCase()) } +} - return defaultValue +function reformatSpanSamplingRules (rules) { + if (!rules) return rules + return rules.map(rule => { + return remapify(rule, { + sample_rate: 'sampleRate', + max_per_second: 'maxPerSecond' + }) + }) } class Config { @@ -229,36 +237,11 @@ class Config { checkIfBothOtelAndDdEnvVarSet() - const DD_TRACE_MEMCACHED_COMMAND_ENABLED = coalesce( - process.env.DD_TRACE_MEMCACHED_COMMAND_ENABLED, - false - ) - - const DD_SERVICE_MAPPING = coalesce( - options.serviceMapping, - process.env.DD_SERVICE_MAPPING - ? fromEntries( - process.env.DD_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) - ) - : {} - ) - const DD_API_KEY = coalesce( process.env.DATADOG_API_KEY, process.env.DD_API_KEY ) - // TODO: Remove the experimental env vars as a major? - const DD_TRACE_B3_ENABLED = coalesce( - options.experimental && options.experimental.b3, - process.env.DD_TRACE_EXPERIMENTAL_B3_ENABLED, - false - ) - const defaultPropagationStyle = ['datadog', 'tracecontext'] - if (isTrue(DD_TRACE_B3_ENABLED)) { - defaultPropagationStyle.push('b3') - defaultPropagationStyle.push('b3 single header') - } if (process.env.DD_TRACE_PROPAGATION_STYLE && ( process.env.DD_TRACE_PROPAGATION_STYLE_INJECT || process.env.DD_TRACE_PROPAGATION_STYLE_EXTRACT @@ -272,21 +255,11 @@ class Config { const PROPAGATION_STYLE_INJECT = propagationStyle( 'inject', options.tracePropagationStyle, - defaultPropagationStyle - ) - const PROPAGATION_STYLE_EXTRACT = propagationStyle( - 'extract', - options.tracePropagationStyle, - defaultPropagationStyle + this._getDefaultPropagationStyle(options) ) validateOtelPropagators(PROPAGATION_STYLE_INJECT) - const DD_TRACE_PROPAGATION_EXTRACT_FIRST = coalesce( - process.env.DD_TRACE_PROPAGATION_EXTRACT_FIRST, - false - ) - if (typeof options.appsec === 'boolean') { options.appsec = { enabled: options.appsec @@ -295,33 +268,6 @@ class Config { options.appsec = {} } - const DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON = coalesce( - maybeFile(options.appsec.blockedTemplateGraphql), - maybeFile(process.env.DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON) - ) - const DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = coalesce( - options.appsec.eventTracking && options.appsec.eventTracking.mode, - process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING, - 'safe' - ).toLowerCase() - const DD_API_SECURITY_ENABLED = coalesce( - options.appsec?.apiSecurity?.enabled, - process.env.DD_API_SECURITY_ENABLED && isTrue(process.env.DD_API_SECURITY_ENABLED), - process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED && isTrue(process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED), - true - ) - const DD_API_SECURITY_REQUEST_SAMPLE_RATE = coalesce( - options.appsec?.apiSecurity?.requestSampling, - parseFloat(process.env.DD_API_SECURITY_REQUEST_SAMPLE_RATE), - 0.1 - ) - - // 0: disabled, 1: logging, 2: garbage collection + logging - const DD_TRACE_SPAN_LEAK_DEBUG = coalesce( - process.env.DD_TRACE_SPAN_LEAK_DEBUG, - 0 - ) - const DD_INSTRUMENTATION_INSTALL_ID = coalesce( process.env.DD_INSTRUMENTATION_INSTALL_ID, null @@ -335,51 +281,10 @@ class Config { null ) - const sampler = { - spanSamplingRules: coalesce( - options.spanSamplingRules, - safeJsonParse(maybeFile(process.env.DD_SPAN_SAMPLING_RULES_FILE)), - safeJsonParse(process.env.DD_SPAN_SAMPLING_RULES), - [] - ).map(rule => { - return remapify(rule, { - sample_rate: 'sampleRate', - max_per_second: 'maxPerSecond' - }) - }) - } - // TODO: refactor this.apiKey = DD_API_KEY - this.serviceMapping = DD_SERVICE_MAPPING - this.tracePropagationStyle = { - inject: PROPAGATION_STYLE_INJECT, - extract: PROPAGATION_STYLE_EXTRACT, - otelPropagators: process.env.DD_TRACE_PROPAGATION_STYLE || - process.env.DD_TRACE_PROPAGATION_STYLE_INJECT || - process.env.DD_TRACE_PROPAGATION_STYLE_EXTRACT - ? false - : !!process.env.OTEL_PROPAGATORS - } - this.tracePropagationExtractFirst = isTrue(DD_TRACE_PROPAGATION_EXTRACT_FIRST) - this.sampler = sampler - this.appsec = { - blockedTemplateGraphql: DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON, - eventTracking: { - enabled: ['extended', 'safe'].includes(DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING), - mode: DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING - }, - apiSecurity: { - enabled: DD_API_SECURITY_ENABLED, - // Coerce value between 0 and 1 - requestSampling: Math.min(1, Math.max(0, DD_API_SECURITY_REQUEST_SAMPLE_RATE)) - } - } - // Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent - this.memcachedCommandEnabled = isTrue(DD_TRACE_MEMCACHED_COMMAND_ENABLED) - this.isAzureFunction = getIsAzureFunction() - this.spanLeakDebug = Number(DD_TRACE_SPAN_LEAK_DEBUG) + // sent in telemetry event app-started this.installSignature = { id: DD_INSTRUMENTATION_INSTALL_ID, time: DD_INSTRUMENTATION_INSTALL_TIME, @@ -453,6 +358,21 @@ class Config { this._merge() } + _getDefaultPropagationStyle (options) { + // TODO: Remove the experimental env vars as a major? + const DD_TRACE_B3_ENABLED = coalesce( + options.experimental && options.experimental.b3, + process.env.DD_TRACE_EXPERIMENTAL_B3_ENABLED, + false + ) + const defaultPropagationStyle = ['datadog', 'tracecontext'] + if (isTrue(DD_TRACE_B3_ENABLED)) { + defaultPropagationStyle.push('b3') + defaultPropagationStyle.push('b3 single header') + } + return defaultPropagationStyle + } + _isInServerlessEnvironment () { const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined const isGCPFunction = getIsGCPFunction() @@ -478,9 +398,14 @@ 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.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.obfuscatorKeyRegex', defaultWafObfuscatorKeyRegex) this._setValue(defaults, 'appsec.obfuscatorValueRegex', defaultWafObfuscatorValueRegex) this._setValue(defaults, 'appsec.rasp.enabled', true) @@ -516,6 +441,7 @@ class Config { this._setValue(defaults, 'iast.redactionValuePattern', null) this._setValue(defaults, 'iast.requestSampling', 30) this._setValue(defaults, 'iast.telemetryVerbosity', 'INFORMATION') + this._setValue(defaults, 'isAzureFunction', false) this._setValue(defaults, 'isCiVisibility', false) this._setValue(defaults, 'isEarlyFlakeDetectionEnabled', false) this._setValue(defaults, 'isGCPFunction', false) @@ -524,6 +450,7 @@ class Config { this._setValue(defaults, 'isManualApiEnabled', false) this._setValue(defaults, 'logInjection', false) this._setValue(defaults, 'lookup', undefined) + this._setValue(defaults, 'memcachedCommandEnabled', false) this._setValue(defaults, 'openAiLogsEnabled', false) this._setValue(defaults, 'openaiSpanCharLimit', 128) this._setValue(defaults, 'peerServiceMapping', {}) @@ -544,11 +471,14 @@ class Config { this._setValue(defaults, 'sampleRate', undefined) this._setValue(defaults, 'sampler.rateLimit', undefined) this._setValue(defaults, 'sampler.rules', []) + this._setValue(defaults, 'sampler.spanSamplingRules', []) this._setValue(defaults, 'scope', undefined) this._setValue(defaults, 'service', service) + this._setValue(defaults, 'serviceMapping', {}) this._setValue(defaults, 'site', 'datadoghq.com') this._setValue(defaults, 'spanAttributeSchema', 'v0') this._setValue(defaults, 'spanComputePeerService', false) + this._setValue(defaults, 'spanLeakDebug', 0) this._setValue(defaults, 'spanRemoveIntegrationFromService', false) this._setValue(defaults, 'startupLogs', false) this._setValue(defaults, 'stats.enabled', false) @@ -562,6 +492,10 @@ class Config { this._setValue(defaults, 'telemetry.metrics', true) this._setValue(defaults, 'traceId128BitGenerationEnabled', true) this._setValue(defaults, 'traceId128BitLoggingEnabled', false) + this._setValue(defaults, 'tracePropagationExtractFirst', false) + this._setValue(defaults, 'tracePropagationStyle.inject', ['datadog', 'tracecontext']) + this._setValue(defaults, 'tracePropagationStyle.extract', ['datadog', 'tracecontext']) + this._setValue(defaults, 'tracePropagationStyle.otelPropagators', false) this._setValue(defaults, 'tracing', true) this._setValue(defaults, 'url', undefined) this._setValue(defaults, 'version', pkg.version) @@ -572,7 +506,11 @@ class Config { const { AWS_LAMBDA_FUNCTION_NAME, DD_AGENT_HOST, + DD_API_SECURITY_ENABLED, + DD_API_SECURITY_REQUEST_SAMPLE_RATE, + DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING, DD_APPSEC_ENABLED, + DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON, DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML, DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON, DD_APPSEC_MAX_STACK_TRACES, @@ -590,6 +528,7 @@ class Config { DD_DOGSTATSD_HOSTNAME, DD_DOGSTATSD_PORT, DD_ENV, + DD_EXPERIMENTAL_API_SECURITY_ENABLED, DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED, DD_EXPERIMENTAL_PROFILING_ENABLED, JEST_WORKER_ID, @@ -616,8 +555,11 @@ class Config { DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS, DD_RUNTIME_METRICS_ENABLED, DD_SERVICE, + DD_SERVICE_MAPPING, DD_SERVICE_NAME, DD_SITE, + DD_SPAN_SAMPLING_RULES, + DD_SPAN_SAMPLING_RULES_FILE, DD_TAGS, DD_TELEMETRY_DEBUG, DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED, @@ -637,9 +579,14 @@ class Config { DD_TRACE_GIT_METADATA_ENABLED, DD_TRACE_GLOBAL_TAGS, DD_TRACE_HEADER_TAGS, + DD_TRACE_MEMCACHED_COMMAND_ENABLED, DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP, DD_TRACE_PARTIAL_FLUSH_MIN_SPANS, DD_TRACE_PEER_SERVICE_MAPPING, + DD_TRACE_PROPAGATION_EXTRACT_FIRST, + DD_TRACE_PROPAGATION_STYLE, + DD_TRACE_PROPAGATION_STYLE_INJECT, + DD_TRACE_PROPAGATION_STYLE_EXTRACT, DD_TRACE_RATE_LIMIT, DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED, DD_TRACE_REPORT_HOSTNAME, @@ -647,17 +594,19 @@ class Config { DD_TRACE_SAMPLING_RULES, DD_TRACE_SCOPE, DD_TRACE_SPAN_ATTRIBUTE_SCHEMA, + DD_TRACE_SPAN_LEAK_DEBUG, DD_TRACE_STARTUP_LOGS, DD_TRACE_TAGS, DD_TRACE_TELEMETRY_ENABLED, DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, DD_TRACING_ENABLED, DD_VERSION, - OTEL_SERVICE_NAME, + OTEL_METRICS_EXPORTER, + OTEL_PROPAGATORS, OTEL_RESOURCE_ATTRIBUTES, + OTEL_SERVICE_NAME, OTEL_TRACES_SAMPLER, - OTEL_TRACES_SAMPLER_ARG, - OTEL_METRICS_EXPORTER + OTEL_TRACES_SAMPLER_ARG } = process.env const tags = {} @@ -669,11 +618,22 @@ class Config { tagger.add(tags, DD_TRACE_TAGS) tagger.add(tags, DD_TRACE_GLOBAL_TAGS) + this._setBoolean(env, 'appsec.apiSecurity.enabled', coalesce( + 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.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.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) @@ -721,8 +681,11 @@ class Config { } this._envUnprocessed['iast.requestSampling'] = DD_IAST_REQUEST_SAMPLING this._setString(env, 'iast.telemetryVerbosity', DD_IAST_TELEMETRY_VERBOSITY) + this._setBoolean(env, 'isAzureFunction', getIsAzureFunction()) this._setBoolean(env, 'isGCPFunction', getIsGCPFunction()) this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION) + // Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent + this._setBoolean(env, 'memcachedCommandEnabled', DD_TRACE_MEMCACHED_COMMAND_ENABLED) this._setBoolean(env, 'openAiLogsEnabled', DD_OPENAI_LOGS_ENABLED) this._setValue(env, 'openaiSpanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT)) this._envUnprocessed.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT @@ -762,6 +725,10 @@ class Config { : undefined this._setBoolean(env, 'runtimeMetrics', DD_RUNTIME_METRICS_ENABLED || otelSetRuntimeMetrics) + this._setArray(env, 'sampler.spanSamplingRules', reformatSpanSamplingRules(coalesce( + safeJsonParse(maybeFile(DD_SPAN_SAMPLING_RULES_FILE)), + safeJsonParse(DD_SPAN_SAMPLING_RULES) + ))) this._setUnit(env, 'sampleRate', DD_TRACE_SAMPLE_RATE || getFromOtelSamplerMap(OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG)) this._setValue(env, 'sampler.rateLimit', DD_TRACE_RATE_LIMIT) @@ -769,11 +736,18 @@ class Config { this._envUnprocessed['sampler.rules'] = DD_TRACE_SAMPLING_RULES this._setString(env, 'scope', DD_TRACE_SCOPE) this._setString(env, 'service', DD_SERVICE || DD_SERVICE_NAME || tags.service || OTEL_SERVICE_NAME) + if (DD_SERVICE_MAPPING) { + this._setValue(env, 'serviceMapping', fromEntries( + process.env.DD_SERVICE_MAPPING.split(',').map(x => x.trim().split(':')) + )) + } this._setString(env, 'site', DD_SITE) if (DD_TRACE_SPAN_ATTRIBUTE_SCHEMA) { this._setString(env, 'spanAttributeSchema', validateNamingVersion(DD_TRACE_SPAN_ATTRIBUTE_SCHEMA)) this._envUnprocessed.spanAttributeSchema = DD_TRACE_SPAN_ATTRIBUTE_SCHEMA } + // 0: disabled, 1: logging, 2: garbage collection + logging + this._setValue(env, 'spanLeakDebug', maybeInt(DD_TRACE_SPAN_LEAK_DEBUG)) this._setBoolean(env, 'spanRemoveIntegrationFromService', DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED) this._setBoolean(env, 'startupLogs', DD_TRACE_STARTUP_LOGS) this._setTags(env, 'tags', tags) @@ -797,6 +771,13 @@ class Config { this._setBoolean(env, 'telemetry.metrics', DD_TELEMETRY_METRICS_ENABLED) this._setBoolean(env, 'traceId128BitGenerationEnabled', DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED) this._setBoolean(env, 'traceId128BitLoggingEnabled', DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED) + this._setBoolean(env, 'tracePropagationExtractFirst', DD_TRACE_PROPAGATION_EXTRACT_FIRST) + this._setBoolean(env, 'tracePropagationStyle.otelPropagators', + DD_TRACE_PROPAGATION_STYLE || + DD_TRACE_PROPAGATION_STYLE_INJECT || + DD_TRACE_PROPAGATION_STYLE_EXTRACT + ? false + : !!OTEL_PROPAGATORS) this._setBoolean(env, 'tracing', DD_TRACING_ENABLED) this._setString(env, 'version', DD_VERSION || tags.version) } @@ -810,11 +791,20 @@ 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.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex) this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex) this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec.rasp?.enabled) @@ -880,11 +870,13 @@ class Config { } this._setBoolean(opts, 'reportHostname', options.reportHostname) this._setBoolean(opts, 'runtimeMetrics', options.runtimeMetrics) + this._setArray(opts, 'sampler.spanSamplingRules', reformatSpanSamplingRules(options.spanSamplingRules)) this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate)) const ingestion = options.ingestion || {} this._setValue(opts, 'sampler.rateLimit', coalesce(options.rateLimit, ingestion.rateLimit)) this._setSamplingRule(opts, 'sampler.rules', options.samplingRules) this._setString(opts, 'service', options.service || tags.service) + this._setValue(opts, 'serviceMapping', options.serviceMapping) this._setString(opts, 'site', options.site) if (options.spanAttributeSchema) { this._setString(opts, 'spanAttributeSchema', validateNamingVersion(options.spanAttributeSchema)) @@ -994,7 +986,8 @@ class Config { const calc = setHiddenProperty(this, '_calculated', {}) const { - DD_CIVISIBILITY_AGENTLESS_URL + DD_CIVISIBILITY_AGENTLESS_URL, + DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED } = process.env if (DD_CIVISIBILITY_AGENTLESS_URL) { @@ -1004,7 +997,7 @@ class Config { } if (this._isCiVisibility()) { this._setBoolean(calc, 'isEarlyFlakeDetectionEnabled', - coalesce(process.env.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED, true)) + coalesce(DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED, true)) this._setBoolean(calc, 'isIntelligentTestRunnerEnabled', isTrue(this._isCiVisibilityItrEnabled())) this._setBoolean(calc, 'isManualApiEnabled', this._isCiVisibilityManualApiEnabled()) } @@ -1013,6 +1006,19 @@ class Config { calc.isIntelligentTestRunnerEnabled && !isFalse(this._isCiVisibilityGitUploadEnabled())) this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService()) this._setBoolean(calc, 'stats.enabled', this._isTraceStatsComputationEnabled()) + const defaultPropagationStyle = this._getDefaultPropagationStyle(this._optionsArg) + this._setValue(calc, 'tracePropagationStyle.inject', propagationStyle( + 'inject', + this._optionsArg.tracePropagationStyle + )) + this._setValue(calc, 'tracePropagationStyle.extract', propagationStyle( + 'extract', + this._optionsArg.tracePropagationStyle + )) + if (defaultPropagationStyle.length > 2) { + calc['tracePropagationStyle.inject'] = calc['tracePropagationStyle.inject'] || defaultPropagationStyle + calc['tracePropagationStyle.extract'] = calc['tracePropagationStyle.extract'] || defaultPropagationStyle + } } _applyRemote (options) { diff --git a/packages/dd-trace/src/opentelemetry/context_manager.js b/packages/dd-trace/src/opentelemetry/context_manager.js index 03e9bf8f647..fba84eef9f4 100644 --- a/packages/dd-trace/src/opentelemetry/context_manager.js +++ b/packages/dd-trace/src/opentelemetry/context_manager.js @@ -2,61 +2,46 @@ const { AsyncLocalStorage } = require('async_hooks') const { trace, ROOT_CONTEXT } = require('@opentelemetry/api') +const DataDogSpanContext = require('../opentracing/span_context') const SpanContext = require('./span_context') const tracer = require('../../') -// Horrible hack to acquire the otherwise inaccessible SPAN_KEY so we can redirect it... -// This is used for getting the current span context in OpenTelemetry, but the SPAN_KEY value is -// not exposed as it's meant to be read-only from outside the module. We want to hijack this logic -// so we can instead get the span context from the datadog context manager instead. -let SPAN_KEY -trace.getSpan({ - getValue (key) { - SPAN_KEY = key - } -}) - -// Whenever a value is acquired from the context map we should mostly delegate to the real getter, -// but when accessing the current span we should hijack that access to instead provide a fake span -// which we can use to get an OTel span context wrapping the datadog active scope span context. -function wrappedGetValue (target) { - return (key) => { - if (key === SPAN_KEY) { - return { - spanContext () { - const activeSpan = tracer.scope().active() - const context = activeSpan && activeSpan.context() - return new SpanContext(context) - } - } - } - return target.getValue(key) - } -} - class ContextManager { constructor () { this._store = new AsyncLocalStorage() } active () { - const active = this._store.getStore() || ROOT_CONTEXT + const activeSpan = tracer.scope().active() + const store = this._store.getStore() + const context = (activeSpan && activeSpan.context()) || store || ROOT_CONTEXT - return new Proxy(active, { - get (target, key) { - return key === 'getValue' ? wrappedGetValue(target) : target[key] - } - }) + if (!(context instanceof DataDogSpanContext)) { + return context + } + + if (!context._otelSpanContext) { + const newSpanContext = new SpanContext(context) + context._otelSpanContext = newSpanContext + } + if (store && trace.getSpanContext(store) === context._otelSpanContext) { + return store + } + return trace.setSpanContext(store || ROOT_CONTEXT, context._otelSpanContext) } with (context, fn, thisArg, ...args) { const span = trace.getSpan(context) const ddScope = tracer.scope() - return ddScope.activate(span._ddSpan, () => { + const run = () => { const cb = thisArg == null ? fn : fn.bind(thisArg) return this._store.run(context, cb, ...args) - }) + } + if (span && span._ddSpan) { + return ddScope.activate(span._ddSpan, run) + } + return run() } bind (context, target) { @@ -66,9 +51,7 @@ class ContextManager { } } - // Not part of the spec but the Node.js API expects these enable () {} disable () {} } - module.exports = ContextManager diff --git a/packages/dd-trace/src/opentelemetry/span_context.js b/packages/dd-trace/src/opentelemetry/span_context.js index f070ba525c2..06c9b26f8a4 100644 --- a/packages/dd-trace/src/opentelemetry/span_context.js +++ b/packages/dd-trace/src/opentelemetry/span_context.js @@ -24,11 +24,11 @@ class SpanContext { } get traceId () { - return this._ddContext._traceId.toString(16) + return this._ddContext.toTraceId(true) } get spanId () { - return this._ddContext._spanId.toString(16) + return this._ddContext.toSpanId(true) } get traceFlags () { diff --git a/packages/dd-trace/src/opentelemetry/tracer.js b/packages/dd-trace/src/opentelemetry/tracer.js index 56fc935b21d..bb9b81e6ccd 100644 --- a/packages/dd-trace/src/opentelemetry/tracer.js +++ b/packages/dd-trace/src/opentelemetry/tracer.js @@ -7,6 +7,7 @@ const Sampler = require('./sampler') const Span = require('./span') const id = require('../id') const SpanContext = require('./span_context') +const TextMapPropagator = require('../opentracing/propagation/text_map') class Tracer { constructor (library, config, tracerProvider) { @@ -22,6 +23,24 @@ class Tracer { return this._tracerProvider.resource } + _createSpanContextFromParent (parentSpanContext) { + return new SpanContext({ + traceId: parentSpanContext._traceId, + spanId: id(), + parentId: parentSpanContext._spanId, + sampling: parentSpanContext._sampling, + baggageItems: Object.assign({}, parentSpanContext._baggageItems), + trace: parentSpanContext._trace, + tracestate: parentSpanContext._tracestate + }) + } + + // Extracted method to create span context for a new span + _createSpanContextForNewSpan (context) { + const { traceId, spanId, traceFlags, traceState } = context + return TextMapPropagator._convertOtelContextToDatadog(traceId, spanId, traceFlags, traceState) + } + startSpan (name, options = {}, context = api.context.active()) { // remove span from context in case a root span is requested via options if (options.root) { @@ -29,21 +48,11 @@ class Tracer { } const parentSpan = api.trace.getSpan(context) const parentSpanContext = parentSpan && parentSpan.spanContext() - let spanContext - // TODO: Need a way to get 128-bit trace IDs for the validity check API to work... - // if (parent && api.trace.isSpanContextValid(parent)) { - if (parentSpanContext && parentSpanContext.traceId) { - const parent = parentSpanContext._ddContext - spanContext = new SpanContext({ - traceId: parent._traceId, - spanId: id(), - parentId: parent._spanId, - sampling: parent._sampling, - baggageItems: Object.assign({}, parent._baggageItems), - trace: parent._trace, - tracestate: parent._tracestate - }) + if (parentSpanContext && api.trace.isSpanContextValid(parentSpanContext)) { + spanContext = parentSpanContext._ddContext + ? this._createSpanContextFromParent(parentSpanContext._ddContext) + : this._createSpanContextForNewSpan(parentSpanContext) } else { spanContext = new SpanContext() } diff --git a/packages/dd-trace/src/opentelemetry/tracer_provider.js b/packages/dd-trace/src/opentelemetry/tracer_provider.js index 1d4119cbba1..e015cfad7db 100644 --- a/packages/dd-trace/src/opentelemetry/tracer_provider.js +++ b/packages/dd-trace/src/opentelemetry/tracer_provider.js @@ -1,6 +1,7 @@ 'use strict' -const { trace, context } = require('@opentelemetry/api') +const { trace, context, propagation } = require('@opentelemetry/api') +const { W3CTraceContextPropagator } = require('@opentelemetry/core') const tracer = require('../../') @@ -52,6 +53,13 @@ class TracerProvider { if (!trace.setGlobalTracerProvider(this)) { trace.getTracerProvider().setDelegate(this) } + // The default propagator used is the W3C Trace Context propagator, users should be able to pass in others + // as needed + if (config.propagator) { + propagation.setGlobalPropagator(config.propagator) + } else { + propagation.setGlobalPropagator(new W3CTraceContextPropagator()) + } } forceFlush () { diff --git a/packages/dd-trace/src/opentracing/propagation/log.js b/packages/dd-trace/src/opentracing/propagation/log.js index 957bfc113d2..2203f253fb6 100644 --- a/packages/dd-trace/src/opentracing/propagation/log.js +++ b/packages/dd-trace/src/opentracing/propagation/log.js @@ -15,7 +15,7 @@ class LogPropagator { if (spanContext) { if (this._config.traceId128BitLoggingEnabled && spanContext._trace.tags['_dd.p.tid']) { - carrier.dd.trace_id = spanContext._trace.tags['_dd.p.tid'] + spanContext._traceId.toString(16) + carrier.dd.trace_id = spanContext.toTraceId(true) } else { carrier.dd.trace_id = spanContext.toTraceId() } diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index a183e977d7f..346e5e00911 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -3,6 +3,7 @@ const pick = require('../../../../datadog-core/src/utils/src/pick') const id = require('../../id') const DatadogSpanContext = require('../span_context') +const OtelSpanContext = require('../../opentelemetry/span_context') const log = require('../../log') const TraceState = require('./tracestate') const tags = require('../../../../../ext/tags') @@ -618,6 +619,65 @@ class TextMapPropagator { return spanContext._traceId.toString(16) } + + static _convertOtelContextToDatadog (traceId, spanId, traceFlag, ts, meta = {}) { + const origin = null + let samplingPriority = traceFlag + + ts = ts?.traceparent || null + + if (ts) { + // Use TraceState.fromString to parse the tracestate header + const traceState = TraceState.fromString(ts) + let ddTraceStateData = null + + // Extract Datadog specific trace state data + traceState.forVendor('dd', (state) => { + ddTraceStateData = state + return state // You might need to adjust this part based on actual logic needed + }) + + if (ddTraceStateData) { + // Assuming ddTraceStateData is now a Map or similar structure containing Datadog trace state data + // Extract values as needed, similar to the original logic + const samplingPriorityTs = ddTraceStateData.get('s') + const origin = ddTraceStateData.get('o') + // Convert Map to object for meta + const otherPropagatedTags = Object.fromEntries(ddTraceStateData.entries()) + + // Update meta and samplingPriority based on extracted values + Object.assign(meta, otherPropagatedTags) + samplingPriority = TextMapPropagator._getSamplingPriority(traceFlag, parseInt(samplingPriorityTs, 10), origin) + } else { + log.debug(`no dd list member in tracestate from incoming request: ${ts}`) + } + } + + const spanContext = new OtelSpanContext({ + traceId: id(traceId, 16), spanId: id(), tags: meta, parentId: id(spanId, 16) + }) + + spanContext._sampling = { priority: samplingPriority } + spanContext._trace = { origin } + return spanContext + } + + static _getSamplingPriority (traceparentSampled, tracestateSamplingPriority, origin = null) { + const fromRumWithoutPriority = !tracestateSamplingPriority && origin === 'rum' + + let samplingPriority + if (!fromRumWithoutPriority && traceparentSampled === 0 && + (!tracestateSamplingPriority || tracestateSamplingPriority >= 0)) { + samplingPriority = 0 + } else if (!fromRumWithoutPriority && traceparentSampled === 1 && + (!tracestateSamplingPriority || tracestateSamplingPriority < 0)) { + samplingPriority = 1 + } else { + samplingPriority = tracestateSamplingPriority + } + + return samplingPriority + } } module.exports = TextMapPropagator diff --git a/packages/dd-trace/src/opentracing/span_context.js b/packages/dd-trace/src/opentracing/span_context.js index 309fb8b2d80..207c97080bb 100644 --- a/packages/dd-trace/src/opentracing/span_context.js +++ b/packages/dd-trace/src/opentracing/span_context.js @@ -27,6 +27,7 @@ class DatadogSpanContext { finished: [], tags: {} } + this._otelSpanContext = undefined } toTraceId (get128bitId = false) { diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index e09af4bed82..8c8c15c8e55 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -145,7 +145,7 @@ module.exports = class CiPlugin extends Plugin { incrementCountMetric(name, { testLevel, testFramework, - isUnsupportedCIProvider: this.isUnsupportedCIProvider, + isUnsupportedCIProvider: !this.ciProviderName, ...tags }) }, @@ -179,7 +179,7 @@ module.exports = class CiPlugin extends Plugin { this.codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot) - this.isUnsupportedCIProvider = !ciProviderName + this.ciProviderName = ciProviderName this.testConfiguration = { repositoryUrl, diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index fd9288afcc4..06325724b71 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -69,6 +69,7 @@ module.exports = { get 'node:http2' () { return require('../../../datadog-plugin-http2/src') }, get 'node:https' () { return require('../../../datadog-plugin-http/src') }, get 'node:net' () { return require('../../../datadog-plugin-net/src') }, + get nyc () { return require('../../../datadog-plugin-nyc/src') }, get oracledb () { return require('../../../datadog-plugin-oracledb/src') }, get openai () { return require('../../../datadog-plugin-openai/src') }, get paperplane () { return require('../../../datadog-plugin-paperplane/src') }, diff --git a/packages/dd-trace/src/plugins/util/git.js b/packages/dd-trace/src/plugins/util/git.js index 3e2a3ef9726..06b9521817f 100644 --- a/packages/dd-trace/src/plugins/util/git.js +++ b/packages/dd-trace/src/plugins/util/git.js @@ -77,6 +77,18 @@ function isDirectory (path) { } } +function isGitAvailable () { + const isWindows = os.platform() === 'win32' + const command = isWindows ? 'where' : 'which' + try { + cp.execFileSync(command, ['git'], { stdio: 'pipe' }) + return true + } catch (e) { + incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'check_git', exitCode: 'missing' }) + return false + } +} + function isShallowRepository () { return sanitizedExec( 'git', @@ -342,5 +354,6 @@ module.exports = { getCommitsRevList, GIT_REV_LIST_MAX_BUFFER, isShallowRepository, - unshallowRepository + unshallowRepository, + isGitAvailable } diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index dea883ffb12..6bd8ffd1b87 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -317,7 +317,7 @@ function updateConfig (changes, config) { 'sampler.rules': 'DD_TRACE_SAMPLING_RULES' } - const namesNeedFormatting = new Set(['DD_TAGS', 'peerServiceMapping']) + const namesNeedFormatting = new Set(['DD_TAGS', 'peerServiceMapping', 'serviceMapping']) const configuration = [] const names = [] // list of config names whose values have been changed diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/resources/propagationFunctions.js b/packages/dd-trace/test/appsec/iast/taint-tracking/resources/propagationFunctions.js index f0c10451ba1..4028f265b3e 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/resources/propagationFunctions.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/resources/propagationFunctions.js @@ -6,6 +6,12 @@ function insertStr (str) { return `pre_${str}_suf` } +function templateLiteralEndingWithNumberParams (str) { + const num1 = 1 + const num2 = 2 + return `${str}Literal${num1}${num2}` +} + function appendStr (str) { let pre = 'pre_' pre += str @@ -101,6 +107,7 @@ module.exports = { sliceStr, substrStr, substringStr, + templateLiteralEndingWithNumberParams, toLowerCaseStr, toUpperCaseStr, trimEndStr, diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-impl.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-impl.spec.js index 0087af4da85..e0eb9fc580a 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-impl.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/taint-tracking-impl.spec.js @@ -25,6 +25,7 @@ const propagationFns = [ 'sliceStr', 'substrStr', 'substringStr', + 'templateLiteralEndingWithNumberParams', 'toLowerCaseStr', 'toUpperCaseStr', 'trimEndStr', @@ -135,7 +136,8 @@ describe('TaintTracking', () => { 'arrayProtoJoin', 'concatSuffix', 'concatTaintedStr', - 'insertStr' + 'insertStr', + 'templateLiteralEndingWithNumberParams' ] propagationFns.forEach((propFn) => { if (filtered.includes(propFn)) return diff --git a/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js b/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js index 497fc51372e..b4d0acb747d 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/git/git_metadata.spec.js @@ -325,17 +325,20 @@ describe('git_metadata', () => { }) it('should not crash if git is missing', (done) => { + const oldPath = process.env.PATH + // git will not be found + process.env.PATH = '' + const scope = nock('https://api.test.com') .post('/api/v2/git/repository/search_commits') .reply(200, JSON.stringify({ data: [] })) .post('/api/v2/git/repository/packfile') .reply(204) - getRepositoryUrlStub.returns('') - gitMetadata.sendGitMetadata(new URL('https://api.test.com'), { isEvpProxy: false }, '', (err) => { - expect(err.message).to.contain('Repository URL is empty') + expect(err.message).to.contain('Git is not available') expect(scope.isDone()).to.be.false + process.env.PATH = oldPath done() }) }) diff --git a/packages/dd-trace/test/opentelemetry/context_manager.spec.js b/packages/dd-trace/test/opentelemetry/context_manager.spec.js new file mode 100644 index 00000000000..ebf8f122d87 --- /dev/null +++ b/packages/dd-trace/test/opentelemetry/context_manager.spec.js @@ -0,0 +1,117 @@ +'use strict' + +require('../setup/tap') + +const { expect } = require('chai') +const ContextManager = require('../../src/opentelemetry/context_manager') +const { ROOT_CONTEXT } = require('@opentelemetry/api') +const api = require('@opentelemetry/api') + +describe('OTel Context Manager', () => { + let contextManager + let db + + beforeEach(() => { + contextManager = new ContextManager() + api.context.setGlobalContextManager(contextManager) + db = { + getSomeValue: async () => { + await new Promise(resolve => setTimeout(resolve, 100)) + return { name: 'Dummy Name' } + } + } + }) + + it('should create a new context', () => { + const key1 = api.createContextKey('My first key') + const key2 = api.createContextKey('My second key') + expect(key1.description).to.equal('My first key') + expect(key2.description).to.equal('My second key') + }) + + it('should delete a context', () => { + const key = api.createContextKey('some key') + const ctx = api.ROOT_CONTEXT + const ctx2 = ctx.setValue(key, 'context 2') + + // remove the entry + const ctx3 = ctx.deleteValue(key) + + expect(ctx3.getValue(key)).to.equal(undefined) + expect(ctx2.getValue(key)).to.equal('context 2') + expect(ctx.getValue(key)).to.equal(undefined) + }) + + it('should create a new root context', () => { + const key = api.createContextKey('some key') + const ctx = api.ROOT_CONTEXT + const ctx2 = ctx.setValue(key, 'context 2') + expect(ctx2.getValue(key)).to.equal('context 2') + expect(ctx.getValue(key)).to.equal(undefined) + }) + + it('should return root context', () => { + const ctx = api.context.active() + expect(ctx).to.be.an.instanceof(ROOT_CONTEXT.constructor) + }) + + it('should set active context', () => { + const key = api.createContextKey('Key to store a value') + const ctx = api.context.active() + + api.context.with(ctx.setValue(key, 'context 2'), async () => { + expect(api.context.active().getValue(key)).to.equal('context 2') + }) + }) + + it('should set active context on an asynchronous callback and return the result synchronously', async () => { + const name = await api.context.with(api.context.active(), async () => { + const row = await db.getSomeValue() + return row.name + }) + + expect(name).to.equal('Dummy Name') + }) + + it('should set active contexts in nested functions', async () => { + const key = api.createContextKey('Key to store a value') + const ctx = api.context.active() + expect(api.context.active().getValue(key)).to.equal(undefined) + api.context.with(ctx.setValue(key, 'context 2'), () => { + expect(api.context.active().getValue(key)).to.equal('context 2') + api.context.with(ctx.setValue(key, 'context 3'), () => { + expect(api.context.active().getValue(key)).to.equal('context 3') + }) + expect(api.context.active().getValue(key)).to.equal('context 2') + }) + expect(api.context.active().getValue(key)).to.equal(undefined) + }) + + it('should not modify contexts, instead it should create new context objects', async () => { + const key = api.createContextKey('Key to store a value') + + const ctx = api.context.active() + + const ctx2 = ctx.setValue(key, 'context 2') + expect(ctx.getValue(key)).to.equal(undefined) + expect(ctx).to.be.an.instanceof(ROOT_CONTEXT.constructor) + expect(ctx2.getValue(key)).to.equal('context 2') + + const ret = api.context.with(ctx2, () => { + const ctx3 = api.context.active().setValue(key, 'context 3') + + expect(api.context.active().getValue(key)).to.equal('context 2') + expect(ctx.getValue(key)).to.equal(undefined) + expect(ctx2.getValue(key)).to.equal('context 2') + expect(ctx3.getValue(key)).to.equal('context 3') + + api.context.with(ctx3, () => { + expect(api.context.active().getValue(key)).to.equal('context 3') + }) + expect(api.context.active().getValue(key)).to.equal('context 2') + + return 'return value' + }) + expect(ret).to.equal('return value') + }) +}) diff --git a/packages/dd-trace/test/opentelemetry/span.spec.js b/packages/dd-trace/test/opentelemetry/span.spec.js index 91c02263076..578d92a6224 100644 --- a/packages/dd-trace/test/opentelemetry/span.spec.js +++ b/packages/dd-trace/test/opentelemetry/span.spec.js @@ -46,7 +46,6 @@ describe('OTel Span', () => { it('should expose parent span id', () => { tracer.trace('outer', (outer) => { const span = makeSpan('name', {}) - expect(span.parentSpanId).to.equal(outer.context()._spanId.toString(16)) }) }) diff --git a/packages/dd-trace/test/opentelemetry/span_context.spec.js b/packages/dd-trace/test/opentelemetry/span_context.spec.js index 2611e4baece..0be1fd7a3dd 100644 --- a/packages/dd-trace/test/opentelemetry/span_context.spec.js +++ b/packages/dd-trace/test/opentelemetry/span_context.spec.js @@ -42,7 +42,9 @@ describe('OTel Span Context', () => { const context = new SpanContext({ traceId }) - expect(context.traceId).to.equal(traceId.toString(16)) + // normalize to 128 bit since that is what otel expects + const normalizedTraceId = traceId.toString(16).padStart(32, '0') + expect(context.traceId).to.equal(normalizedTraceId) }) it('should get span id as hex', () => { diff --git a/packages/dd-trace/test/opentelemetry/tracer.spec.js b/packages/dd-trace/test/opentelemetry/tracer.spec.js index e74ddee72ba..169a3d20ed7 100644 --- a/packages/dd-trace/test/opentelemetry/tracer.spec.js +++ b/packages/dd-trace/test/opentelemetry/tracer.spec.js @@ -183,4 +183,63 @@ describe('OTel Tracer', () => { expect(childContext._parentId).to.not.eql(parentContext._spanId) }) }) + + it('test otel context span parenting', () => { + const tracerProvider = new TracerProvider() + tracerProvider.register() + const otelTracer = new Tracer({}, {}, tracerProvider) + otelTracer.startActiveSpan('otel-root', async (root) => { + await new Promise(resolve => setTimeout(resolve, 200)) + otelTracer.startActiveSpan('otel-parent1', async (parent1) => { + isChildOf(parent1._ddSpan, root._ddSpan) + await new Promise(resolve => setTimeout(resolve, 400)) + otelTracer.startActiveSpan('otel-child1', async (child) => { + isChildOf(child._ddSpan, parent1._ddSpan) + await new Promise(resolve => setTimeout(resolve, 600)) + }) + }) + const orphan1 = otelTracer.startSpan('orphan1') + isChildOf(orphan1._ddSpan, root._ddSpan) + const ctx = api.trace.setSpan(api.context.active(), root) + + otelTracer.startActiveSpan('otel-parent2', ctx, async (parent2) => { + isChildOf(parent2._ddSpan, root._ddSpan) + await new Promise(resolve => setTimeout(resolve, 400)) + const ctx = api.trace.setSpan(api.context.active(), root) + otelTracer.startActiveSpan('otel-child2', ctx, async (child) => { + isChildOf(child._ddSpan, parent2._ddSpan) + await new Promise(resolve => setTimeout(resolve, 600)) + }) + }) + orphan1.end() + }) + }) + + it('test otel context mixed span parenting', () => { + const tracerProvider = new TracerProvider() + tracerProvider.register() + const otelTracer = new Tracer({}, {}, tracerProvider) + otelTracer.startActiveSpan('otel-top-level', async (root) => { + tracer.trace('ddtrace-top-level', async (ddSpan) => { + isChildOf(ddSpan, root._ddSpan) + await new Promise(resolve => setTimeout(resolve, 200)) + tracer.trace('ddtrace-child', async (ddSpanChild) => { + isChildOf(ddSpanChild, ddSpan) + await new Promise(resolve => setTimeout(resolve, 400)) + }) + + otelTracer.startActiveSpan('otel-child', async (otelSpan) => { + isChildOf(otelSpan._ddSpan, ddSpan) + await new Promise(resolve => setTimeout(resolve, 200)) + tracer.trace('ddtrace-grandchild', async (ddSpanGrandchild) => { + isChildOf(ddSpanGrandchild, otelSpan._ddSpan) + otelTracer.startActiveSpan('otel-grandchild', async (otelGrandchild) => { + isChildOf(otelGrandchild._ddSpan, ddSpanGrandchild) + await new Promise(resolve => setTimeout(resolve, 200)) + }) + }) + }) + }) + }) + }) }) diff --git a/packages/dd-trace/test/opentracing/propagation/log.spec.js b/packages/dd-trace/test/opentracing/propagation/log.spec.js index 95ff1ce82ac..50c815f1b7c 100644 --- a/packages/dd-trace/test/opentracing/propagation/log.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/log.spec.js @@ -76,6 +76,25 @@ describe('LogPropagator', () => { expect(carrier.dd).to.have.property('span_id', '456') }) + it('should correctly inject 128 bit trace ids when _dd.p.tid is present', () => { + config.traceId128BitLoggingEnabled = true + const carrier = {} + const traceId = id('4e2a9c1573d240b1a3b7e3c1d4c2f9a7', 16) + const traceIdTag = '8765432187654321' + const spanContext = new SpanContext({ + traceId, + spanId: id('456', 10) + }) + + spanContext._trace.tags['_dd.p.tid'] = traceIdTag + + propagator.inject(spanContext, carrier) + + expect(carrier).to.have.property('dd') + expect(carrier.dd).to.have.property('trace_id', '4e2a9c1573d240b1a3b7e3c1d4c2f9a7') + expect(carrier.dd).to.have.property('span_id', '456') + }) + it('should not inject 128-bit trace IDs when disabled', () => { const carrier = {} const traceId = id('123', 10) diff --git a/packages/dd-trace/test/opentracing/span_context.spec.js b/packages/dd-trace/test/opentracing/span_context.spec.js index a5013ef575b..cfa184d433b 100644 --- a/packages/dd-trace/test/opentracing/span_context.spec.js +++ b/packages/dd-trace/test/opentracing/span_context.spec.js @@ -56,7 +56,8 @@ describe('SpanContext', () => { tags: { foo: 'bar' } }, _traceparent: '00-1111aaaa2222bbbb3333cccc4444dddd-5555eeee6666ffff-01', - _tracestate: TraceState.fromString('dd=s:-1;o:foo;t.dm:-4;t.usr.id:bar') + _tracestate: TraceState.fromString('dd=s:-1;o:foo;t.dm:-4;t.usr.id:bar'), + _otelSpanContext: undefined }) }) @@ -84,7 +85,8 @@ describe('SpanContext', () => { tags: {} }, _traceparent: undefined, - _tracestate: undefined + _tracestate: undefined, + _otelSpanContext: undefined }) }) diff --git a/packages/dd-trace/test/plugins/util/git.spec.js b/packages/dd-trace/test/plugins/util/git.spec.js index f74a8530515..9c971701f89 100644 --- a/packages/dd-trace/test/plugins/util/git.spec.js +++ b/packages/dd-trace/test/plugins/util/git.spec.js @@ -7,7 +7,7 @@ const os = require('os') const fs = require('fs') const path = require('path') -const { GIT_REV_LIST_MAX_BUFFER } = require('../../../src/plugins/util/git') +const { GIT_REV_LIST_MAX_BUFFER, isGitAvailable } = require('../../../src/plugins/util/git') const proxyquire = require('proxyquire') const execFileSyncStub = sinon.stub().returns('') @@ -378,3 +378,25 @@ describe('user credentials', () => { .to.equal('ssh://host.xz:port/path/to/repo.git/') }) }) + +describe('isGitAvailable', () => { + let originalPath + + beforeEach(() => { + originalPath = process.env.PATH + }) + + afterEach(() => { + process.env.PATH = originalPath + }) + + it('returns true if git is available', () => { + expect(isGitAvailable()).to.be.true + }) + + it('returns false if git is not available', () => { + process.env.PATH = '' + + expect(isGitAvailable()).to.be.false + }) +}) diff --git a/packages/dd-trace/test/setup/core.js b/packages/dd-trace/test/setup/core.js index ab1e6531089..491a1a92122 100644 --- a/packages/dd-trace/test/setup/core.js +++ b/packages/dd-trace/test/setup/core.js @@ -42,3 +42,9 @@ if (global.describe && typeof global.describe.skip !== 'function') { } process.env.DD_INSTRUMENTATION_TELEMETRY_ENABLED = 'false' + +// If this is a release PR, set the SSI variables. +if (/^v\d+\.x$/.test(process.env.GITHUB_BASE_REF || '')) { + process.env.DD_INJECTION_ENABLED = 'true' + process.env.DD_INJECT_FORCE = 'true' +} diff --git a/scripts/prepare-release-proposal.js b/scripts/prepare-release-proposal.js new file mode 100644 index 00000000000..507ac3d4708 --- /dev/null +++ b/scripts/prepare-release-proposal.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +'use strict' + +const semver = require('semver') +const packageJson = require('../package.json') +const path = require('path') +const { execSync } = require('child_process') +const { readFileSync, writeFileSync } = require('fs') + +function helpAndExit () { + console.log('usage: node prepare-release-proposal.js ') + console.log('Actions:') + console.log(' create-branch Create a branch for the release proposal') + console.log(' commit-branch-diffs Commit the branch diffs to the release proposal branch') + console.log(' update-package-json Update the package.json version to the release proposal version') + console.log(' help Show this help message and exit') + process.exit() +} + +function createReleaseBranch (args) { + if (typeof args === 'string') { + const newVersion = semver.inc(packageJson.version, args) + const branchName = `v${newVersion}-proposal` + execSync(`git checkout -b ${branchName}`, { stdio: 'ignore' }) + + console.log(branchName) + return + } + + switch (args[0]) { + case 'minor': + case 'patch': + createReleaseBranch(args[0]) + break + case 'help': + default: + console.log('usage: node prepare-release-proposal.js create-branch ') + console.log('Version types:') + console.log(' minor Create a branch for a minor release proposal') + console.log(' patch Create a branch for a patch release proposal') + break + } +} + +function commitBranchDiffs (args) { + if (args.length !== 1) { + console.log('usage: node prepare-release-proposal.js commit-branch-diffs ') + console.log('release-branches:') + console.log(' v4.x') + console.log(' v5.x') + return + } + const releaseBranch = args[0] + + const excludedLabels = [ + 'semver-major', + `dont-land-on-${releaseBranch}` + ] + + const commandCore = `branch-diff --user DataDog --repo dd-trace-js --exclude-label=${excludedLabels.join(',')}` + + const releaseNotesDraft = execSync(`${commandCore} ${releaseBranch} master`).toString() + + execSync(`${commandCore} --format=sha --reverse ${releaseBranch} master | xargs git cherry-pick`) + + console.log(releaseNotesDraft) +} + +function updatePackageJson (args) { + if (args.length !== 1) { + console.log('usage: node prepare-release-proposal.js update-package-json ') + console.log(' minor') + console.log(' patch') + return + } + + const newVersion = semver.inc(packageJson.version, args[0]) + const packageJsonPath = path.join(__dirname, '..', 'package.json') + + const packageJsonString = readFileSync(packageJsonPath).toString() + .replace(`"version": "${packageJson.version}"`, `"version": "${newVersion}"`) + + writeFileSync(packageJsonPath, packageJsonString) + + console.log(newVersion) +} + +const methodArgs = process.argv.slice(3) +switch (process.argv[2]) { + case 'create-branch': + createReleaseBranch(methodArgs) + break + case 'commit-branch-diffs': + commitBranchDiffs(methodArgs) + break + case 'update-package-json': + updatePackageJson(methodArgs) + break + case 'help': + default: + helpAndExit() + break +} diff --git a/yarn.lock b/yarn.lock index a8844ea1726..a959686d8d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -263,18 +263,18 @@ dependencies: node-gyp-build "^3.9.0" -"@datadog/native-iast-rewriter@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.4.0.tgz#369de141ecb31118882d3339ba6e4d3487c9de97" - integrity sha512-vmcPM9h/jfv1vsej0tBglTah/gia4r6cwzWd4EDsSVOjuNdD+a7L2ZcCtYUxRlrETsOfuiblp6D+o921Qcb0MQ== +"@datadog/native-iast-rewriter@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.4.1.tgz#e8211f78c818906513fb96a549374da0382c7623" + integrity sha512-j3auTmyyn63e2y+SL28CGNy/l+jXQyh+pxqoGTacWaY5FW/dvo5nGQepAismgJ3qJ8VhQfVWRdxBSiT7wu9clw== dependencies: lru-cache "^7.14.0" node-gyp-build "^4.5.0" -"@datadog/native-iast-taint-tracking@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.0.0.tgz#a0be16f49f49c2a5917d08e5986c0fc6c877ed13" - integrity sha512-V+25+edlNCQSNRUvL45IajN+CFEjii9NbjfSMG6HRHbH/zeLL9FCNE+GU88dwB1bqXKNpBdrIxsfgTN65Yq9tA== +"@datadog/native-iast-taint-tracking@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.1.0.tgz#7b2ed7f8fad212d65e5ab03bcdea8b42a3051b2e" + integrity sha512-rw6qSjmxmu1yFHVvZLXFt/rVq2tUZXocNogPLB8n7MPpA0jijNGb109WokWw5ITImiW91GcGDuBW6elJDVKouQ== dependencies: node-gyp-build "^3.9.0" @@ -657,13 +657,18 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/node@>=13.7.0", "@types/node@>=18": - version "20.10.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.6.tgz#a3ec84c22965802bf763da55b2394424f22bfbb5" - integrity sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw== +"@types/node@>=13.7.0": + version "20.14.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.11.tgz#09b300423343460455043ddd4d0ded6ac579b74b" + integrity sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA== 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/prop-types@*": version "15.7.5" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz"