From cdb503cf592019b91cb17819e69020ef8ee2b9b9 Mon Sep 17 00:00:00 2001 From: William Conti Date: Thu, 7 Aug 2025 09:09:12 -0400 Subject: [PATCH 01/28] initial commit --- packages/dd-trace/src/plugins/tracing.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index f012f5546c8..894114bea8b 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -4,6 +4,7 @@ const Plugin = require('./plugin') const { storage } = require('../../../datadog-core') const analyticsSampler = require('../analytics_sampler') const { COMPONENT } = require('../constants') +const DatadogSpanContext = require('../opentracing/span_context') class TracingPlugin extends Plugin { constructor (...args) { @@ -125,6 +126,15 @@ class TracingPlugin extends Plugin { childOf = store.span } + if (!childOf && process.env.DD_TRACE_PARENT_ID) { + childOf = new DatadogSpanContext({ + traceId: process.env.DD_TRACE_TRACE_ID, + spanId: process.env.DD_TRACE_PARENT_ID, + isRemote: true, + }) + process.env.DD_TRACE_PARENT_ID = undefined + } + const span = tracer.startSpan(name, { startTime, childOf, From 0cfc6098701ffebae25077cf155b4f42d4f12b9d Mon Sep 17 00:00:00 2001 From: William Conti Date: Fri, 8 Aug 2025 09:10:26 -0400 Subject: [PATCH 02/28] change --- packages/dd-trace/src/plugins/tracing.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index 894114bea8b..29c6bb7f7b4 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -5,6 +5,7 @@ const { storage } = require('../../../datadog-core') const analyticsSampler = require('../analytics_sampler') const { COMPONENT } = require('../constants') const DatadogSpanContext = require('../opentracing/span_context') +const id = require('../id') class TracingPlugin extends Plugin { constructor (...args) { @@ -126,13 +127,15 @@ class TracingPlugin extends Plugin { childOf = store.span } - if (!childOf && process.env.DD_TRACE_PARENT_ID) { + console.log(process.env.DD_TRACE_ID, process.env.DD_PARENT_SPAN_ID) + if (!childOf && process.env.DD_PARENT_SPAN_ID) { + console.log("starting spans from remote trace") childOf = new DatadogSpanContext({ - traceId: process.env.DD_TRACE_TRACE_ID, - spanId: process.env.DD_TRACE_PARENT_ID, + traceId: id(process.env.DD_TRACE_ID, 16), + spanId: id(process.env.DD_PARENT_SPAN_ID, 16), isRemote: true, + sampling: { priority: 1 }, }) - process.env.DD_TRACE_PARENT_ID = undefined } const span = tracer.startSpan(name, { From abdde8ef0a59f06b75cf47da142bd77eace091df Mon Sep 17 00:00:00 2001 From: William Conti Date: Fri, 8 Aug 2025 10:12:32 -0400 Subject: [PATCH 03/28] initial commit --- .github/workflows/serverless.yml | 74 +- .../src/index.js | 9 + .../tests/index.spec.js | 268 +++++ .../src/index.js | 9 +- .../datadog-plugin-express/src/code_origin.js | 7 +- .../datadog-plugin-fastify/src/code_origin.js | 7 +- .../datadog-plugin-find-my-way/src/index.js | 7 +- packages/datadog-plugin-hapi/src/index.js | 3 +- packages/datadog-plugin-hono/src/index.js | 5 +- packages/datadog-plugin-http/src/server.js | 19 +- packages/datadog-plugin-http2/src/server.js | 19 +- .../src/index.js | 131 +++ packages/datadog-plugin-koa/src/index.js | 3 +- .../src/index.js | 3 +- packages/datadog-plugin-next/src/index.js | 13 +- packages/datadog-plugin-restify/src/index.js | 3 +- packages/datadog-plugin-router/src/index.js | 7 +- packages/datadog-plugin-web/src/index.js | 618 ++++++++++- .../datadog-plugin-web/test/index.spec.js | 972 ++++++++++++++++++ .../src/plugins/util/inferred_proxy.js | 116 --- packages/dd-trace/src/plugins/util/web.js | 633 ------------ 21 files changed, 2078 insertions(+), 848 deletions(-) create mode 100644 packages/datadog-plugin-aws-apigateway/src/index.js create mode 100644 packages/datadog-plugin-aws-apigateway/tests/index.spec.js create mode 100644 packages/datadog-plugin-inferred-proxy/src/index.js create mode 100644 packages/datadog-plugin-web/test/index.spec.js delete mode 100644 packages/dd-trace/src/plugins/util/inferred_proxy.js delete mode 100644 packages/dd-trace/src/plugins/util/web.js diff --git a/.github/workflows/serverless.yml b/.github/workflows/serverless.yml index 3e6bbfebefe..664ee9cce58 100644 --- a/.github/workflows/serverless.yml +++ b/.github/workflows/serverless.yml @@ -62,7 +62,7 @@ jobs: localstack-legacy: image: localstack/localstack:1.1.0 ports: - - "127.0.0.1:4567:4567" # Edge + - '127.0.0.1:4567:4567' # Edge env: LOCALSTACK_SERVICES: dynamodb,kinesis,s3,sqs,sns,redshift,route53,logs,serverless EXTRA_CORS_ALLOWED_HEADERS: x-amz-request-id,x-amzn-requestid,x-amz-id-2 @@ -94,25 +94,33 @@ jobs: suffix: plugins-${{ github.job }}-${{ matrix.node-version }} - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + aws-apigateway: + runs-on: ubuntu-latest + env: + PLUGINS: aws-apigateway + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: ./.github/actions/plugins/test-and-upstream + azure-functions: runs-on: ubuntu-latest services: - azureservicebusemulator: - image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 - ports: - - "127.0.0.1:5672:5672" - - "127.0.0.1:5300:5300" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" - SQL_SERVER: azuresqledge - azuresqledge: - image: mcr.microsoft.com/azure-sql-edge:1.0.7 - ports: - - "127.0.0.1:1433:1433" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" + azureservicebusemulator: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 + ports: + - '127.0.0.1:5672:5672' + - '127.0.0.1:5300:5300' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' + SQL_SERVER: azuresqledge + azuresqledge: + image: mcr.microsoft.com/azure-sql-edge:1.0.7 + ports: + - '127.0.0.1:1433:1433' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' env: PLUGINS: azure-functions SERVICES: azureservicebusemulator,azuresqledge @@ -123,22 +131,22 @@ jobs: azure-service-bus: runs-on: ubuntu-latest services: - azureservicebusemulator: - image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 - ports: - - "127.0.0.1:5672:5672" - - "127.0.0.1:5300:5300" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" - SQL_SERVER: azuresqledge - azuresqledge: - image: mcr.microsoft.com/azure-sql-edge:1.0.7 - ports: - - "127.0.0.1:1433:1433" - env: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "Localtestpass1!" + azureservicebusemulator: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:1.1.2 + ports: + - '127.0.0.1:5672:5672' + - '127.0.0.1:5300:5300' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' + SQL_SERVER: azuresqledge + azuresqledge: + image: mcr.microsoft.com/azure-sql-edge:1.0.7 + ports: + - '127.0.0.1:1433:1433' + env: + ACCEPT_EULA: 'Y' + MSSQL_SA_PASSWORD: 'Localtestpass1!' env: PLUGINS: azure-service-bus SERVICES: azureservicebusemulator,azuresqledge diff --git a/packages/datadog-plugin-aws-apigateway/src/index.js b/packages/datadog-plugin-aws-apigateway/src/index.js new file mode 100644 index 00000000000..acff124ba49 --- /dev/null +++ b/packages/datadog-plugin-aws-apigateway/src/index.js @@ -0,0 +1,9 @@ +'use strict' + +const InferredProxyPlugin = require('../../datadog-plugin-inferred-proxy/src') + +class ApiGatewayPlugin extends InferredProxyPlugin { + static id = 'aws-apigateway' +} + +module.exports = ApiGatewayPlugin diff --git a/packages/datadog-plugin-aws-apigateway/tests/index.spec.js b/packages/datadog-plugin-aws-apigateway/tests/index.spec.js new file mode 100644 index 00000000000..51969fda544 --- /dev/null +++ b/packages/datadog-plugin-aws-apigateway/tests/index.spec.js @@ -0,0 +1,268 @@ +'use strict' + +const agent = require('../../dd-trace/test/plugins/agent') +const { expect } = require('chai') +const axios = require('axios') +const { Agent } = require('http') + +// Create axios instance with no connection pooling +const httpClient = axios.create({ + httpAgent: new Agent({ keepAlive: false }), + timeout: 5000 +}) + +describe('Plugin', function () { + let http + let appListener + let controller + let port + + // tap was throwing timeout errors when trying to use hooks like `before`, so instead we just use this function + // and call before the test starts + const loadTest = async function ({ inferredProxyServicesEnabled = true } = {}) { + const options = { + inferredProxyServicesEnabled, + service: 'aws-server' + } + + await agent.load( + ['http', 'dns', 'net', 'aws-apigateway'], + [{ client: false }, { enabled: false }, { enabled: false }, { enabled: true }], + options + ) + + // we can't force re-init the tracer, so we have to set the config manually + const tracer = require('../../../../dd-trace').init(options) + tracer._tracer._config.inferredProxyServicesEnabled = inferredProxyServicesEnabled + + http = require('http') + + const server = new http.Server(async (req, res) => { + controller && await controller(req, res) + if (req.url === '/error') { + res.statusCode = 500 + res.end(JSON.stringify({ message: 'ERROR' })) + } else { + res.writeHead(200) + res.end(JSON.stringify({ message: 'OK' })) + } + }) + + // Force close connections when server closes + const connections = new Set() + server.on('connection', (connection) => { + connections.add(connection) + connection.on('close', () => { + connections.delete(connection) + }) + }) + + return new Promise((resolve, reject) => { + appListener = server.listen(0, '127.0.0.1', () => { + port = server.address().port + appListener._connections = connections + resolve() + }) + }) + } + + const cleanupTest = async function () { + controller = null + + if (appListener) { + // Force close all existing connections + if (appListener._connections) { + for (const connection of appListener._connections) { + connection.destroy() + } + } + + await new Promise((resolve, reject) => { + appListener.close((err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + appListener = null + } + + await agent.close() + } + + const inferredHeaders = { + 'x-dd-proxy': 'aws-apigateway', + 'x-dd-proxy-request-time-ms': '1729780025473', + 'x-dd-proxy-path': '/test', + 'x-dd-proxy-httpmethod': 'GET', + 'x-dd-proxy-domain-name': 'example.com', + 'x-dd-proxy-stage': 'dev' + } + + afterEach(async () => { + await cleanupTest() + }) + + describe('without configuration', () => { + it('should create a parent span and a child span for a 200', async () => { + await loadTest({}) + + await httpClient.get(`http://127.0.0.1:${port}/`, { + headers: inferredHeaders + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + + expect(spans.length).to.be.equal(2) + + expect(spans[0]).to.have.property('name', 'aws.apigateway') + expect(spans[0]).to.have.property('service', 'example.com') + expect(spans[0]).to.have.property('resource', 'GET /test') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0].meta).to.have.property('http.url', 'example.com/test') + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('component', 'aws-apigateway') + expect(spans[0].meta).to.have.property('_dd.integration', 'aws-apigateway') + expect(spans[0].metrics).to.have.property('_dd.inferred_span', 1) + expect(spans[0].start.toString()).to.be.equal('1729780025472999936') + + expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) + + expect(spans[1]).to.have.property('name', 'web.request') + expect(spans[1]).to.have.property('service', 'aws-server') + expect(spans[1]).to.have.property('type', 'web') + expect(spans[1]).to.have.property('resource', 'GET') + expect(spans[1].meta).to.have.property('component', 'http') + expect(spans[1].meta).to.have.property('span.kind', 'server') + expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/`) + expect(spans[1].meta).to.have.property('http.method', 'GET') + expect(spans[1].meta).to.have.property('http.status_code', '200') + expect(spans[1].meta).to.have.property('span.kind', 'server') + }) + }) + + it('should create a parent span and a child span for an error', async () => { + await loadTest({}) + + await httpClient.get(`http://127.0.0.1:${port}/error`, { + headers: inferredHeaders, + validateStatus: function (status) { + return status === 500 + } + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + expect(spans.length).to.be.equal(2) + + expect(spans[0]).to.have.property('name', 'aws.apigateway') + expect(spans[0]).to.have.property('service', 'example.com') + expect(spans[0]).to.have.property('resource', 'GET /test') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0].meta).to.have.property('http.url', 'example.com/test') + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '500') + expect(spans[0].meta).to.have.property('component', 'aws-apigateway') + expect(spans[0].error).to.be.equal(1) + expect(spans[0].start.toString()).to.be.equal('1729780025472999936') + expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) + + expect(spans[1]).to.have.property('name', 'web.request') + expect(spans[1]).to.have.property('service', 'aws-server') + expect(spans[1]).to.have.property('type', 'web') + expect(spans[1]).to.have.property('resource', 'GET') + expect(spans[1].meta).to.have.property('component', 'http') + expect(spans[1].meta).to.have.property('span.kind', 'server') + expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/error`) + expect(spans[1].meta).to.have.property('http.method', 'GET') + expect(spans[1].meta).to.have.property('http.status_code', '500') + expect(spans[1].meta).to.have.property('span.kind', 'server') + expect(spans[1].error).to.be.equal(1) + }) + }) + + it('should not create an API Gateway span if all necessary headers are missing', async () => { + await loadTest({}) + + await httpClient.get(`http://127.0.0.1:${port}/no-aws-headers`, { + headers: {} + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + expect(spans.length).to.be.equal(1) + + expect(spans[0]).to.have.property('name', 'web.request') + expect(spans[0]).to.have.property('service', 'aws-server') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0]).to.have.property('resource', 'GET') + expect(spans[0].meta).to.have.property('component', 'http') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/no-aws-headers`) + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].error).to.be.equal(0) + }) + }) + + it('should not create an API Gateway span if missing the proxy system header', async () => { + await loadTest({}) + + // remove x-dd-proxy from headers + const { 'x-dd-proxy': _, ...newHeaders } = inferredHeaders + + await httpClient.get(`http://127.0.0.1:${port}/a-few-aws-headers`, { + headers: newHeaders + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + expect(spans.length).to.be.equal(1) + + expect(spans[0]).to.have.property('name', 'web.request') + expect(spans[0]).to.have.property('service', 'aws-server') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0]).to.have.property('resource', 'GET') + expect(spans[0].meta).to.have.property('component', 'http') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/a-few-aws-headers`) + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].error).to.be.equal(0) + }) + }) + }) + + describe('with configuration', function () { + it('should not create a span when configured to be off', async () => { + await loadTest({ inferredProxyServicesEnabled: false }) + + await httpClient.get(`http://127.0.0.1:${port}/configured-off`, { + headers: inferredHeaders + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + + expect(spans.length).to.be.equal(1) + + expect(spans[0]).to.have.property('name', 'web.request') + expect(spans[0]).to.have.property('service', 'aws-server') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0]).to.have.property('resource', 'GET') + expect(spans[0].meta).to.have.property('component', 'http') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/configured-off`) + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('span.kind', 'server') + }) + }) + }) +}) diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index 23355bd11a3..93daba89e4d 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -1,8 +1,7 @@ 'use strict' -const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const WebPlugin = require('../../datadog-plugin-web/src') const serverless = require('../../dd-trace/src/plugins/util/serverless') -const web = require('../../dd-trace/src/plugins/util/web') const triggerMap = { deleteRequest: 'Http', @@ -15,7 +14,7 @@ const triggerMap = { serviceBusTopic: 'ServiceBus', } -class AzureFunctionsPlugin extends TracingPlugin { +class AzureFunctionsPlugin extends WebPlugin { static id = 'azure-functions' static operation = 'invoke' static kind = 'server' @@ -51,7 +50,7 @@ class AzureFunctionsPlugin extends TracingPlugin { headers: Object.fromEntries(httpRequest.headers), url: path } - const context = web.patch(req) + const context = this.patch(req) context.config = this.config context.paths = [path] context.res = { statusCode: result.status } @@ -65,7 +64,7 @@ class AzureFunctionsPlugin extends TracingPlugin { } configure (config) { - return super.configure(web.normalizeConfig(config)) + return super.configure(this.normalizeConfig(config)) } } diff --git a/packages/datadog-plugin-express/src/code_origin.js b/packages/datadog-plugin-express/src/code_origin.js index 19755bc1012..bb0ae12a271 100644 --- a/packages/datadog-plugin-express/src/code_origin.js +++ b/packages/datadog-plugin-express/src/code_origin.js @@ -1,10 +1,9 @@ 'use strict' const { entryTags } = require('../../datadog-code-origin') -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') -class ExpressCodeOriginForSpansPlugin extends Plugin { +class ExpressCodeOriginForSpansPlugin extends WebPlugin { static id = 'express' constructor (...args) { @@ -15,7 +14,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin { this.addSub('apm:express:middleware:enter', ({ req, layer }) => { const tags = layerTags.get(layer) if (!tags) return - web.getContext(req).span?.addTags(tags) + this.getContext(req).span?.addTags(tags) }) this.addSub('apm:express:route:added', ({ topOfStackFunc, layer }) => { diff --git a/packages/datadog-plugin-fastify/src/code_origin.js b/packages/datadog-plugin-fastify/src/code_origin.js index 3b8bf1ef15d..d85187efed6 100644 --- a/packages/datadog-plugin-fastify/src/code_origin.js +++ b/packages/datadog-plugin-fastify/src/code_origin.js @@ -1,12 +1,11 @@ 'use strict' const { entryTags } = require('../../datadog-code-origin') -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') const kCodeOriginForSpansTagsSym = Symbol('datadog.codeOriginForSpansTags') -class FastifyCodeOriginForSpansPlugin extends Plugin { +class FastifyCodeOriginForSpansPlugin extends WebPlugin { static id = 'fastify' constructor (...args) { @@ -15,7 +14,7 @@ class FastifyCodeOriginForSpansPlugin extends Plugin { this.addSub('apm:fastify:request:handle', ({ req, routeConfig }) => { const tags = routeConfig?.[kCodeOriginForSpansTagsSym] if (!tags) return - const context = web.getContext(req) + const context = this.getContext(req) context.span?.addTags(tags) }) diff --git a/packages/datadog-plugin-find-my-way/src/index.js b/packages/datadog-plugin-find-my-way/src/index.js index f8d152d2060..fae020e6f52 100644 --- a/packages/datadog-plugin-find-my-way/src/index.js +++ b/packages/datadog-plugin-find-my-way/src/index.js @@ -1,16 +1,15 @@ 'use strict' -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') -class FindMyWayPlugin extends Plugin { +class FindMyWayPlugin extends WebPlugin { static id = 'find-my-way' constructor (...args) { super(...args) this.addSub('apm:find-my-way:request:route', ({ req, route }) => { - web.setRoute(req, route) + this.setRoute(req, route) }) } } diff --git a/packages/datadog-plugin-hapi/src/index.js b/packages/datadog-plugin-hapi/src/index.js index 2565de4e4a8..15cc9097f7a 100644 --- a/packages/datadog-plugin-hapi/src/index.js +++ b/packages/datadog-plugin-hapi/src/index.js @@ -2,7 +2,6 @@ const { storage } = require('../../datadog-core') const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class HapiPlugin extends RouterPlugin { static id = 'hapi' @@ -21,7 +20,7 @@ class HapiPlugin extends RouterPlugin { }) this.addSub('apm:hapi:request:route', ({ req, route }) => { - web.setRoute(req, route) + this.setRoute(req, route) }) this.addSub('apm:hapi:request:error', error => { diff --git a/packages/datadog-plugin-hono/src/index.js b/packages/datadog-plugin-hono/src/index.js index cec77fdd004..d01be76b0a5 100644 --- a/packages/datadog-plugin-hono/src/index.js +++ b/packages/datadog-plugin-hono/src/index.js @@ -1,7 +1,6 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class HonoPlugin extends RouterPlugin { static id = 'hono' @@ -14,11 +13,11 @@ class HonoPlugin extends RouterPlugin { }) this.addSub('apm:hono:request:route', ({ req, route }) => { - web.setRoute(req, route) + this.setRoute(req, route) }) this.addSub('apm:hono:request:error', ({ req, error }) => { - web.addError(req, error) + this.addError(req, error) }) } } diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index 7fad4f20133..25960926f67 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -1,12 +1,11 @@ 'use strict' -const ServerPlugin = require('../../dd-trace/src/plugins/server') +const WebPlugin = require('../../datadog-plugin-web/src') const { storage } = require('../../datadog-core') -const web = require('../../dd-trace/src/plugins/util/web') const { incomingHttpRequestStart, incomingHttpRequestEnd } = require('../../dd-trace/src/appsec/channels') const { COMPONENT } = require('../../dd-trace/src/constants') -class HttpServerPlugin extends ServerPlugin { +class HttpServerPlugin extends WebPlugin { static id = 'http' static prefix = 'apm:http:server:request' @@ -19,7 +18,7 @@ class HttpServerPlugin extends ServerPlugin { start ({ req, res, abortController }) { const store = storage('legacy').getStore() - const span = web.startSpan( + const span = this.startSpan( this.tracer, { ...this.config, @@ -35,10 +34,10 @@ class HttpServerPlugin extends ServerPlugin { this._parentStore = store this.enter(span, { ...store, req, res }) - const context = web.getContext(req) + const context = this.getContext(req) if (!context.instrumented) { - context.res.writeHead = web.wrapWriteHead(context) + context.res.writeHead = this.wrapWriteHead(context) context.instrumented = true } @@ -48,11 +47,11 @@ class HttpServerPlugin extends ServerPlugin { } error (error) { - web.addError(error) + this.addError(error) } finish ({ req }) { - const context = web.getContext(req) + const context = this.getContext(req) if (!context || !context.res) return // Not created by a http.Server instance. @@ -60,7 +59,7 @@ class HttpServerPlugin extends ServerPlugin { incomingHttpRequestEnd.publish({ req, res: context.res }) } - web.finishAll(context) + this.finishAll(context) } exit ({ req }) { @@ -70,7 +69,7 @@ class HttpServerPlugin extends ServerPlugin { } configure (config) { - return super.configure(web.normalizeConfig(config)) + return super.configure(this.normalizeConfig(config)) } } diff --git a/packages/datadog-plugin-http2/src/server.js b/packages/datadog-plugin-http2/src/server.js index c7bdb6d5274..c2bcdc2a7ec 100644 --- a/packages/datadog-plugin-http2/src/server.js +++ b/packages/datadog-plugin-http2/src/server.js @@ -2,11 +2,10 @@ // Plugin temporarily disabled. See https://github.com/DataDog/dd-trace-js/issues/312 -const ServerPlugin = require('../../dd-trace/src/plugins/server') -const web = require('../../dd-trace/src/plugins/util/web') +const WebPlugin = require('../../datadog-plugin-web/src') const { COMPONENT } = require('../../dd-trace/src/constants') -class Http2ServerPlugin extends ServerPlugin { +class Http2ServerPlugin extends WebPlugin { constructor (tracer, config) { super(tracer, config) this.addBind('apm:http2:server:response:emit', this.bindEmit) @@ -19,7 +18,7 @@ class Http2ServerPlugin extends ServerPlugin { bindStart (ctx) { const { req, res } = ctx - const span = web.startSpan( + const span = this.startSpan( this.tracer, { ...this.config, @@ -37,10 +36,10 @@ class Http2ServerPlugin extends ServerPlugin { ctx.currentStore.req = req ctx.currentStore.res = res - const context = web.getContext(req) + const context = this.getContext(req) if (!context.instrumented) { - context.res.writeHead = web.wrapWriteHead(context) + context.res.writeHead = this.wrapWriteHead(context) context.instrumented = true } @@ -52,21 +51,21 @@ class Http2ServerPlugin extends ServerPlugin { const { req } = ctx - const context = web.getContext(req) + const context = this.getContext(req) if (!context || !context.res) return // Not created by a http.Server instance. - web.finishAll(context) + this.finishAll(context) return ctx.currentStore } error (error) { - web.addError(error) + this.addError(error) } configure (config) { - return super.configure(web.normalizeConfig(config)) + return super.configure(this.normalizeConfig(config)) } } diff --git a/packages/datadog-plugin-inferred-proxy/src/index.js b/packages/datadog-plugin-inferred-proxy/src/index.js new file mode 100644 index 00000000000..1ce455b4583 --- /dev/null +++ b/packages/datadog-plugin-inferred-proxy/src/index.js @@ -0,0 +1,131 @@ +'use strict' + +const WebPlugin = require('../../datadog-plugin-web/src') +const log = require('../../dd-trace/src/log') +const tags = require('../../../ext/tags') + +const RESOURCE_NAME = tags.RESOURCE_NAME +const SPAN_TYPE = tags.SPAN_TYPE +const HTTP_URL = tags.HTTP_URL +const HTTP_METHOD = tags.HTTP_METHOD + +const PROXY_HEADER_SYSTEM = 'x-dd-proxy' +const PROXY_HEADER_START_TIME_MS = 'x-dd-proxy-request-time-ms' +const PROXY_HEADER_PATH = 'x-dd-proxy-path' +const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod' +const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name' +const PROXY_HEADER_STAGE = 'x-dd-proxy-stage' + +class InferredProxyPlugin extends WebPlugin { + static id = 'inferred-proxy' + + constructor (...args) { + super(...args) + + this.supportedProxies = { + 'aws-apigateway': { + spanName: 'aws.apigateway', + component: 'aws-apigateway' + } + } + } + + startChildSpan (tracer, config, name, req, traceCtx) { + const headers = req.headers + const reqCtx = this.getContext(req) + let childOf = super.startChildSpan(tracer, config, name, req, traceCtx) + + if (!headers) { + return childOf + } + + if (!tracer._config?.inferredProxyServicesEnabled) { + return childOf + } + + const proxyContext = this.extractInferredProxyContext(headers) + + if (!proxyContext) { + return childOf + } + + const proxySpanInfo = this.supportedProxies[proxyContext.proxySystemName] + + log.debug('Successfully extracted inferred span info %s for proxy:', proxyContext, proxyContext.proxySystemName) + + const span = super.startSpan(proxySpanInfo.spanName, { + childOf, + type: 'web', + startTime: proxyContext.requestTime, + integrationName: proxySpanInfo.component, + meta: { + service: proxyContext.domainName || tracer._config.service, + component: proxySpanInfo.component, + [SPAN_TYPE]: 'web', + [HTTP_METHOD]: proxyContext.method, + [HTTP_URL]: proxyContext.domainName + proxyContext.path, + stage: proxyContext.stage + } + }, traceCtx, config) + + reqCtx.inferredProxySpan = span + childOf = span + + log.debug('Successfully created inferred proxy span.') + + this.setInferredProxySpanTags(span, proxyContext) + + return childOf + } + + setInferredProxySpanTags (span, proxyContext) { + span.setTag(RESOURCE_NAME, `${proxyContext.method} ${proxyContext.path}`) + span.setTag('_dd.inferred_span', 1) + return span + } + + extractInferredProxyContext (headers) { + if (!(PROXY_HEADER_START_TIME_MS in headers)) { + return null + } + + if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in this.supportedProxies)) { + log.debug('Received headers to create inferred proxy span but headers include an unsupported proxy type', headers) + return null + } + + return { + requestTime: headers[PROXY_HEADER_START_TIME_MS] + ? Number.parseInt(headers[PROXY_HEADER_START_TIME_MS], 10) + : null, + method: headers[PROXY_HEADER_HTTPMETHOD], + path: headers[PROXY_HEADER_PATH], + stage: headers[PROXY_HEADER_STAGE], + domainName: headers[PROXY_HEADER_DOMAIN], + proxySystemName: headers[PROXY_HEADER_SYSTEM] + } + } + + finishAll (context) { + super.finishAll(context) + this.finishInferredProxySpan(context) + } + + finishInferredProxySpan (context) { + const { req } = context + + if (!context.inferredProxySpan) return + + if (context.inferredProxySpanFinished && !req.stream) return + + // context.config.hooks.request(context.inferredProxySpan, req, res) # TODO: Do we need this?? + + // Only close the inferred span if one was created + if (context.inferredProxySpan) { + context.inferredProxySpan.finish() + context.inferredProxySpanFinished = true + } + } +} + +module.exports = InferredProxyPlugin diff --git a/packages/datadog-plugin-koa/src/index.js b/packages/datadog-plugin-koa/src/index.js index 594b7c648f0..8cc9f1164b4 100644 --- a/packages/datadog-plugin-koa/src/index.js +++ b/packages/datadog-plugin-koa/src/index.js @@ -1,7 +1,6 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class KoaPlugin extends RouterPlugin { static id = 'koa' @@ -14,7 +13,7 @@ class KoaPlugin extends RouterPlugin { }) this.addSub('apm:koa:request:route', ({ req, route }) => { - web.setRoute(req, route) + this.setRoute(req, route) }) } } diff --git a/packages/datadog-plugin-microgateway-core/src/index.js b/packages/datadog-plugin-microgateway-core/src/index.js index 1160aa974ee..2f853144a33 100644 --- a/packages/datadog-plugin-microgateway-core/src/index.js +++ b/packages/datadog-plugin-microgateway-core/src/index.js @@ -1,7 +1,6 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class MicrogatewayCorePlugin extends RouterPlugin { static id = 'microgateway-core' @@ -14,7 +13,7 @@ class MicrogatewayCorePlugin extends RouterPlugin { }) this.addSub('apm:microgateway-core:request:route', ({ req, route }) => { - web.setRoute(req, route) + this.setRoute(req, route) }) this.addSub('apm:microgateway-core:request:error', ({ error }) => { diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index d1e80f658df..0dd9feecd69 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -1,14 +1,13 @@ 'use strict' -const ServerPlugin = require('../../dd-trace/src/plugins/server') +const WebPlugin = require('../../datadog-plugin-web/src') const { storage } = require('../../datadog-core') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { COMPONENT } = require('../../dd-trace/src/constants') -const web = require('../../dd-trace/src/plugins/util/web') const errorPages = new Set(['/404', '/500', '/_error', '/_not-found', '/_not-found/page']) -class NextPlugin extends ServerPlugin { +class NextPlugin extends WebPlugin { static id = 'next' constructor (...args) { @@ -63,15 +62,15 @@ class NextPlugin extends ServerPlugin { if (requestError) { // prioritize user-set errors from API routes span.setTag('error', requestError) - web.addError(req, requestError) + this.addError(req, requestError) } else if (error) { // general error handling span.setTag('error', error) - web.addError(req, requestError || error) + this.addError(req, requestError || error) } else if (!this.config.validateStatus(res.statusCode)) { // where there's no error, we still need to validate status span.setTag('error', true) - web.addError(req, true) + this.addError(req, true) } span.addTags({ @@ -119,7 +118,7 @@ class NextPlugin extends ServerPlugin { 'resource.name': `${req.method} ${page}`.trim(), 'next.page': page }) - web.setRoute(req, page) + this.setRoute(req, page) } configure (config) { diff --git a/packages/datadog-plugin-restify/src/index.js b/packages/datadog-plugin-restify/src/index.js index 5f10d317936..04328db6165 100644 --- a/packages/datadog-plugin-restify/src/index.js +++ b/packages/datadog-plugin-restify/src/index.js @@ -1,7 +1,6 @@ 'use strict' const RouterPlugin = require('../../datadog-plugin-router/src') -const web = require('../../dd-trace/src/plugins/util/web') class RestifyPlugin extends RouterPlugin { static id = 'restify' @@ -14,7 +13,7 @@ class RestifyPlugin extends RouterPlugin { }) this.addSub('apm:restify:request:route', ({ req, route }) => { - web.setRoute(req, route) + this.setRoute(req, route) }) } diff --git a/packages/datadog-plugin-router/src/index.js b/packages/datadog-plugin-router/src/index.js index a2a3f419046..3e70888b1ff 100644 --- a/packages/datadog-plugin-router/src/index.js +++ b/packages/datadog-plugin-router/src/index.js @@ -1,6 +1,5 @@ 'use strict' -const web = require('../../dd-trace/src/plugins/util/web') const WebPlugin = require('../../datadog-plugin-web/src') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { storage } = require('../../datadog-core') @@ -31,8 +30,8 @@ class RouterPlugin extends WebPlugin { this._storeStack.push(store) this.enter(span, store) - web.patch(req) - web.setRoute(req, context.route) + this.patch(req) + this.setRoute(req, context.route) }) this.addSub(`apm:${this.constructor.id}:middleware:next`, ({ req }) => { @@ -58,7 +57,7 @@ class RouterPlugin extends WebPlugin { }) this.addSub(`apm:${this.constructor.id}:middleware:error`, ({ req, error }) => { - web.addError(req, error) + this.addError(req, error) if (!this.config.middleware) return diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 27219be714a..aa0ff07340d 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -1,18 +1,622 @@ 'use strict' -const Plugin = require('../../dd-trace/src/plugins/plugin') -const web = require('../../dd-trace/src/plugins/util/web') +const uniq = require('../../datadog-core/src/utils/src/uniq') +const analyticsSampler = require('../../dd-trace/src/analytics_sampler') +const FORMAT_HTTP_HEADERS = 'http_headers' +const log = require('../../dd-trace/src/log') +const tags = require('../../../ext/tags') +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') +const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') +const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') -class WebPlugin extends Plugin { - static id = 'web' +let extractIp - configure (config) { - return super.configure(web.normalizeConfig(config)) +const WEB = types.WEB +const SERVER = kinds.SERVER +const RESOURCE_NAME = tags.RESOURCE_NAME +const SERVICE_NAME = tags.SERVICE_NAME +const SPAN_TYPE = tags.SPAN_TYPE +const SPAN_KIND = tags.SPAN_KIND +const ERROR = tags.ERROR +const HTTP_METHOD = tags.HTTP_METHOD +const HTTP_URL = tags.HTTP_URL +const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE +const HTTP_ROUTE = tags.HTTP_ROUTE +const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS +const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS +const HTTP_USERAGENT = tags.HTTP_USERAGENT +const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP +const MANUAL_DROP = tags.MANUAL_DROP + +const HTTP2_HEADER_AUTHORITY = ':authority' +const HTTP2_HEADER_SCHEME = ':scheme' +const HTTP2_HEADER_PATH = ':path' + +const contexts = new WeakMap() +const ends = new WeakMap() + +class WebPlugin extends TracingPlugin { + static id = WEB + + constructor (...args) { + super(...args) + this.addSub('apm:http:server:request:error', ({ req, error }) => { + if (error) { + this.addError(req, error) + } + }) + + this.addSub('apm:http:server:request:end', ({ req }) => { + this.finish(req) + }) + } + + // Ensure the configuration has the correct structure and defaults. + normalizeConfig (config) { + const headers = getHeadersToRecord(config) + const validateStatus = getStatusValidator(config) + const hooks = getHooks(config) + const filter = urlFilter.getFilter(config) + const middleware = getMiddlewareSetting(config) + const queryStringObfuscation = getQsObfuscator(config) + + extractIp = config.clientIpEnabled && require('../../dd-trace/src/plugins/util/ip_extractor').extractIp + + return { + ...config, + headers, + validateStatus, + hooks, + filter, + middleware, + queryStringObfuscation + } } setFramework (req, name, config) { - web.setFramework(req, name, config) + const context = this.patch(req) + const span = context.span + + if (!span) return + + span.context()._name = `${name}.request` + span.context()._tags.component = name + span._integrationName = name + + this.setConfig(req, config) + } + + setConfig (req, config) { + const context = contexts.get(req) + const span = context.span + + context.config = config + + if (!config.filter(req.url)) { + span.setTag(MANUAL_DROP, true) + span.context()._trace.isRecording = false + } + + if (config.service) { + span.setTag(SERVICE_NAME, config.service) + } + + analyticsSampler.sample(span, config.measured, true) + } + + startSpan (tracer, config, req, res, name, traceCtx) { + const context = this.patch(req) + + let span + + if (context.span) { + context.span.context()._name = name + span = context.span + } else { + span = this.startChildSpan(tracer, config, name, req, traceCtx) + } + + context.tracer = tracer + context.span = span + context.res = res + + this.setConfig(req, config) + addRequestTags(context, this.constructor.id) + + return span + } + + wrap (req) { + const context = contexts.get(req) + if (!context.instrumented) { + this.wrapEnd(context) + context.instrumented = true + } + } + + // Start a span and activate a scope for a request. + instrument (tracer, config, req, res, name, callback) { + const span = this.startSpan(tracer, config, req, res, name) + + this.wrap(req) + + return callback && tracer.scope().activate(span, () => callback(span)) + } + + // Reactivate the request scope in case it was changed by a middleware. + reactivate (req, fn) { + return reactivate(req, fn) + } + + // Add a route segment that will be used for the resource name. + enterRoute (req, path) { + if (typeof path === 'string') { + contexts.get(req).paths.push(path) + } + } + + setRoute (req, path) { + const context = contexts.get(req) + + if (!context) return + + context.paths = [path] + } + + // Remove the current route segment. + exitRoute (req) { + contexts.get(req).paths.pop() + } + + // Start a new middleware span and activate a new scope with the span. + wrapMiddleware (req, middleware, name, fn) { + if (!this.active(req)) return fn() + + const context = contexts.get(req) + const tracer = context.tracer + const childOf = this.active(req) + const config = context.config + const traceCtx = context.traceCtx + + if (config.middleware === false) return this.bindAndWrapMiddlewareErrors(fn, req, tracer, childOf) + + const span = super.startSpan(name, { childOf }, traceCtx) + + analyticsSampler.sample(span, config.measured) + + span.addTags({ + [RESOURCE_NAME]: middleware._name || middleware.name || '' + }) + + context.middleware.push(span) + + return tracer.scope().activate(span, fn) + } + + // catch errors and apply to active span + bindAndWrapMiddlewareErrors (fn, req, tracer, activeSpan) { + try { + return tracer.scope().bind(fn, activeSpan).apply(this, arguments) + } catch (e) { + this.addError(req, e) // TODO: remove when error formatting is moved to Span + throw e + } + } + + // Finish the active middleware span. + finish (req, error) { + if (!this.active(req)) return + + const context = contexts.get(req) + const span = context.middleware.pop() + + if (span) { + if (error) { + span.addTags({ + [ERROR_TYPE]: error.name, + [ERROR_MESSAGE]: error.message, + [ERROR_STACK]: error.stack + }) + } + + span.finish() + } + } + + // Register a callback to run before res.end() is called. + beforeEnd (req, callback) { + contexts.get(req).beforeEnd.push(callback) + } + + // Prepare the request for instrumentation. + patch (req) { + let context = contexts.get(req) + + if (context) return context + + context = req.stream && contexts.get(req.stream) + + if (context) { + contexts.set(req, context) + return context + } + + context = { + req, + span: null, + paths: [], + middleware: [], + beforeEnd: [], + config: {} + } + + contexts.set(req, context) + + return context + } + + // Return the request root span. + root (req) { + const context = contexts.get(req) + return context ? context.span : null + } + + // Return the active span. + active (req) { + const context = contexts.get(req) + + if (!context) return null + if (context.middleware.length === 0) return context.span || null + + return context.middleware.at(-1) + } + + // Extract the parent span from the headers and start a new span as its child + startChildSpan (tracer, config, name, req, traceCtx) { + const headers = req.headers + const childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers) + + const span = super.startSpan(name, { childOf }, traceCtx) + + return span + } + + // Validate a request's status code and then add error tags if necessary + addStatusError (req, statusCode) { + const context = contexts.get(req) + const { span, inferredProxySpan, error } = context + + const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] + const inferredSpanContext = inferredProxySpan?.context() + const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] + + const isValidStatusCode = context.config.validateStatus(statusCode) + + if (!spanHasExistingError && !isValidStatusCode) { + span.setTag(ERROR, error || true) + } + + if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { + inferredProxySpan.setTag(ERROR, error || true) + } + } + + // Add an error to the request + addError (req, error) { + if (error instanceof Error) { + const context = contexts.get(req) + + if (context) { + context.error = error + } + } + } + + finishMiddleware (context) { + if (context.finished) return + + let span + + while ((span = context.middleware.pop())) { + span.finish() + } + } + + finishSpan (context) { + const { req, res } = context + + if (context.finished && !req.stream) return + + addRequestTags(context, this.constructor.id) + addResponseTags(context) + + context.config.hooks.request(context.span, req, res) + addResourceTag(context) + + context.span.finish() + context.finished = true } + + finishAll (context) { + for (const beforeEnd of context.beforeEnd) { + beforeEnd() + } + + this.finishMiddleware(context) + + this.finishSpan(context) + } + + obfuscateQs (config, url) { + const { queryStringObfuscation } = config + + if (queryStringObfuscation === false) return url + + const i = url.indexOf('?') + if (i === -1) return url + + const path = url.slice(0, i) + if (queryStringObfuscation === true) return path + + let qs = url.slice(i + 1) + + qs = qs.replace(queryStringObfuscation, '') + + return `${path}?${qs}` + } + + wrapWriteHead (context) { + const { req, res } = context + const writeHead = res.writeHead + + return function (statusCode, statusMessage, headers) { + headers = typeof statusMessage === 'string' ? headers : statusMessage + headers = Object.assign(res.getHeaders(), headers) + + if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) { + addAllowHeaders(req, res, headers) + } + + return writeHead.apply(this, arguments) + } + } + + getContext (req) { + return contexts.get(req) + } + + wrapRes (context, req, res, end) { + return function () { + this.finishAll(context) + + return end.apply(res, arguments) + } + } + + wrapEnd (context) { + const scope = context.tracer.scope() + const req = context.req + const res = context.res + const end = res.end + + res.writeHead = this.wrapWriteHead(context) + + ends.set(res, this.wrapRes(context, req, res, end)) + + Object.defineProperty(res, 'end', { + configurable: true, + get () { + return ends.get(this) + }, + set (value) { + ends.set(this, scope.bind(value, context.span)) + } + }) + } +} + +function addAllowHeaders (req, res, headers) { + const allowHeaders = splitHeader(headers['access-control-allow-headers']) + const requestHeaders = splitHeader(req.headers['access-control-request-headers']) + const contextHeaders = [ + 'x-datadog-origin', + 'x-datadog-parent-id', + 'x-datadog-sampled', // Deprecated, but still accept it in case it's sent. + 'x-datadog-sampling-priority', + 'x-datadog-trace-id', + 'x-datadog-tags' + ] + + for (const header of contextHeaders) { + if (requestHeaders.includes(header)) { + allowHeaders.push(header) + } + } + + if (allowHeaders.length > 0) { + res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(',')) + } +} + +function isOriginAllowed (req, headers) { + const origin = req.headers.origin + const allowOrigin = headers['access-control-allow-origin'] + + return origin && (allowOrigin === '*' || allowOrigin === origin) +} + +function splitHeader (str) { + return typeof str === 'string' ? str.split(/\s*,\s*/) : [] +} + +function reactivate (req, fn) { + const context = contexts.get(req) + + return context + ? context.tracer.scope().activate(context.span, fn) + : fn() +} + +function addRequestTags (context, spanType) { + const { req, span, inferredProxySpan, config } = context + const url = extractURL(req) + + span.addTags({ + [HTTP_URL]: this.obfuscateQs(config, url), + [HTTP_METHOD]: req.method, + [SPAN_KIND]: SERVER, + [SPAN_TYPE]: spanType, + [HTTP_USERAGENT]: req.headers['user-agent'] + }) + + // if client ip has already been set by appsec, no need to run it again + if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { + const clientIp = extractIp(config, req) + + if (clientIp) { + span.setTag(HTTP_CLIENT_IP, clientIp) + inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) + } + } + + addHeaders(context) +} + +function addResponseTags (context) { + const { req, res, paths, span, inferredProxySpan } = context + + const route = paths.join('') + if (route) { + span.setTag(HTTP_ROUTE, route) + } + + span.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + inferredProxySpan?.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + + this.addStatusError(req, res.statusCode) +} + +function addResourceTag (context) { + const { req, span } = context + const tags = span.context()._tags + + if (tags['resource.name']) return + + const resource = [req.method, tags[HTTP_ROUTE]] + .filter(Boolean) + .join(' ') + + span.setTag(RESOURCE_NAME, resource) +} + +function addHeaders (context) { + const { req, res, config, span, inferredProxySpan } = context + + config.headers.forEach(([key, tag]) => { + const reqHeader = req.headers[key] + const resHeader = res.getHeader(key) + + if (reqHeader) { + span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) + inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) + } + + if (resHeader) { + span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) + inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) + } + }) +} + +function extractURL (req) { + const headers = req.headers + + if (req.stream) { + return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}` + } + const protocol = getProtocol(req) + return `${protocol}://${req.headers.host}${req.originalUrl || req.url}` +} + +function getProtocol (req) { + if (req.socket && req.socket.encrypted) return 'https' + if (req.connection && req.connection.encrypted) return 'https' + + return 'http' +} + +function getHeadersToRecord (config) { + if (Array.isArray(config.headers)) { + try { + return config.headers + .map(h => h.split(':')) + .map(([key, tag]) => [key.toLowerCase(), tag]) + } catch (err) { + log.error('Web plugin error getting headers', err) + } + } else if (config.hasOwnProperty('headers')) { + log.error('Expected `headers` to be an array of strings.') + } + return [] +} + +function getStatusValidator (config) { + if (typeof config.validateStatus === 'function') { + return config.validateStatus + } else if (config.hasOwnProperty('validateStatus')) { + log.error('Expected `validateStatus` to be a function.') + } + return code => code < 500 +} + +const noop = () => {} + +function getHooks (config) { + const request = config.hooks?.request ?? noop + + return { request } +} + +function getMiddlewareSetting (config) { + if (config && typeof config.middleware === 'boolean') { + return config.middleware + } else if (config && config.hasOwnProperty('middleware')) { + log.error('Expected `middleware` to be a boolean.') + } + + return true +} + +function getQsObfuscator (config) { + const obfuscator = config.queryStringObfuscation + + if (typeof obfuscator === 'boolean') { + return obfuscator + } + + if (typeof obfuscator === 'string') { + if (obfuscator === '') return false // disable obfuscator + + if (obfuscator === '.*') return true // optimize full redact + + try { + return new RegExp(obfuscator, 'gi') + } catch (err) { + log.error('Web plugin error getting qs obfuscator', err) + } + } + + if (config.hasOwnProperty('queryStringObfuscation')) { + log.error('Expected `queryStringObfuscation` to be a regex string or boolean.') + } + + return true } module.exports = WebPlugin diff --git a/packages/datadog-plugin-web/test/index.spec.js b/packages/datadog-plugin-web/test/index.spec.js new file mode 100644 index 00000000000..021d67c2c01 --- /dev/null +++ b/packages/datadog-plugin-web/test/index.spec.js @@ -0,0 +1,972 @@ +'use strict' + +require('../../setup/tap') + +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') +const tags = require('../../../ext/tags') +const { USER_REJECT } = require('../../../ext/priority') +const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') + +const WEB = types.WEB +const SERVER = kinds.SERVER +const RESOURCE_NAME = tags.RESOURCE_NAME +const SERVICE_NAME = tags.SERVICE_NAME +const SPAN_TYPE = tags.SPAN_TYPE +const SPAN_KIND = tags.SPAN_KIND +const ERROR = tags.ERROR +const HTTP_METHOD = tags.HTTP_METHOD +const HTTP_URL = tags.HTTP_URL +const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE +const HTTP_ROUTE = tags.HTTP_ROUTE +const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS +const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS +const HTTP_USERAGENT = tags.HTTP_USERAGENT +const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP + +describe('plugins/util/web', () => { + let web + let tracer + let span + let req + let res + let end + let config + let tags + + beforeEach(() => { + // `req` should only have common properties exposed and not things like + // `socket` or `connection` since some libraries rely on fake objects that + // may not have those. + req = { + method: 'GET', + headers: { + host: 'localhost', + date: 'now' + } + } + end = sinon.stub() + res = { + end, + getHeader: sinon.stub(), + getHeaders: sinon.stub().returns({}), + setHeader: sinon.spy(), + writeHead: () => {} + } + res.getHeader.withArgs('server').returns('test') + config = { hooks: {} } + + tracer = require('../../..').init({ plugins: false }) + web = require('../../../src/plugins/util/web') + }) + + beforeEach(() => { + config = web.normalizeConfig(config) + }) + + describe('normalizeConfig', () => { + it('should set the correct defaults', () => { + const config = web.normalizeConfig({}) + + expect(config).to.have.property('headers') + expect(config.headers).to.be.an('array') + expect(config).to.have.property('validateStatus') + expect(config.validateStatus).to.be.a('function') + expect(config.validateStatus(200)).to.equal(true) + expect(config.validateStatus(500)).to.equal(false) + expect(config).to.have.property('hooks') + expect(config.hooks).to.be.an('object') + expect(config.hooks).to.have.property('request') + expect(config.hooks.request).to.be.a('function') + expect(config).to.have.property('queryStringObfuscation', true) + }) + + it('should use the shared config if set', () => { + const config = web.normalizeConfig({ + headers: ['test'], + validateStatus: code => false, + hooks: { + request: () => 'test' + } + }) + + expect(config.headers).to.deep.equal([['test', undefined]]) + expect(config.validateStatus(200)).to.equal(false) + expect(config).to.have.property('hooks') + expect(config.hooks.request()).to.equal('test') + }) + + describe('queryStringObfuscation', () => { + it('should keep booleans as is', () => { + const config = web.normalizeConfig({ + queryStringObfuscation: false + }) + + expect(config).to.have.property('queryStringObfuscation', false) + }) + + it('should change to false when passed empty string', () => { + const config = web.normalizeConfig({ + queryStringObfuscation: '' + }) + + expect(config).to.have.property('queryStringObfuscation', false) + }) + + it('should change to true when passed ".*"', () => { + const config = web.normalizeConfig({ + queryStringObfuscation: '.*' + }) + + expect(config).to.have.property('queryStringObfuscation', true) + }) + + it('should convert to regex when passed valid string', () => { + const config = web.normalizeConfig({ + queryStringObfuscation: 'a*' + }) + + expect(config).to.have.deep.property('queryStringObfuscation', /a*/gi) + }) + + it('should default to true when passed a bad regex', () => { + const config = web.normalizeConfig({ + queryStringObfuscation: '(?)' + }) + + expect(config).to.have.property('queryStringObfuscation', true) + }) + }) + }) + + describe('instrument', () => { + describe('on request start', () => { + it('should set the parent from the request headers', () => { + req.headers = { + 'x-datadog-trace-id': '123', + 'x-datadog-parent-id': '456' + } + + web.instrument(tracer, config, req, res, 'test.request', span => { + expect(span.context()._traceId.toString(10)).to.equal('123') + expect(span.context()._parentId.toString(10)).to.equal('456') + }) + }) + + it('should set the service name', () => { + config.service = 'custom' + + web.instrument(tracer, config, req, res, 'test.request', span => { + expect(span.context()._tags).to.have.property(SERVICE_NAME, 'custom') + }) + }) + + it('should activate a scope with the span', () => { + web.instrument(tracer, config, req, res, 'test.request', span => { + expect(tracer.scope().active()).to.equal(span) + }) + }) + + it('should add request tags to the span', () => { + req.method = 'GET' + req.url = '/user/123' + req.headers['user-agent'] = 'curl' + req.headers['x-forwarded-for'] = '8.8.8.8' + res.statusCode = '200' + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [SPAN_TYPE]: WEB, + [HTTP_URL]: 'http://localhost/user/123', + [HTTP_METHOD]: 'GET', + [SPAN_KIND]: SERVER, + [HTTP_USERAGENT]: 'curl' + }) + }) + }) + + it('should add client ip tag to the span when enabled', () => { + req.headers['x-forwarded-for'] = '8.8.8.8' + + config.clientIpEnabled = true + + web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [HTTP_CLIENT_IP]: '8.8.8.8' + }) + }) + }) + + it('should add custom client ip tag to the span when it is configured', () => { + req.headers['X-Forwad-For'] = '8.8.8.8' + + config.clientIpEnabled = true + config.clientIpHeader = 'X-Forwad-For' + + web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [HTTP_CLIENT_IP]: '8.8.8.8' + }) + }) + }) + + it('should not add custom client ip tag to the span when it is not configured', () => { + req.headers['X-Forwad-For'] = '8.8.8.8' + + config.clientIpEnabled = true + + web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.not.have.property(HTTP_CLIENT_IP) + }) + }) + + it('should not add client ip tag to the span when disabled', () => { + req.headers['x-forwarded-for'] = '8.8.8.8' + + config.clientIpEnabled = false + + web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.not.have.property(HTTP_CLIENT_IP) + }) + }) + + it('should not replace client ip when it exists', () => { + req.headers['x-forwarded-for'] = '8.8.8.8' + + config.clientIpEnabled = true + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + span.setTag(HTTP_CLIENT_IP, '1.1.1.1') + + res.end() + + expect(tags).to.include({ + [HTTP_CLIENT_IP]: '1.1.1.1' + }) + }) + }) + + it('should not add client ip tag when no candidate header is present in request', () => { + config.clientIpEnabled = true + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.not.have.property(HTTP_CLIENT_IP) + }) + }) + + it('should add configured headers to the span tags', () => { + req.headers.req = 'incoming' + req.headers.res = 'outgoing' + config.headers = ['host', 'req:http.req', 'server', 'res:http.res'] + config = web.normalizeConfig(config) + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [`${HTTP_REQUEST_HEADERS}.host`]: 'localhost', + 'http.req': 'incoming', + [`${HTTP_RESPONSE_HEADERS}.server`]: 'test', + 'http.res': 'outgoing' + }) + }) + }) + + it('should only start one span for the entire request', () => { + web.instrument(tracer, config, req, res, 'test.request', span1 => { + web.instrument(tracer, config, req, res, 'test.request', span2 => { + expect(span1).to.equal(span2) + }) + }) + }) + + it('should allow overriding the span name', () => { + web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(tracer, config, req, res, 'test2.request', span => { + expect(span.context()._name).to.equal('test2.request') + }) + }) + }) + + it('should allow overriding the span service name', () => { + web.instrument(tracer, config, req, res, 'test.request', span => { + config.service = 'test2' + web.instrument(tracer, config, req, res, 'test.request') + + expect(span.context()._tags).to.have.property('service.name', 'test2') + }) + }) + + it('should only wrap res.end once', () => { + web.instrument(tracer, config, req, res, 'test.request') + const end = res.end + web.instrument(tracer, config, req, res, 'test.request') + + expect(end).to.equal(res.end) + }) + + it('should use the config from the last call', () => { + config.headers = ['host'] + + const override = web.normalizeConfig({ + headers: ['date'] + }) + + web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(tracer, override, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [`${HTTP_REQUEST_HEADERS}.date`]: 'now' + }) + }) + }) + }) + + it('should obfuscate the query string from the URL', () => { + const config = web.normalizeConfig({ + queryStringObfuscation: 'secret=.*?(&|$)' + }) + + req.method = 'GET' + req.url = '/user/123?secret=password&foo=bar' + res.statusCode = '200' + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [HTTP_URL]: 'http://localhost/user/123?foo=bar' + }) + }) + }) + + it('should handle CORS preflight', () => { + const headers = [ + 'x-datadog-origin', + 'x-datadog-parent-id', + 'x-datadog-sampled', + 'x-datadog-sampling-priority', + 'x-datadog-trace-id', + 'x-datadog-tags' + ].join(',') + + req.method = 'OPTIONS' + req.headers.origin = 'http://test.com' + req.headers['access-control-request-headers'] = headers + + res.getHeaders.returns({ + 'access-control-allow-origin': 'http://test.com' + }) + + web.instrument(tracer, config, req, res, 'test.request') + + res.writeHead() + + expect(res.setHeader).to.have.been.calledWith('access-control-allow-headers', headers) + }) + + it('should handle CORS preflight with partial headers', () => { + const headers = [ + 'x-datadog-parent-id', + 'x-datadog-trace-id' + ].join(',') + + req.method = 'OPTIONS' + req.headers.origin = 'http://test.com' + req.headers['access-control-request-headers'] = headers + + res.getHeaders.returns({ + 'access-control-allow-origin': 'http://test.com' + }) + + web.instrument(tracer, config, req, res, 'test.request') + + res.writeHead() + + expect(res.setHeader).to.have.been.calledWith('access-control-allow-headers', headers) + }) + + it('should handle CORS preflight when the origin does not match', () => { + const headers = ['x-datadog-trace-id'] + + req.method = 'OPTIONS' + req.headers.origin = 'http://test.com' + req.headers['access-control-request-headers'] = headers + + web.instrument(tracer, config, req, res, 'test.request') + + res.writeHead() + + expect(res.setHeader).to.not.have.been.called + }) + + it('should handle CORS preflight when no header was requested', () => { + req.method = 'OPTIONS' + req.headers.origin = 'http://test.com' + + res.getHeaders.returns({ + 'access-control-allow-origin': 'http://test.com' + }) + + web.instrument(tracer, config, req, res, 'test.request') + + res.writeHead() + + expect(res.setHeader).to.not.have.been.called + }) + + it('should support https', () => { + req.url = '/user/123' + req.headers['user-agent'] = 'curl' + req.headers['x-forwarded-for'] = '8.8.8.8' + req.socket = { encrypted: true } + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [SPAN_TYPE]: WEB, + [HTTP_URL]: 'https://localhost/user/123', + [HTTP_METHOD]: 'GET', + [SPAN_KIND]: SERVER, + [HTTP_USERAGENT]: 'curl' + }) + }) + }) + + it('should support HTTP2 compatibility API', () => { + req.stream = {} + req.method = 'GET' + req.headers = { + ':scheme': 'https', + ':authority': 'localhost', + ':method': 'GET', + ':path': '/user/123', + 'user-agent': 'curl', + 'x-forwarded-for': '8.8.8.8' + } + res.statusCode = '200' + + web.instrument(tracer, config, req, res, 'test.request', span => { + const tags = span.context()._tags + + res.end() + + expect(tags).to.include({ + [SPAN_TYPE]: WEB, + [HTTP_URL]: 'https://localhost/user/123', + [HTTP_METHOD]: 'GET', + [SPAN_KIND]: SERVER, + [HTTP_USERAGENT]: 'curl' + }) + }) + }) + + it('should drop filtered out requests', () => { + config.filter = () => false + + web.instrument(tracer, config, req, res, 'test.request', span => { + const sampling = span.context()._sampling + + res.end() + + expect(sampling).to.have.property('priority', USER_REJECT) + }) + }) + }) + + describe('on request end', () => { + beforeEach(() => { + web.instrument(tracer, config, req, res, 'test.request', reqSpan => { + span = reqSpan + tags = span.context()._tags + }) + }) + + it('should finish the request span', () => { + sinon.spy(span, 'finish') + + res.end() + + expect(span.finish).to.have.been.called + }) + + it('should should only finish once', () => { + sinon.spy(span, 'finish') + + res.end() + res.end() + + expect(span.finish).to.have.been.calledOnce + }) + + it('should finish middleware spans', () => { + web.wrapMiddleware(req, () => {}, 'middleware', () => { + const span = tracer.scope().active() + + sinon.spy(span, 'finish') + + res.end() + + expect(span.finish).to.have.been.called + }) + }) + + it('should execute any beforeEnd handlers', () => { + const spy1 = sinon.spy() + const spy2 = sinon.spy() + + web.beforeEnd(req, spy1) + web.beforeEnd(req, spy2) + + res.end() + + expect(spy1).to.have.been.called + expect(spy2).to.have.been.called + }) + + it('should call the original end', () => { + res.end() + + expect(end).to.have.been.called + }) + + it('should add response tags to the span', () => { + req.method = 'GET' + req.url = '/user/123' + res.statusCode = 200 + + res.end() + + expect(tags).to.include({ + [RESOURCE_NAME]: 'GET', + [HTTP_STATUS_CODE]: 200 + }) + }) + + it('should set the error tag if the request is an error', () => { + res.statusCode = 500 + + res.end() + + expect(tags).to.include({ + [ERROR]: true + }) + }) + + it('should set the error tag if the configured validator returns false', () => { + config.validateStatus = () => false + + res.end() + + expect(tags).to.include({ + [ERROR]: true + }) + }) + + it('should use the user provided route', () => { + span.setTag('http.route', '/custom/route') + + res.end() + + expect(tags).to.include({ + [HTTP_ROUTE]: '/custom/route' + }) + }) + + it('should execute the request end hook', () => { + config.hooks.request = sinon.spy() + + res.end() + + expect(config.hooks.request).to.have.been.calledWith(span, req, res) + }) + + it('should execute multiple end hooks', () => { + config.hooks = { + request: sinon.spy() + } + + web.instrument(tracer, config, req, res, 'test.request', span => { + res.end() + + expect(config.hooks.request).to.have.been.calledWith(span, req, res) + }) + }) + + it('should set the resource name from the http.route tag set in the hooks', () => { + config.hooks = { + request: span => span.setTag('http.route', '/custom/route') + } + + web.instrument(tracer, config, req, res, 'test.request', span => { + res.end() + + expect(tags).to.have.property('resource.name', 'GET /custom/route') + }) + }) + }) + }) + + describe('enterRoute', () => { + beforeEach(() => { + config = web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', () => { + span = tracer.scope().active() + tags = span.context()._tags + }) + }) + + it('should add a route segment that will be added to the span resource name', () => { + req.method = 'GET' + + web.enterRoute(req, '/foo') + web.enterRoute(req, '/bar') + res.end() + + expect(tags).to.have.property(RESOURCE_NAME, 'GET /foo/bar') + expect(tags).to.have.property(HTTP_ROUTE, '/foo/bar') + }) + + it('should only add valid route segments to the span resource name', () => { + req.method = 'GET' + + web.enterRoute(req) + web.enterRoute(req, 1337) + res.end() + + expect(tags).to.have.property(RESOURCE_NAME, 'GET') + expect(tags).to.not.have.property(HTTP_ROUTE) + }) + }) + + describe('exitRoute', () => { + beforeEach(() => { + config = web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', reqSpan => { + span = reqSpan + tags = span.context()._tags + }) + }) + + it('should remove a route segment', () => { + req.method = 'GET' + + web.enterRoute(req, '/foo') + web.enterRoute(req, '/bar') + web.exitRoute(req) + res.end() + + expect(tags).to.have.property(RESOURCE_NAME, 'GET /foo') + }) + }) + + describe('wrapMiddleware', () => { + beforeEach(() => { + config = web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', () => { + span = tracer.scope().active() + tags = span.context()._tags + }) + }) + + it('should activate a scope with the span', (done) => { + const fn = function test () { + expect(tracer.scope().active()).to.not.equal(span) + done() + } + + web.wrapMiddleware(req, fn, 'middleware', () => fn(req, res)) + }) + }) + + describe('finish', () => { + beforeEach(() => { + config = web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', () => { + span = tracer.scope().active() + tags = span.context()._tags + }) + }) + + it('should finish the span of the current middleware', (done) => { + const fn = () => { + const span = tracer.scope().active() + + sinon.spy(span, 'finish') + web.finish(req, fn, 'middleware') + + expect(span.finish).to.have.been.called + + done() + } + + web.wrapMiddleware(req, fn, 'middleware', () => fn(req, res)) + }) + + it('should add an error if provided', (done) => { + const fn = () => { + const span = tracer.scope().active() + const tags = span.context()._tags + const error = new Error('boom') + + sinon.spy(span, 'finish') + web.finish(req, error) + + expect(tags[ERROR_TYPE]).to.equal(error.name) + expect(tags[ERROR_MESSAGE]).to.equal(error.message) + expect(tags[ERROR_STACK]).to.equal(error.stack) + + done() + } + + web.wrapMiddleware(req, fn, 'middleware', () => fn(req, res)) + }) + }) + + describe('root', () => { + it('should return the request root span', () => { + web.instrument(tracer, config, req, res, 'test.request', () => { + const span = tracer.scope().active() + + web.wrapMiddleware(req, () => {}, 'express.middleware', () => { + expect(web.root(req)).to.equal(span) + }) + }) + }) + + it('should return null when not yet instrumented', () => { + expect(web.root(req)).to.be.null + }) + }) + + describe('active', () => { + it('should return the request span by default', () => { + web.instrument(tracer, config, req, res, 'test.request', () => { + expect(web.active(req)).to.equal(tracer.scope().active()) + }) + }) + + it('should return the active middleware span', () => { + web.instrument(tracer, config, req, res, 'test.request', () => { + const span = tracer.scope().active() + + web.wrapMiddleware(req, () => {}, 'express.middleware', () => { + expect(web.active(req)).to.not.be.null + expect(web.active(req)).to.not.equal(span) + }) + }) + }) + + it('should return null when not yet instrumented', () => { + expect(web.active(req)).to.be.null + }) + }) + + describe('addError', () => { + beforeEach(() => { + config = web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', () => { + span = tracer.scope().active() + tags = span.context()._tags + }) + }) + + it('should add an error to the request span', () => { + const error = new Error('boom') + + web.addError(req, error) + web.addStatusError(req, 500) + + expect(tags).to.include({ + [ERROR]: error + }) + }) + + it('should override an existing error', () => { + const error = new Error('boom') + + web.addError(req, new Error('prrr')) + web.addError(req, error) + web.addStatusError(req, 500) + + expect(tags).to.include({ + [ERROR]: error + }) + }) + }) + + describe('addStatusError', () => { + beforeEach(() => { + config = web.normalizeConfig(config) + web.instrument(tracer, config, req, res, 'test.request', () => { + span = tracer.scope().active() + tags = span.context()._tags + }) + }) + + it('should flag the request as an error', () => { + web.addStatusError(req, 500) + + expect(tags).to.include({ + [ERROR]: true + }) + }) + + it('should only flag requests as an error for configured status codes', () => { + config.validateStatus = () => true + + web.addStatusError(req, 500) + + expect(tags).to.not.have.property(ERROR) + }) + }) + + describe('allowlistFilter', () => { + beforeEach(() => { + config = { allowlist: ['/_okay'] } + config = web.normalizeConfig(config) + }) + + it('should not filter the url', () => { + const filtered = config.filter('/_okay') + expect(filtered).to.equal(true) + }) + + it('should filter the url', () => { + const filtered = config.filter('/_notokay') + expect(filtered).to.equal(false) + }) + }) + + describe('whitelistFilter', () => { + beforeEach(() => { + config = { whitelist: ['/_okay'] } + config = web.normalizeConfig(config) + }) + + it('should not filter the url', () => { + const filtered = config.filter('/_okay') + expect(filtered).to.equal(true) + }) + + it('should filter the url', () => { + const filtered = config.filter('/_notokay') + expect(filtered).to.equal(false) + }) + }) + + describe('blocklistFilter', () => { + beforeEach(() => { + config = { blocklist: ['/_notokay'] } + config = web.normalizeConfig(config) + }) + + it('should not filter the url', () => { + const filtered = config.filter('/_okay') + expect(filtered).to.equal(true) + }) + + it('should filter the url', () => { + const filtered = config.filter('/_notokay') + expect(filtered).to.equal(false) + }) + }) + + describe('blacklistFilter', () => { + beforeEach(() => { + config = { blacklist: ['/_notokay'] } + config = web.normalizeConfig(config) + }) + + it('should not filter the url', () => { + const filtered = config.filter('/_okay') + expect(filtered).to.equal(true) + }) + + it('should filter the url', () => { + const filtered = config.filter('/_notokay') + expect(filtered).to.equal(false) + }) + }) + + describe('obfuscateQs', () => { + const url = 'http://perdu.com/path/' + const qs = '?data=secret' + + let config + + beforeEach(() => { + config = { + queryStringObfuscation: /secret/gi + } + }) + + it('should not obfuscate when passed false', () => { + config.queryStringObfuscation = false + + const result = web.obfuscateQs(config, url + qs) + + expect(result).to.equal(url + qs) + }) + + it('should not obfuscate when no querystring is found', () => { + const result = web.obfuscateQs(config, url) + + expect(result).to.equal(url) + }) + + it('should remove the querystring if passed true', () => { + config.queryStringObfuscation = true + + const result = web.obfuscateQs(config, url + qs) + + expect(result).to.equal(url) + }) + + it('should obfuscate only the querystring part of the url', () => { + const result = web.obfuscateQs(config, url + 'secret/' + qs) + + expect(result).to.equal(url + 'secret/?data=') + }) + }) +}) diff --git a/packages/dd-trace/src/plugins/util/inferred_proxy.js b/packages/dd-trace/src/plugins/util/inferred_proxy.js deleted file mode 100644 index f837608ab97..00000000000 --- a/packages/dd-trace/src/plugins/util/inferred_proxy.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict' - -const log = require('../../log') -const tags = require('../../../../../ext/tags') - -const RESOURCE_NAME = tags.RESOURCE_NAME -const SPAN_TYPE = tags.SPAN_TYPE -const HTTP_URL = tags.HTTP_URL -const HTTP_METHOD = tags.HTTP_METHOD - -const PROXY_HEADER_SYSTEM = 'x-dd-proxy' -const PROXY_HEADER_START_TIME_MS = 'x-dd-proxy-request-time-ms' -const PROXY_HEADER_PATH = 'x-dd-proxy-path' -const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod' -const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name' -const PROXY_HEADER_STAGE = 'x-dd-proxy-stage' - -const supportedProxies = { - 'aws-apigateway': { - spanName: 'aws.apigateway', - component: 'aws-apigateway' - } -} - -function createInferredProxySpan (headers, childOf, tracer, reqCtx, traceCtx, config, startSpanHelper) { - if (!headers) { - return null - } - - if (!tracer._config?.inferredProxyServicesEnabled) { - return null - } - - const proxyContext = extractInferredProxyContext(headers) - - if (!proxyContext) { - return null - } - - const proxySpanInfo = supportedProxies[proxyContext.proxySystemName] - - log.debug('Successfully extracted inferred span info %s for proxy:', proxyContext, proxyContext.proxySystemName) - - const span = startSpanHelper(tracer, proxySpanInfo.spanName, { - childOf, - type: 'web', - startTime: proxyContext.requestTime, - integrationName: proxySpanInfo.component, - meta: { - service: proxyContext.domainName || tracer._config.service, - component: proxySpanInfo.component, - [SPAN_TYPE]: 'web', - [HTTP_METHOD]: proxyContext.method, - [HTTP_URL]: proxyContext.domainName + proxyContext.path, - stage: proxyContext.stage - } - }, traceCtx, config) - - reqCtx.inferredProxySpan = span - childOf = span - - log.debug('Successfully created inferred proxy span.') - - setInferredProxySpanTags(span, proxyContext) - - return childOf -} - -function setInferredProxySpanTags (span, proxyContext) { - span.setTag(RESOURCE_NAME, `${proxyContext.method} ${proxyContext.path}`) - span.setTag('_dd.inferred_span', 1) - return span -} - -function extractInferredProxyContext (headers) { - if (!(PROXY_HEADER_START_TIME_MS in headers)) { - return null - } - - if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in supportedProxies)) { - log.debug('Received headers to create inferred proxy span but headers include an unsupported proxy type', headers) - return null - } - - return { - requestTime: headers[PROXY_HEADER_START_TIME_MS] - ? Number.parseInt(headers[PROXY_HEADER_START_TIME_MS], 10) - : null, - method: headers[PROXY_HEADER_HTTPMETHOD], - path: headers[PROXY_HEADER_PATH], - stage: headers[PROXY_HEADER_STAGE], - domainName: headers[PROXY_HEADER_DOMAIN], - proxySystemName: headers[PROXY_HEADER_SYSTEM] - } -} - -function finishInferredProxySpan (context) { - const { req } = context - - if (!context.inferredProxySpan) return - - if (context.inferredProxySpanFinished && !req.stream) return - - // context.config.hooks.request(context.inferredProxySpan, req, res) # TODO: Do we need this?? - - // Only close the inferred span if one was created - if (context.inferredProxySpan) { - context.inferredProxySpan.finish() - context.inferredProxySpanFinished = true - } -} - -module.exports = { - createInferredProxySpan, - finishInferredProxySpan -} diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js deleted file mode 100644 index 690b5cf54f3..00000000000 --- a/packages/dd-trace/src/plugins/util/web.js +++ /dev/null @@ -1,633 +0,0 @@ -'use strict' - -const uniq = require('../../../../datadog-core/src/utils/src/uniq') -const analyticsSampler = require('../../analytics_sampler') -const FORMAT_HTTP_HEADERS = 'http_headers' -const log = require('../../log') -const tags = require('../../../../../ext/tags') -const types = require('../../../../../ext/types') -const kinds = require('../../../../../ext/kinds') -const urlFilter = require('./urlfilter') -const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../constants') -const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy') -const TracingPlugin = require('../tracing') - -let extractIp - -const WEB = types.WEB -const SERVER = kinds.SERVER -const RESOURCE_NAME = tags.RESOURCE_NAME -const SERVICE_NAME = tags.SERVICE_NAME -const SPAN_TYPE = tags.SPAN_TYPE -const SPAN_KIND = tags.SPAN_KIND -const ERROR = tags.ERROR -const HTTP_METHOD = tags.HTTP_METHOD -const HTTP_URL = tags.HTTP_URL -const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE -const HTTP_ROUTE = tags.HTTP_ROUTE -const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS -const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS -const HTTP_USERAGENT = tags.HTTP_USERAGENT -const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP -const MANUAL_DROP = tags.MANUAL_DROP - -const HTTP2_HEADER_AUTHORITY = ':authority' -const HTTP2_HEADER_SCHEME = ':scheme' -const HTTP2_HEADER_PATH = ':path' - -const contexts = new WeakMap() -const ends = new WeakMap() - -// TODO: change this to no longer rely on creating a dummy plugin to be able to access startSpan -function createWebPlugin (tracer, config = {}) { - const plugin = new TracingPlugin(tracer, tracer._config) - plugin.component = 'web' - plugin.config = config - return plugin -} - -function startSpanHelper (tracer, name, options, traceCtx, config = {}) { - if (!web.plugin) { - web.plugin = createWebPlugin(tracer, config) - } - - return web.plugin.startSpan(name, { ...options, tracer, config }, traceCtx) -} - -const web = { - TYPE: WEB, - plugin: null, - - // Ensure the configuration has the correct structure and defaults. - normalizeConfig (config) { - const headers = getHeadersToRecord(config) - const validateStatus = getStatusValidator(config) - const hooks = getHooks(config) - const filter = urlFilter.getFilter(config) - const middleware = getMiddlewareSetting(config) - const queryStringObfuscation = getQsObfuscator(config) - - extractIp = config.clientIpEnabled && require('./ip_extractor').extractIp - - return { - ...config, - headers, - validateStatus, - hooks, - filter, - middleware, - queryStringObfuscation - } - }, - - setFramework (req, name, config) { - const context = this.patch(req) - const span = context.span - - if (!span) return - - span.context()._name = `${name}.request` - span.context()._tags.component = name - span._integrationName = name - - web.setConfig(req, config) - }, - - setConfig (req, config) { - const context = contexts.get(req) - const span = context.span - - context.config = config - - if (!config.filter(req.url)) { - span.setTag(MANUAL_DROP, true) - span.context()._trace.isRecording = false - } - - if (config.service) { - span.setTag(SERVICE_NAME, config.service) - } - - analyticsSampler.sample(span, config.measured, true) - }, - - startSpan (tracer, config, req, res, name, traceCtx) { - const context = this.patch(req) - - let span - - if (context.span) { - context.span.context()._name = name - span = context.span - } else { - span = web.startChildSpan(tracer, config, name, req, traceCtx) - } - - context.tracer = tracer - context.span = span - context.res = res - - this.setConfig(req, config) - addRequestTags(context, this.TYPE) - - return span - }, - wrap (req) { - const context = contexts.get(req) - if (!context.instrumented) { - this.wrapEnd(context) - context.instrumented = true - } - }, - // Start a span and activate a scope for a request. - instrument (tracer, config, req, res, name, callback) { - const span = this.startSpan(tracer, config, req, res, name) - - this.wrap(req) - - return callback && tracer.scope().activate(span, () => callback(span)) - }, - - // Reactivate the request scope in case it was changed by a middleware. - reactivate (req, fn) { - return reactivate(req, fn) - }, - - // Add a route segment that will be used for the resource name. - enterRoute (req, path) { - if (typeof path === 'string') { - contexts.get(req).paths.push(path) - } - }, - - setRoute (req, path) { - const context = contexts.get(req) - - if (!context) return - - context.paths = [path] - }, - - // Remove the current route segment. - exitRoute (req) { - contexts.get(req).paths.pop() - }, - - // Start a new middleware span and activate a new scope with the span. - wrapMiddleware (req, middleware, name, fn) { - if (!this.active(req)) return fn() - - const context = contexts.get(req) - const tracer = context.tracer - const childOf = this.active(req) - const config = context.config - const traceCtx = context.traceCtx - - if (config.middleware === false) return this.bindAndWrapMiddlewareErrors(fn, req, tracer, childOf) - - const span = startSpanHelper(tracer, name, { childOf }, traceCtx, config) - - analyticsSampler.sample(span, config.measured) - - span.addTags({ - [RESOURCE_NAME]: middleware._name || middleware.name || '' - }) - - context.middleware.push(span) - - return tracer.scope().activate(span, fn) - }, - - // catch errors and apply to active span - bindAndWrapMiddlewareErrors (fn, req, tracer, activeSpan) { - try { - return tracer.scope().bind(fn, activeSpan).apply(this, arguments) - } catch (e) { - web.addError(req, e) // TODO: remove when error formatting is moved to Span - throw e - } - }, - - // Finish the active middleware span. - finish (req, error) { - if (!this.active(req)) return - - const context = contexts.get(req) - const span = context.middleware.pop() - - if (span) { - if (error) { - span.addTags({ - [ERROR_TYPE]: error.name, - [ERROR_MESSAGE]: error.message, - [ERROR_STACK]: error.stack - }) - } - - span.finish() - } - }, - - // Register a callback to run before res.end() is called. - beforeEnd (req, callback) { - contexts.get(req).beforeEnd.push(callback) - }, - - // Prepare the request for instrumentation. - patch (req) { - let context = contexts.get(req) - - if (context) return context - - context = req.stream && contexts.get(req.stream) - - if (context) { - contexts.set(req, context) - return context - } - - context = { - req, - span: null, - paths: [], - middleware: [], - beforeEnd: [], - config: {} - } - - contexts.set(req, context) - - return context - }, - - // Return the request root span. - root (req) { - const context = contexts.get(req) - return context ? context.span : null - }, - - // Return the active span. - active (req) { - const context = contexts.get(req) - - if (!context) return null - if (context.middleware.length === 0) return context.span || null - - return context.middleware.at(-1) - }, - - // Extract the parent span from the headers and start a new span as its child - startChildSpan (tracer, config, name, req, traceCtx) { - const headers = req.headers - const reqCtx = contexts.get(req) - let childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers) - - // we may have headers signaling a router proxy span should be created (such as for AWS API Gateway) - if (tracer._config?.inferredProxyServicesEnabled) { - const proxySpan = createInferredProxySpan(headers, childOf, tracer, reqCtx, traceCtx, config, startSpanHelper) - if (proxySpan) { - childOf = proxySpan - } - } - - const span = startSpanHelper(tracer, name, { childOf }, traceCtx, config) - - return span - }, - - // Validate a request's status code and then add error tags if necessary - addStatusError (req, statusCode) { - const context = contexts.get(req) - const { span, inferredProxySpan, error } = context - - const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] - const inferredSpanContext = inferredProxySpan?.context() - const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] - - const isValidStatusCode = context.config.validateStatus(statusCode) - - if (!spanHasExistingError && !isValidStatusCode) { - span.setTag(ERROR, error || true) - } - - if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { - inferredProxySpan.setTag(ERROR, error || true) - } - }, - - // Add an error to the request - addError (req, error) { - if (error instanceof Error) { - const context = contexts.get(req) - - if (context) { - context.error = error - } - } - }, - - finishMiddleware (context) { - if (context.finished) return - - let span - - while ((span = context.middleware.pop())) { - span.finish() - } - }, - - finishSpan (context) { - const { req, res } = context - - if (context.finished && !req.stream) return - - addRequestTags(context, this.TYPE) - addResponseTags(context) - - context.config.hooks.request(context.span, req, res) - addResourceTag(context) - - context.span.finish() - context.finished = true - }, - - finishAll (context) { - for (const beforeEnd of context.beforeEnd) { - beforeEnd() - } - - web.finishMiddleware(context) - - web.finishSpan(context) - - finishInferredProxySpan(context) - }, - - obfuscateQs (config, url) { - const { queryStringObfuscation } = config - - if (queryStringObfuscation === false) return url - - const i = url.indexOf('?') - if (i === -1) return url - - const path = url.slice(0, i) - if (queryStringObfuscation === true) return path - - let qs = url.slice(i + 1) - - qs = qs.replace(queryStringObfuscation, '') - - return `${path}?${qs}` - }, - - wrapWriteHead (context) { - const { req, res } = context - const writeHead = res.writeHead - - return function (statusCode, statusMessage, headers) { - headers = typeof statusMessage === 'string' ? headers : statusMessage - headers = Object.assign(res.getHeaders(), headers) - - if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) { - addAllowHeaders(req, res, headers) - } - - return writeHead.apply(this, arguments) - } - }, - getContext (req) { - return contexts.get(req) - }, - wrapRes (context, req, res, end) { - return function () { - web.finishAll(context) - - return end.apply(res, arguments) - } - }, - wrapEnd (context) { - const scope = context.tracer.scope() - const req = context.req - const res = context.res - const end = res.end - - res.writeHead = web.wrapWriteHead(context) - - ends.set(res, this.wrapRes(context, req, res, end)) - - Object.defineProperty(res, 'end', { - configurable: true, - get () { - return ends.get(this) - }, - set (value) { - ends.set(this, scope.bind(value, context.span)) - } - }) - } -} - -function addAllowHeaders (req, res, headers) { - const allowHeaders = splitHeader(headers['access-control-allow-headers']) - const requestHeaders = splitHeader(req.headers['access-control-request-headers']) - const contextHeaders = [ - 'x-datadog-origin', - 'x-datadog-parent-id', - 'x-datadog-sampled', // Deprecated, but still accept it in case it's sent. - 'x-datadog-sampling-priority', - 'x-datadog-trace-id', - 'x-datadog-tags' - ] - - for (const header of contextHeaders) { - if (requestHeaders.includes(header)) { - allowHeaders.push(header) - } - } - - if (allowHeaders.length > 0) { - res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(',')) - } -} - -function isOriginAllowed (req, headers) { - const origin = req.headers.origin - const allowOrigin = headers['access-control-allow-origin'] - - return origin && (allowOrigin === '*' || allowOrigin === origin) -} - -function splitHeader (str) { - return typeof str === 'string' ? str.split(/\s*,\s*/) : [] -} - -function reactivate (req, fn) { - const context = contexts.get(req) - - return context - ? context.tracer.scope().activate(context.span, fn) - : fn() -} - -function addRequestTags (context, spanType) { - const { req, span, inferredProxySpan, config } = context - const url = extractURL(req) - - span.addTags({ - [HTTP_URL]: web.obfuscateQs(config, url), - [HTTP_METHOD]: req.method, - [SPAN_KIND]: SERVER, - [SPAN_TYPE]: spanType, - [HTTP_USERAGENT]: req.headers['user-agent'] - }) - - // if client ip has already been set by appsec, no need to run it again - if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { - const clientIp = extractIp(config, req) - - if (clientIp) { - span.setTag(HTTP_CLIENT_IP, clientIp) - inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) - } - } - - addHeaders(context) -} - -function addResponseTags (context) { - const { req, res, paths, span, inferredProxySpan } = context - - const route = paths.join('') - if (route) { - span.setTag(HTTP_ROUTE, route) - } - - span.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - inferredProxySpan?.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - - web.addStatusError(req, res.statusCode) -} - -function addResourceTag (context) { - const { req, span } = context - const tags = span.context()._tags - - if (tags['resource.name']) return - - const resource = [req.method, tags[HTTP_ROUTE]] - .filter(Boolean) - .join(' ') - - span.setTag(RESOURCE_NAME, resource) -} - -function addHeaders (context) { - const { req, res, config, span, inferredProxySpan } = context - - config.headers.forEach(([key, tag]) => { - const reqHeader = req.headers[key] - const resHeader = res.getHeader(key) - - if (reqHeader) { - span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) - inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) - } - - if (resHeader) { - span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) - inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) - } - }) -} - -function extractURL (req) { - const headers = req.headers - - if (req.stream) { - return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}` - } - const protocol = getProtocol(req) - return `${protocol}://${req.headers.host}${req.originalUrl || req.url}` -} - -function getProtocol (req) { - if (req.socket && req.socket.encrypted) return 'https' - if (req.connection && req.connection.encrypted) return 'https' - - return 'http' -} - -function getHeadersToRecord (config) { - if (Array.isArray(config.headers)) { - try { - return config.headers - .map(h => h.split(':')) - .map(([key, tag]) => [key.toLowerCase(), tag]) - } catch (err) { - log.error('Web plugin error getting headers', err) - } - } else if (config.hasOwnProperty('headers')) { - log.error('Expected `headers` to be an array of strings.') - } - return [] -} - -function getStatusValidator (config) { - if (typeof config.validateStatus === 'function') { - return config.validateStatus - } else if (config.hasOwnProperty('validateStatus')) { - log.error('Expected `validateStatus` to be a function.') - } - return code => code < 500 -} - -const noop = () => {} - -function getHooks (config) { - const request = config.hooks?.request ?? noop - - return { request } -} - -function getMiddlewareSetting (config) { - if (config && typeof config.middleware === 'boolean') { - return config.middleware - } else if (config && config.hasOwnProperty('middleware')) { - log.error('Expected `middleware` to be a boolean.') - } - - return true -} - -function getQsObfuscator (config) { - const obfuscator = config.queryStringObfuscation - - if (typeof obfuscator === 'boolean') { - return obfuscator - } - - if (typeof obfuscator === 'string') { - if (obfuscator === '') return false // disable obfuscator - - if (obfuscator === '.*') return true // optimize full redact - - try { - return new RegExp(obfuscator, 'gi') - } catch (err) { - log.error('Web plugin error getting qs obfuscator', err) - } - } - - if (config.hasOwnProperty('queryStringObfuscation')) { - log.error('Expected `queryStringObfuscation` to be a regex string or boolean.') - } - - return true -} - -module.exports = web From 2592ffbbb5e5c86d325364dee86245dd9267cefa Mon Sep 17 00:00:00 2001 From: William Conti Date: Fri, 8 Aug 2025 11:34:25 -0400 Subject: [PATCH 04/28] changes --- .../test/index.spec.js | 297 ++++++++++++++++++ packages/datadog-plugin-http/src/server.js | 12 +- packages/datadog-plugin-web/src/index.js | 128 ++++---- .../datadog-plugin-web/test/index.spec.js | 5 +- .../src/appsec/api_security_sampler.js | 6 +- packages/dd-trace/src/appsec/graphql.js | 4 +- packages/dd-trace/src/appsec/iast/index.js | 6 +- .../src/appsec/iast/overhead-controller.js | 4 +- packages/dd-trace/src/appsec/index.js | 20 +- packages/dd-trace/src/appsec/rasp/index.js | 4 +- packages/dd-trace/src/appsec/rasp/utils.js | 4 +- packages/dd-trace/src/appsec/reporter.js | 10 +- packages/dd-trace/src/appsec/waf/index.js | 4 +- packages/dd-trace/src/plugins/tracing.js | 13 - 14 files changed, 404 insertions(+), 113 deletions(-) create mode 100644 packages/datadog-plugin-aws-apigateway/test/index.spec.js diff --git a/packages/datadog-plugin-aws-apigateway/test/index.spec.js b/packages/datadog-plugin-aws-apigateway/test/index.spec.js new file mode 100644 index 00000000000..0ebd0bf9446 --- /dev/null +++ b/packages/datadog-plugin-aws-apigateway/test/index.spec.js @@ -0,0 +1,297 @@ +'use strict' + +const agent = require('../../dd-trace/test/plugins/agent') +const { expect } = require('chai') +const axios = require('axios') +const { Agent } = require('http') + +// Create axios instance with no connection pooling +const httpClient = axios.create({ + httpAgent: new Agent({ keepAlive: false }), + timeout: 5000 +}) + +describe('Plugin', function () { + let http + let appListener + let controller + let port + + const inferredHeaders = { + 'x-dd-proxy': 'aws-apigateway', + 'x-dd-proxy-request-time-ms': '1729780025473', + 'x-dd-proxy-path': '/test', + 'x-dd-proxy-httpmethod': 'GET', + 'x-dd-proxy-domain-name': 'example.com', + 'x-dd-proxy-stage': 'dev' + } + + afterEach(async () => { + controller = null + + if (appListener) { + // Force close all existing connections + if (appListener._connections) { + for (const connection of appListener._connections) { + connection.destroy() + } + } + + await new Promise((resolve, reject) => { + appListener.close((err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + appListener = null + } + + await agent.close() + }) + + describe('without configuration', () => { + beforeEach(async () => { + const options = { + inferredProxyServicesEnabled: true, + service: 'aws-server' + } + + require('../../dd-trace').init(options) + + await agent.load( + ['http', 'dns', 'net', 'aws-apigateway'], + [{ client: false }, { enabled: false }, { enabled: false }, { enabled: true }], + options + ) + + http = require('http') + + const server = new http.Server(async (req, res) => { + controller && await controller(req, res) + if (req.url === '/error') { + res.statusCode = 500 + res.end(JSON.stringify({ message: 'ERROR' })) + } else { + res.writeHead(200) + res.end(JSON.stringify({ message: 'OK' })) + } + }) + + // Force close connections when server closes + const connections = new Set() + server.on('connection', (connection) => { + connections.add(connection) + connection.on('close', () => { + connections.delete(connection) + }) + }) + + return new Promise((resolve, reject) => { + appListener = server.listen(0, '127.0.0.1', () => { + port = server.address().port + appListener._connections = connections + resolve() + }) + }) + }) + + it('should create a parent span and a child span for a 200', async () => { + await httpClient.get(`http://127.0.0.1:${port}/`, { + headers: inferredHeaders + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + + expect(spans.length).to.be.equal(2) + + expect(spans[0]).to.have.property('name', 'aws.apigateway') + expect(spans[0]).to.have.property('service', 'example.com') + expect(spans[0]).to.have.property('resource', 'GET /test') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0].meta).to.have.property('http.url', 'example.com/test') + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('component', 'aws-apigateway') + expect(spans[0].meta).to.have.property('_dd.integration', 'aws-apigateway') + expect(spans[0].metrics).to.have.property('_dd.inferred_span', 1) + expect(spans[0].start.toString()).to.be.equal('1729780025472999936') + + expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) + + expect(spans[1]).to.have.property('name', 'web.request') + expect(spans[1]).to.have.property('service', 'aws-server') + expect(spans[1]).to.have.property('type', 'web') + expect(spans[1]).to.have.property('resource', 'GET') + expect(spans[1].meta).to.have.property('component', 'http') + expect(spans[1].meta).to.have.property('span.kind', 'server') + expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/`) + expect(spans[1].meta).to.have.property('http.method', 'GET') + expect(spans[1].meta).to.have.property('http.status_code', '200') + expect(spans[1].meta).to.have.property('span.kind', 'server') + }) + }) + + it('should create a parent span and a child span for an error', async () => { + await httpClient.get(`http://127.0.0.1:${port}/error`, { + headers: inferredHeaders, + validateStatus: function (status) { + return status === 500 + } + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + expect(spans.length).to.be.equal(2) + + expect(spans[0]).to.have.property('name', 'aws.apigateway') + expect(spans[0]).to.have.property('service', 'example.com') + expect(spans[0]).to.have.property('resource', 'GET /test') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0].meta).to.have.property('http.url', 'example.com/test') + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '500') + expect(spans[0].meta).to.have.property('component', 'aws-apigateway') + expect(spans[0].error).to.be.equal(1) + expect(spans[0].start.toString()).to.be.equal('1729780025472999936') + expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) + + expect(spans[1]).to.have.property('name', 'web.request') + expect(spans[1]).to.have.property('service', 'aws-server') + expect(spans[1]).to.have.property('type', 'web') + expect(spans[1]).to.have.property('resource', 'GET') + expect(spans[1].meta).to.have.property('component', 'http') + expect(spans[1].meta).to.have.property('span.kind', 'server') + expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/error`) + expect(spans[1].meta).to.have.property('http.method', 'GET') + expect(spans[1].meta).to.have.property('http.status_code', '500') + expect(spans[1].meta).to.have.property('span.kind', 'server') + expect(spans[1].error).to.be.equal(1) + }) + }) + + it('should not create an API Gateway span if all necessary headers are missing', async () => { + await httpClient.get(`http://127.0.0.1:${port}/no-aws-headers`, { + headers: {} + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + expect(spans.length).to.be.equal(1) + + expect(spans[0]).to.have.property('name', 'web.request') + expect(spans[0]).to.have.property('service', 'aws-server') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0]).to.have.property('resource', 'GET') + expect(spans[0].meta).to.have.property('component', 'http') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/no-aws-headers`) + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].error).to.be.equal(0) + }) + }) + + it('should not create an API Gateway span if missing the proxy system header', async () => { + // remove x-dd-proxy from headers + const { 'x-dd-proxy': _, ...newHeaders } = inferredHeaders + + await httpClient.get(`http://127.0.0.1:${port}/a-few-aws-headers`, { + headers: newHeaders + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + expect(spans.length).to.be.equal(1) + + expect(spans[0]).to.have.property('name', 'web.request') + expect(spans[0]).to.have.property('service', 'aws-server') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0]).to.have.property('resource', 'GET') + expect(spans[0].meta).to.have.property('component', 'http') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/a-few-aws-headers`) + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].error).to.be.equal(0) + }) + }) + }) + + describe('with configuration', function () { + beforeEach(async () => { + const options = { + inferredProxyServicesEnabled: false, + service: 'aws-server' + } + + // we can't force re-init the tracer, so we have to set the config manually + const tracer = require('../../dd-trace').init(options) + tracer._config.inferredProxyServicesEnabled = false + + await agent.load( + ['http', 'dns', 'net', 'aws-apigateway'], + [{ client: false }, { enabled: false }, { enabled: false }, { enabled: true }], + options + ) + + http = require('http') + + const server = new http.Server(async (req, res) => { + controller && await controller(req, res) + if (req.url === '/error') { + res.statusCode = 500 + res.end(JSON.stringify({ message: 'ERROR' })) + } else { + res.writeHead(200) + res.end(JSON.stringify({ message: 'OK' })) + } + }) + + // Force close connections when server closes + const connections = new Set() + server.on('connection', (connection) => { + connections.add(connection) + connection.on('close', () => { + connections.delete(connection) + }) + }) + + return new Promise((resolve, reject) => { + appListener = server.listen(0, '127.0.0.1', () => { + port = server.address().port + appListener._connections = connections + resolve() + }) + }) + }) + + it('should not create a span when configured to be off', async () => { + await httpClient.get(`http://127.0.0.1:${port}/configured-off`, { + headers: inferredHeaders + }) + + await agent.assertSomeTraces(traces => { + const spans = traces[0] + + expect(spans.length).to.be.equal(1) + + expect(spans[0]).to.have.property('name', 'web.request') + expect(spans[0]).to.have.property('service', 'aws-server') + expect(spans[0]).to.have.property('type', 'web') + expect(spans[0]).to.have.property('resource', 'GET') + expect(spans[0].meta).to.have.property('component', 'http') + expect(spans[0].meta).to.have.property('span.kind', 'server') + expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/configured-off`) + expect(spans[0].meta).to.have.property('http.method', 'GET') + expect(spans[0].meta).to.have.property('http.status_code', '200') + expect(spans[0].meta).to.have.property('span.kind', 'server') + }) + }) + }) +}) diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index 25960926f67..4888ece9de1 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -7,23 +7,23 @@ const { COMPONENT } = require('../../dd-trace/src/constants') class HttpServerPlugin extends WebPlugin { static id = 'http' - static prefix = 'apm:http:server:request' + static type = 'web' + static kind = 'server' constructor (...args) { super(...args) this._parentStore = undefined this.addTraceSub('exit', message => this.exit(message)) + this.config = { + ...this.config, + service: this.config.service || this.serviceName(), + } } start ({ req, res, abortController }) { const store = storage('legacy').getStore() const span = this.startSpan( - this.tracer, - { - ...this.config, - service: this.config.service || this.serviceName(), - }, req, res, this.operationName() diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index aa0ff07340d..fb38734c718 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -106,7 +106,7 @@ class WebPlugin extends TracingPlugin { analyticsSampler.sample(span, config.measured, true) } - startSpan (tracer, config, req, res, name, traceCtx) { + startSpan (req, res, name, traceCtx) { const context = this.patch(req) let span @@ -115,15 +115,15 @@ class WebPlugin extends TracingPlugin { context.span.context()._name = name span = context.span } else { - span = this.startChildSpan(tracer, config, name, req, traceCtx) + span = this.startChildSpan(name, req, traceCtx) } - context.tracer = tracer + context.tracer = this.tracer context.span = span context.res = res - this.setConfig(req, config) - addRequestTags(context, this.constructor.id) + this.setConfig(req, this.config) + this._addRequestTags(context, this.constructor.id) return span } @@ -137,12 +137,12 @@ class WebPlugin extends TracingPlugin { } // Start a span and activate a scope for a request. - instrument (tracer, config, req, res, name, callback) { - const span = this.startSpan(tracer, config, req, res, name) + instrument (req, res, name, callback) { + const span = this.startSpan(req, res, name) this.wrap(req) - return callback && tracer.scope().activate(span, () => callback(span)) + return callback && this.tracer.scope().activate(span, () => callback(span)) } // Reactivate the request scope in case it was changed by a middleware. @@ -258,11 +258,15 @@ class WebPlugin extends TracingPlugin { } // Return the request root span. - root (req) { + static root (req) { const context = contexts.get(req) return context ? context.span : null } + root (req) { + return WebPlugin.root(req) + } + // Return the active span. active (req) { const context = contexts.get(req) @@ -274,9 +278,9 @@ class WebPlugin extends TracingPlugin { } // Extract the parent span from the headers and start a new span as its child - startChildSpan (tracer, config, name, req, traceCtx) { + startChildSpan (name, req, traceCtx) { const headers = req.headers - const childOf = tracer.extract(FORMAT_HTTP_HEADERS, headers) + const childOf = this.tracer.extract(FORMAT_HTTP_HEADERS, headers) const span = super.startSpan(name, { childOf }, traceCtx) @@ -329,8 +333,8 @@ class WebPlugin extends TracingPlugin { if (context.finished && !req.stream) return - addRequestTags(context, this.constructor.id) - addResponseTags(context) + this._addRequestTags(context, this.constructor.id) + this._addResponseTags(context) context.config.hooks.request(context.span, req, res) addResourceTag(context) @@ -349,8 +353,8 @@ class WebPlugin extends TracingPlugin { this.finishSpan(context) } - obfuscateQs (config, url) { - const { queryStringObfuscation } = config + _obfuscateQs (url) { + const { queryStringObfuscation } = this.config if (queryStringObfuscation === false) return url @@ -383,12 +387,16 @@ class WebPlugin extends TracingPlugin { } } - getContext (req) { + static getContext (req) { return contexts.get(req) } + getContext (req) { + return WebPlugin.getContext(req) + } + wrapRes (context, req, res, end) { - return function () { + return () => { this.finishAll(context) return end.apply(res, arguments) @@ -415,6 +423,49 @@ class WebPlugin extends TracingPlugin { } }) } + + _addRequestTags (context, spanType) { + const { req, span, inferredProxySpan } = context + const url = extractURL(req) + + span.addTags({ + [HTTP_URL]: this._obfuscateQs(url), + [HTTP_METHOD]: req.method, + [SPAN_KIND]: SERVER, + [SPAN_TYPE]: spanType, + [HTTP_USERAGENT]: req.headers['user-agent'] + }) + + // if client ip has already been set by appsec, no need to run it again + if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { + const clientIp = extractIp(this.config, req) + + if (clientIp) { + span.setTag(HTTP_CLIENT_IP, clientIp) + inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) + } + } + + addHeaders(context) + } + + _addResponseTags (context) { + const { req, res, paths, span, inferredProxySpan } = context + + const route = paths.join('') + if (route) { + span.setTag(HTTP_ROUTE, route) + } + + span.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + inferredProxySpan?.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + + this.addStatusError(req, res.statusCode) + } } function addAllowHeaders (req, res, headers) { @@ -459,49 +510,6 @@ function reactivate (req, fn) { : fn() } -function addRequestTags (context, spanType) { - const { req, span, inferredProxySpan, config } = context - const url = extractURL(req) - - span.addTags({ - [HTTP_URL]: this.obfuscateQs(config, url), - [HTTP_METHOD]: req.method, - [SPAN_KIND]: SERVER, - [SPAN_TYPE]: spanType, - [HTTP_USERAGENT]: req.headers['user-agent'] - }) - - // if client ip has already been set by appsec, no need to run it again - if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { - const clientIp = extractIp(config, req) - - if (clientIp) { - span.setTag(HTTP_CLIENT_IP, clientIp) - inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) - } - } - - addHeaders(context) -} - -function addResponseTags (context) { - const { req, res, paths, span, inferredProxySpan } = context - - const route = paths.join('') - if (route) { - span.setTag(HTTP_ROUTE, route) - } - - span.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - inferredProxySpan?.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - - this.addStatusError(req, res.statusCode) -} - function addResourceTag (context) { const { req, span } = context const tags = span.context()._tags diff --git a/packages/datadog-plugin-web/test/index.spec.js b/packages/datadog-plugin-web/test/index.spec.js index 021d67c2c01..03d060dcf0f 100644 --- a/packages/datadog-plugin-web/test/index.spec.js +++ b/packages/datadog-plugin-web/test/index.spec.js @@ -1,7 +1,5 @@ 'use strict' -require('../../setup/tap') - const types = require('../../../ext/types') const kinds = require('../../../ext/kinds') const tags = require('../../../ext/tags') @@ -57,7 +55,8 @@ describe('plugins/util/web', () => { config = { hooks: {} } tracer = require('../../..').init({ plugins: false }) - web = require('../../../src/plugins/util/web') + const WebPlugin = require('../src') + web = new WebPlugin(tracer, config) }) beforeEach(() => { diff --git a/packages/dd-trace/src/appsec/api_security_sampler.js b/packages/dd-trace/src/appsec/api_security_sampler.js index bf3fc9f513d..a1dbbd7e870 100644 --- a/packages/dd-trace/src/appsec/api_security_sampler.js +++ b/packages/dd-trace/src/appsec/api_security_sampler.js @@ -1,7 +1,7 @@ 'use strict' const TTLCache = require('@isaacs/ttlcache') -const web = require('../plugins/util/web') +const WebPlugin = require('../../../datadog-plugin-web/src') const log = require('../log') const { AUTO_REJECT, USER_REJECT } = require('../../../../ext/priority') const { keepTrace } = require('../priority_sampler') @@ -42,7 +42,7 @@ function sampleRequest (req, res, force = false) { const key = computeKey(req, res) if (!key || isSampled(key)) return false - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return false if (asmStandaloneEnabled) { @@ -71,7 +71,7 @@ function isSampled (key) { } function computeKey (req, res) { - const route = web.getContext(req)?.paths?.join('') || '' + const route = WebPlugin.getContext(req)?.paths?.join('') || '' const method = req.method const status = res.statusCode diff --git a/packages/dd-trace/src/appsec/graphql.js b/packages/dd-trace/src/appsec/graphql.js index 2c59657fe2f..6a109184ad4 100644 --- a/packages/dd-trace/src/appsec/graphql.js +++ b/packages/dd-trace/src/appsec/graphql.js @@ -10,7 +10,7 @@ const { const log = require('../log') const waf = require('./waf') const addresses = require('./addresses') -const web = require('../plugins/util/web') +const WebPlugin = require('../../../datadog-plugin-web/src') const { startGraphqlResolve, graphqlMiddlewareChannel, @@ -89,7 +89,7 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) { const requestData = graphqlRequestData.get(req) if (requestData?.blocked) { - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return try { diff --git a/packages/dd-trace/src/appsec/iast/index.js b/packages/dd-trace/src/appsec/iast/index.js index 76209f0599f..7d4d000cd3c 100644 --- a/packages/dd-trace/src/appsec/iast/index.js +++ b/packages/dd-trace/src/appsec/iast/index.js @@ -2,7 +2,7 @@ const vulnerabilityReporter = require('./vulnerability-reporter') const { enableAllAnalyzers, disableAllAnalyzers } = require('./analyzers') -const web = require('../../plugins/util/web') +const WebPlugin = require('../../../../datadog-plugin-web/src') const { storage } = require('../../../../datadog-core') const overheadController = require('./overhead-controller') const dc = require('dc-polyfill') @@ -65,7 +65,7 @@ function onIncomingHttpRequestStart (data) { if (data?.req) { const store = storage('legacy').getStore() if (store) { - const topContext = web.getContext(data.req) + const topContext = WebPlugin.getContext(data.req) if (topContext) { const rootSpan = topContext.span const isRequestAcquired = overheadController.acquireRequest(rootSpan) @@ -89,7 +89,7 @@ function onIncomingHttpRequestStart (data) { function onIncomingHttpRequestEnd (data) { if (data?.req) { const store = storage('legacy').getStore() - const topContext = web.getContext(data.req) + const topContext = WebPlugin.getContext(data.req) const iastContext = iastContextFunctions.getIastContext(store, topContext) if (iastContext?.rootSpan) { const storedHeaders = collectedResponseHeaders.get(data.res) || {} diff --git a/packages/dd-trace/src/appsec/iast/overhead-controller.js b/packages/dd-trace/src/appsec/iast/overhead-controller.js index ad5f005fd84..8e566772965 100644 --- a/packages/dd-trace/src/appsec/iast/overhead-controller.js +++ b/packages/dd-trace/src/appsec/iast/overhead-controller.js @@ -1,7 +1,7 @@ 'use strict' const { LRUCache } = require('lru-cache') -const web = require('../../plugins/util/web') +const WebPlugin = require('../../../../datadog-plugin-web/src') const vulnerabilities = require('./vulnerabilities') const OVERHEAD_CONTROLLER_CONTEXT_KEY = 'oce' @@ -97,7 +97,7 @@ function _getContext (iastContext) { if (iastContext?.[OVERHEAD_CONTROLLER_CONTEXT_KEY]) { const oceContext = iastContext[OVERHEAD_CONTROLLER_CONTEXT_KEY] if (!oceContext.webContext) { - oceContext.webContext = web.getContext(iastContext.req) + oceContext.webContext = WebPlugin.getContext(iastContext.req) oceContext.method = iastContext.req?.method } diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index cb76ab64b2b..44fc84865b9 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -31,7 +31,7 @@ const addresses = require('./addresses') const Reporter = require('./reporter') const appsecTelemetry = require('./telemetry') const apiSecuritySampler = require('./api_security_sampler') -const web = require('../plugins/util/web') +const WebPlugin = require('../../../datadog-plugin-web/src') const { extractIp } = require('../plugins/util/ip_extractor') const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking') @@ -111,7 +111,7 @@ function onRequestBodyParsed ({ req, res, body, abortController }) { req = store?.req } - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return const results = waf.run({ @@ -126,7 +126,7 @@ function onRequestBodyParsed ({ req, res, body, abortController }) { function onRequestCookieParser ({ req, res, abortController, cookies }) { if (!cookies || typeof cookies !== 'object') return - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return const results = waf.run({ @@ -139,7 +139,7 @@ function onRequestCookieParser ({ req, res, abortController, cookies }) { } function incomingHttpStartTranslator ({ req, res, abortController }) { - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return const clientIp = extractIp(config, req) @@ -209,7 +209,7 @@ function incomingHttpEndTranslator ({ req, res }) { function onPassportVerify ({ framework, login, user, success, abortController }) { const store = storage('legacy').getStore() - const rootSpan = store?.req && web.root(store.req) + const rootSpan = store?.req && WebPlugin.root(store.req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onPassportVerify') @@ -223,7 +223,7 @@ function onPassportVerify ({ framework, login, user, success, abortController }) function onPassportDeserializeUser ({ user, abortController }) { const store = storage('legacy').getStore() - const rootSpan = store?.req && web.root(store.req) + const rootSpan = store?.req && WebPlugin.root(store.req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onPassportDeserializeUser') @@ -236,7 +236,7 @@ function onPassportDeserializeUser ({ user, abortController }) { } function onExpressSession ({ req, res, sessionId, abortController }) { - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onExpressSession') return @@ -262,7 +262,7 @@ function onRequestQueryParsed ({ req, res, query, abortController }) { req = store?.req } - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return const results = waf.run({ @@ -275,7 +275,7 @@ function onRequestQueryParsed ({ req, res, query, abortController }) { } function onRequestProcessParams ({ req, res, abortController, params }) { - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return if (!params || typeof params !== 'object' || !Object.keys(params).length) return @@ -317,7 +317,7 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH return } - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return responseHeaders = { ...responseHeaders } diff --git a/packages/dd-trace/src/appsec/rasp/index.js b/packages/dd-trace/src/appsec/rasp/index.js index 6da75c91283..2d5bf08d1c3 100644 --- a/packages/dd-trace/src/appsec/rasp/index.js +++ b/packages/dd-trace/src/appsec/rasp/index.js @@ -1,6 +1,6 @@ 'use strict' -const web = require('../../plugins/util/web') +const WebPlugin = require('../../../../datadog-plugin-web/src') const { setUncaughtExceptionCaptureCallbackStart, expressMiddlewareError } = require('../channels') const { block, isBlocked } = require('../blocking') const ssrf = require('./ssrf') @@ -87,7 +87,7 @@ function blockOnDatadogRaspAbortError ({ error }) { const { req, res, blockingAction, raspRule, ruleTriggered } = abortError if (!isBlocked(res)) { - const blocked = block(req, res, web.root(req), null, blockingAction) + const blocked = block(req, res, WebPlugin.root(req), null, blockingAction) if (ruleTriggered) { updateRaspRuleMatchMetricTags(req, raspRule, true, blocked) } diff --git a/packages/dd-trace/src/appsec/rasp/utils.js b/packages/dd-trace/src/appsec/rasp/utils.js index f3b3cea6ba9..80861a0c06b 100644 --- a/packages/dd-trace/src/appsec/rasp/utils.js +++ b/packages/dd-trace/src/appsec/rasp/utils.js @@ -1,6 +1,6 @@ 'use strict' -const web = require('../../plugins/util/web') +const WebPlugin = require('../../../../datadog-plugin-web/src') const { getCallsiteFrames, reportStackTrace, canReportStackTrace } = require('../stack_trace') const { getBlockingAction } = require('../blocking') const log = require('../../log') @@ -36,7 +36,7 @@ function handleResult (result, req, res, abortController, config, raspRule) { const { enabled, maxDepth, maxStackTraces } = config.appsec.stackTrace - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) const ruleTriggered = !!result?.events?.length diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index af806041e43..813da668d98 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -4,7 +4,7 @@ const dc = require('dc-polyfill') const zlib = require('zlib') const { storage } = require('../../../datadog-core') -const web = require('../plugins/util/web') +const WebPlugin = require('../../../datadog-plugin-web/src') const { ipHeaderList } = require('../plugins/util/ip_extractor') const { incrementWafInitMetric, @@ -276,7 +276,7 @@ function reportWafConfigUpdate (product, rcConfigId, diagnostics, wafVersion) { function reportMetrics (metrics, raspRule) { const store = storage('legacy').getStore() - const rootSpan = store?.req && web.root(store.req) + const rootSpan = store?.req && WebPlugin.root(store.req) if (!rootSpan) return @@ -310,7 +310,7 @@ function reportTruncationMetrics (rootSpan, metrics) { function reportAttack (attackData) { const store = storage('legacy').getStore() const req = store?.req - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return const currentTags = rootSpan.context()._tags @@ -426,7 +426,7 @@ function reportAttributes (attributes) { if (!attributes) return const req = storage('legacy').getStore()?.req - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return @@ -443,7 +443,7 @@ function reportAttributes (attributes) { } function finishRequest (req, res, storedResponseHeaders) { - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) if (!rootSpan) return if (metricsQueue.size) { diff --git a/packages/dd-trace/src/appsec/waf/index.js b/packages/dd-trace/src/appsec/waf/index.js index a2035073f73..a5e6c11ece6 100644 --- a/packages/dd-trace/src/appsec/waf/index.js +++ b/packages/dd-trace/src/appsec/waf/index.js @@ -6,7 +6,7 @@ const Reporter = require('../reporter') const Limiter = require('../../rate_limiter') const { keepTrace } = require('../../priority_sampler') const { ASM } = require('../../standalone/product') -const web = require('../../plugins/util/web') +const WebPlugin = require('../../../../datadog-plugin-web/src') const { updateRateLimitedMetric } = require('../telemetry') class WafUpdateError extends Error { @@ -112,7 +112,7 @@ function run (data, req, raspRule) { if (result?.keep) { if (limiter.isAllowed()) { - const rootSpan = web.root(req) + const rootSpan = WebPlugin.root(req) keepTrace(rootSpan, ASM) } else { updateRateLimitedMetric(req) diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index 29c6bb7f7b4..f012f5546c8 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -4,8 +4,6 @@ const Plugin = require('./plugin') const { storage } = require('../../../datadog-core') const analyticsSampler = require('../analytics_sampler') const { COMPONENT } = require('../constants') -const DatadogSpanContext = require('../opentracing/span_context') -const id = require('../id') class TracingPlugin extends Plugin { constructor (...args) { @@ -127,17 +125,6 @@ class TracingPlugin extends Plugin { childOf = store.span } - console.log(process.env.DD_TRACE_ID, process.env.DD_PARENT_SPAN_ID) - if (!childOf && process.env.DD_PARENT_SPAN_ID) { - console.log("starting spans from remote trace") - childOf = new DatadogSpanContext({ - traceId: id(process.env.DD_TRACE_ID, 16), - spanId: id(process.env.DD_PARENT_SPAN_ID, 16), - isRemote: true, - sampling: { priority: 1 }, - }) - } - const span = tracer.startSpan(name, { startTime, childOf, From 079a03deb15b6b9cc841592b57207160d351d641 Mon Sep 17 00:00:00 2001 From: William Conti Date: Fri, 8 Aug 2025 14:11:00 -0400 Subject: [PATCH 05/28] more changes --- packages/datadog-plugin-web/src/index.js | 35 +-- .../datadog-plugin-web/test/index.spec.js | 201 ++++++++---------- 2 files changed, 111 insertions(+), 125 deletions(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index fb38734c718..30066842dbd 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -51,6 +51,12 @@ class WebPlugin extends TracingPlugin { this.addSub('apm:http:server:request:end', ({ req }) => { this.finish(req) }) + + this.configure(this.config) + } + + configure (config) { + return super.configure(this.normalizeConfig(config)) } // Ensure the configuration has the correct structure and defaults. @@ -88,22 +94,25 @@ class WebPlugin extends TracingPlugin { this.setConfig(req, config) } - setConfig (req, config) { + setConfig (req, config = {}) { const context = contexts.get(req) - const span = context.span + if (!context) return + + context.config = { ...this.config, ...config } - context.config = config + const span = context.span + if (!span) return - if (!config.filter(req.url)) { + if (!context.config.filter(req.url)) { span.setTag(MANUAL_DROP, true) span.context()._trace.isRecording = false } - if (config.service) { - span.setTag(SERVICE_NAME, config.service) + if (context.config.service) { + span.setTag(SERVICE_NAME, context.config.service) } - analyticsSampler.sample(span, config.measured, true) + analyticsSampler.sample(span, context.config.measured, true) } startSpan (req, res, name, traceCtx) { @@ -122,7 +131,7 @@ class WebPlugin extends TracingPlugin { context.span = span context.res = res - this.setConfig(req, this.config) + this.setConfig(req) this._addRequestTags(context, this.constructor.id) return span @@ -249,7 +258,7 @@ class WebPlugin extends TracingPlugin { paths: [], middleware: [], beforeEnd: [], - config: {} + config: this.config } contexts.set(req, context) @@ -353,8 +362,8 @@ class WebPlugin extends TracingPlugin { this.finishSpan(context) } - _obfuscateQs (url) { - const { queryStringObfuscation } = this.config + _obfuscateQs (url, config) { + const { queryStringObfuscation } = config if (queryStringObfuscation === false) return url @@ -429,7 +438,7 @@ class WebPlugin extends TracingPlugin { const url = extractURL(req) span.addTags({ - [HTTP_URL]: this._obfuscateQs(url), + [HTTP_URL]: this._obfuscateQs(url, context.config), [HTTP_METHOD]: req.method, [SPAN_KIND]: SERVER, [SPAN_TYPE]: spanType, @@ -438,7 +447,7 @@ class WebPlugin extends TracingPlugin { // if client ip has already been set by appsec, no need to run it again if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { - const clientIp = extractIp(this.config, req) + const clientIp = extractIp(context.config, req) if (clientIp) { span.setTag(HTTP_CLIENT_IP, clientIp) diff --git a/packages/datadog-plugin-web/test/index.spec.js b/packages/datadog-plugin-web/test/index.spec.js index 03d060dcf0f..d2c3c2476be 100644 --- a/packages/datadog-plugin-web/test/index.spec.js +++ b/packages/datadog-plugin-web/test/index.spec.js @@ -59,10 +59,6 @@ describe('plugins/util/web', () => { web = new WebPlugin(tracer, config) }) - beforeEach(() => { - config = web.normalizeConfig(config) - }) - describe('normalizeConfig', () => { it('should set the correct defaults', () => { const config = web.normalizeConfig({}) @@ -146,22 +142,22 @@ describe('plugins/util/web', () => { 'x-datadog-parent-id': '456' } - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { expect(span.context()._traceId.toString(10)).to.equal('123') expect(span.context()._parentId.toString(10)).to.equal('456') }) }) it('should set the service name', () => { - config.service = 'custom' + web.configure({ service: 'custom' }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { expect(span.context()._tags).to.have.property(SERVICE_NAME, 'custom') }) }) it('should activate a scope with the span', () => { - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { expect(tracer.scope().active()).to.equal(span) }) }) @@ -173,7 +169,7 @@ describe('plugins/util/web', () => { req.headers['x-forwarded-for'] = '8.8.8.8' res.statusCode = '200' - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -191,10 +187,9 @@ describe('plugins/util/web', () => { it('should add client ip tag to the span when enabled', () => { req.headers['x-forwarded-for'] = '8.8.8.8' - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -208,11 +203,9 @@ describe('plugins/util/web', () => { it('should add custom client ip tag to the span when it is configured', () => { req.headers['X-Forwad-For'] = '8.8.8.8' - config.clientIpEnabled = true - config.clientIpHeader = 'X-Forwad-For' + web.configure({ clientIpEnabled: true, clientIpHeader: 'X-Forwad-For' }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -226,10 +219,9 @@ describe('plugins/util/web', () => { it('should not add custom client ip tag to the span when it is not configured', () => { req.headers['X-Forwad-For'] = '8.8.8.8' - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -241,10 +233,9 @@ describe('plugins/util/web', () => { it('should not add client ip tag to the span when disabled', () => { req.headers['x-forwarded-for'] = '8.8.8.8' - config.clientIpEnabled = false + web.configure({ clientIpEnabled: false }) - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -256,9 +247,9 @@ describe('plugins/util/web', () => { it('should not replace client ip when it exists', () => { req.headers['x-forwarded-for'] = '8.8.8.8' - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags span.setTag(HTTP_CLIENT_IP, '1.1.1.1') @@ -272,9 +263,9 @@ describe('plugins/util/web', () => { }) it('should not add client ip tag when no candidate header is present in request', () => { - config.clientIpEnabled = true + web.configure({ clientIpEnabled: true }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -286,10 +277,10 @@ describe('plugins/util/web', () => { it('should add configured headers to the span tags', () => { req.headers.req = 'incoming' req.headers.res = 'outgoing' - config.headers = ['host', 'req:http.req', 'server', 'res:http.res'] - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.configure({ headers: ['host', 'req:http.req', 'server', 'res:http.res'] }) + + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -304,47 +295,44 @@ describe('plugins/util/web', () => { }) it('should only start one span for the entire request', () => { - web.instrument(tracer, config, req, res, 'test.request', span1 => { - web.instrument(tracer, config, req, res, 'test.request', span2 => { + web.instrument(req, res, 'test.request', span1 => { + web.instrument(req, res, 'test.request', span2 => { expect(span1).to.equal(span2) }) }) }) it('should allow overriding the span name', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { - web.instrument(tracer, config, req, res, 'test2.request', span => { + web.instrument(req, res, 'test.request', () => { + web.instrument(req, res, 'test2.request', span => { expect(span.context()._name).to.equal('test2.request') }) }) }) it('should allow overriding the span service name', () => { - web.instrument(tracer, config, req, res, 'test.request', span => { - config.service = 'test2' - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request', span => { + web.configure({ service: 'test2' }) + web.instrument(req, res, 'test.request') expect(span.context()._tags).to.have.property('service.name', 'test2') }) }) it('should only wrap res.end once', () => { - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') const end = res.end - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') expect(end).to.equal(res.end) }) it('should use the config from the last call', () => { - config.headers = ['host'] + web.configure({ headers: ['host'] }) - const override = web.normalizeConfig({ - headers: ['date'] - }) - - web.instrument(tracer, config, req, res, 'test.request', () => { - web.instrument(tracer, override, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', () => { + web.configure({ headers: ['date'] }) + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -357,15 +345,13 @@ describe('plugins/util/web', () => { }) it('should obfuscate the query string from the URL', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: 'secret=.*?(&|$)' - }) + web.configure({ queryStringObfuscation: 'secret=.*?(&|$)' }) req.method = 'GET' req.url = '/user/123?secret=password&foo=bar' res.statusCode = '200' - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -394,7 +380,7 @@ describe('plugins/util/web', () => { 'access-control-allow-origin': 'http://test.com' }) - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -415,7 +401,7 @@ describe('plugins/util/web', () => { 'access-control-allow-origin': 'http://test.com' }) - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -429,7 +415,7 @@ describe('plugins/util/web', () => { req.headers.origin = 'http://test.com' req.headers['access-control-request-headers'] = headers - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -444,7 +430,7 @@ describe('plugins/util/web', () => { 'access-control-allow-origin': 'http://test.com' }) - web.instrument(tracer, config, req, res, 'test.request') + web.instrument(req, res, 'test.request') res.writeHead() @@ -457,7 +443,7 @@ describe('plugins/util/web', () => { req.headers['x-forwarded-for'] = '8.8.8.8' req.socket = { encrypted: true } - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -485,7 +471,7 @@ describe('plugins/util/web', () => { } res.statusCode = '200' - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const tags = span.context()._tags res.end() @@ -501,9 +487,9 @@ describe('plugins/util/web', () => { }) it('should drop filtered out requests', () => { - config.filter = () => false + web.configure({ filter: () => false }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { const sampling = span.context()._sampling res.end() @@ -515,7 +501,7 @@ describe('plugins/util/web', () => { describe('on request end', () => { beforeEach(() => { - web.instrument(tracer, config, req, res, 'test.request', reqSpan => { + web.instrument(req, res, 'test.request', reqSpan => { span = reqSpan tags = span.context()._tags }) @@ -593,7 +579,7 @@ describe('plugins/util/web', () => { }) it('should set the error tag if the configured validator returns false', () => { - config.validateStatus = () => false + web.configure({ validateStatus: () => false }) res.end() @@ -613,31 +599,34 @@ describe('plugins/util/web', () => { }) it('should execute the request end hook', () => { - config.hooks.request = sinon.spy() + const hook = sinon.spy() + + web.configure({ hooks: { request: hook } }) res.end() - expect(config.hooks.request).to.have.been.calledWith(span, req, res) + expect(hook).to.have.been.calledWith(span, req, res) }) it('should execute multiple end hooks', () => { - config.hooks = { - request: sinon.spy() - } + const hook = sinon.spy() + web.configure({ hooks: { request: hook } }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { res.end() - expect(config.hooks.request).to.have.been.calledWith(span, req, res) + expect(hook).to.have.been.calledWith(span, req, res) }) }) it('should set the resource name from the http.route tag set in the hooks', () => { - config.hooks = { - request: span => span.setTag('http.route', '/custom/route') - } + web.configure({ + hooks: { + request: span => span.setTag('http.route', '/custom/route') + } + }) - web.instrument(tracer, config, req, res, 'test.request', span => { + web.instrument(req, res, 'test.request', span => { res.end() expect(tags).to.have.property('resource.name', 'GET /custom/route') @@ -648,8 +637,7 @@ describe('plugins/util/web', () => { describe('enterRoute', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -680,8 +668,7 @@ describe('plugins/util/web', () => { describe('exitRoute', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', reqSpan => { + web.instrument(req, res, 'test.request', reqSpan => { span = reqSpan tags = span.context()._tags }) @@ -701,8 +688,7 @@ describe('plugins/util/web', () => { describe('wrapMiddleware', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -720,8 +706,7 @@ describe('plugins/util/web', () => { describe('finish', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -764,7 +749,7 @@ describe('plugins/util/web', () => { describe('root', () => { it('should return the request root span', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { const span = tracer.scope().active() web.wrapMiddleware(req, () => {}, 'express.middleware', () => { @@ -780,13 +765,13 @@ describe('plugins/util/web', () => { describe('active', () => { it('should return the request span by default', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { expect(web.active(req)).to.equal(tracer.scope().active()) }) }) it('should return the active middleware span', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { const span = tracer.scope().active() web.wrapMiddleware(req, () => {}, 'express.middleware', () => { @@ -803,8 +788,7 @@ describe('plugins/util/web', () => { describe('addError', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -836,8 +820,7 @@ describe('plugins/util/web', () => { describe('addStatusError', () => { beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { + web.instrument(req, res, 'test.request', () => { span = tracer.scope().active() tags = span.context()._tags }) @@ -852,7 +835,7 @@ describe('plugins/util/web', () => { }) it('should only flag requests as an error for configured status codes', () => { - config.validateStatus = () => true + web.configure({ validateStatus: () => true }) web.addStatusError(req, 500) @@ -862,68 +845,64 @@ describe('plugins/util/web', () => { describe('allowlistFilter', () => { beforeEach(() => { - config = { allowlist: ['/_okay'] } - config = web.normalizeConfig(config) + web.configure({ allowlist: ['/_okay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) describe('whitelistFilter', () => { beforeEach(() => { - config = { whitelist: ['/_okay'] } - config = web.normalizeConfig(config) + web.configure({ whitelist: ['/_okay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) describe('blocklistFilter', () => { beforeEach(() => { - config = { blocklist: ['/_notokay'] } - config = web.normalizeConfig(config) + web.configure({ blocklist: ['/_notokay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) describe('blacklistFilter', () => { beforeEach(() => { - config = { blacklist: ['/_notokay'] } - config = web.normalizeConfig(config) + web.configure({ blacklist: ['/_notokay'] }) }) it('should not filter the url', () => { - const filtered = config.filter('/_okay') + const filtered = web.config.filter('/_okay') expect(filtered).to.equal(true) }) it('should filter the url', () => { - const filtered = config.filter('/_notokay') + const filtered = web.config.filter('/_notokay') expect(filtered).to.equal(false) }) }) @@ -932,38 +911,36 @@ describe('plugins/util/web', () => { const url = 'http://perdu.com/path/' const qs = '?data=secret' - let config - beforeEach(() => { - config = { + web.configure({ queryStringObfuscation: /secret/gi - } + }) }) it('should not obfuscate when passed false', () => { - config.queryStringObfuscation = false + web.configure({ queryStringObfuscation: false }) - const result = web.obfuscateQs(config, url + qs) + const result = web._obfuscateQs(url + qs) expect(result).to.equal(url + qs) }) it('should not obfuscate when no querystring is found', () => { - const result = web.obfuscateQs(config, url) + const result = web._obfuscateQs(url) expect(result).to.equal(url) }) it('should remove the querystring if passed true', () => { - config.queryStringObfuscation = true + web.configure({ queryStringObfuscation: true }) - const result = web.obfuscateQs(config, url + qs) + const result = web._obfuscateQs(url + qs, web.config) expect(result).to.equal(url) }) it('should obfuscate only the querystring part of the url', () => { - const result = web.obfuscateQs(config, url + 'secret/' + qs) + const result = web._obfuscateQs(url + 'secret/' + qs, web.config) expect(result).to.equal(url + 'secret/?data=') }) From 967d42ec1b07619eebe34717ab86a4d36ce8a510 Mon Sep 17 00:00:00 2001 From: William Conti Date: Mon, 11 Aug 2025 11:16:53 -0400 Subject: [PATCH 06/28] more changes --- packages/datadog-plugin-connect/src/index.js | 8 -------- .../datadog-plugin-express/src/tracing.js | 8 -------- .../datadog-plugin-fastify/src/tracing.js | 4 ---- .../datadog-plugin-find-my-way/src/index.js | 8 -------- packages/datadog-plugin-hapi/src/index.js | 6 +----- packages/datadog-plugin-hono/src/index.js | 16 --------------- packages/datadog-plugin-http2/src/server.js | 4 ---- packages/datadog-plugin-koa/src/index.js | 12 ----------- .../src/index.js | 17 +--------------- packages/datadog-plugin-restify/src/index.js | 12 ----------- packages/datadog-plugin-web/src/index.js | 20 ++++++++++++++----- 11 files changed, 17 insertions(+), 98 deletions(-) diff --git a/packages/datadog-plugin-connect/src/index.js b/packages/datadog-plugin-connect/src/index.js index 9453fe5168e..0179be94e92 100644 --- a/packages/datadog-plugin-connect/src/index.js +++ b/packages/datadog-plugin-connect/src/index.js @@ -4,14 +4,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class ConnectPlugin extends RouterPlugin { static id = 'connect' - - constructor (...args) { - super(...args) - - this.addSub('apm:connect:request:handle', ({ req }) => { - this.setFramework(req, 'connect', this.config) - }) - } } module.exports = ConnectPlugin diff --git a/packages/datadog-plugin-express/src/tracing.js b/packages/datadog-plugin-express/src/tracing.js index ddedf3b3b1f..eca31fdb5f1 100644 --- a/packages/datadog-plugin-express/src/tracing.js +++ b/packages/datadog-plugin-express/src/tracing.js @@ -4,14 +4,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class ExpressTracingPlugin extends RouterPlugin { static id = 'express' - - constructor (...args) { - super(...args) - - this.addSub('apm:express:request:handle', ({ req }) => { - this.setFramework(req, 'express', this.config) - }) - } } module.exports = ExpressTracingPlugin diff --git a/packages/datadog-plugin-fastify/src/tracing.js b/packages/datadog-plugin-fastify/src/tracing.js index debe8cf61b9..afaffbf4c98 100644 --- a/packages/datadog-plugin-fastify/src/tracing.js +++ b/packages/datadog-plugin-fastify/src/tracing.js @@ -9,10 +9,6 @@ class FastifyTracingPlugin extends RouterPlugin { constructor (...args) { super(...args) - this.addSub('apm:fastify:request:handle', ({ req }) => { - this.setFramework(req, 'fastify', this.config) - }) - this.addBind('datadog:fastify:pre-parsing:start', getParentStore) this.addBind('datadog:fastify:pre-validation:start', getParentStore) diff --git a/packages/datadog-plugin-find-my-way/src/index.js b/packages/datadog-plugin-find-my-way/src/index.js index fae020e6f52..217cb9950c9 100644 --- a/packages/datadog-plugin-find-my-way/src/index.js +++ b/packages/datadog-plugin-find-my-way/src/index.js @@ -4,14 +4,6 @@ const WebPlugin = require('../../datadog-plugin-web/src') class FindMyWayPlugin extends WebPlugin { static id = 'find-my-way' - - constructor (...args) { - super(...args) - - this.addSub('apm:find-my-way:request:route', ({ req, route }) => { - this.setRoute(req, route) - }) - } } module.exports = FindMyWayPlugin diff --git a/packages/datadog-plugin-hapi/src/index.js b/packages/datadog-plugin-hapi/src/index.js index 15cc9097f7a..348f0acb8b1 100644 --- a/packages/datadog-plugin-hapi/src/index.js +++ b/packages/datadog-plugin-hapi/src/index.js @@ -15,14 +15,10 @@ class HapiPlugin extends RouterPlugin { const store = storage('legacy').getStore() const span = store && store.span - this.setFramework(req, 'hapi', this.config) + this.setFramework(req, 'hapi') this._requestSpans.set(req, span) }) - this.addSub('apm:hapi:request:route', ({ req, route }) => { - this.setRoute(req, route) - }) - this.addSub('apm:hapi:request:error', error => { if (!error || !error.isBoom || !this.config.validateStatus(error.output.statusCode)) { this.addError(error) diff --git a/packages/datadog-plugin-hono/src/index.js b/packages/datadog-plugin-hono/src/index.js index d01be76b0a5..5471ea91784 100644 --- a/packages/datadog-plugin-hono/src/index.js +++ b/packages/datadog-plugin-hono/src/index.js @@ -4,22 +4,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class HonoPlugin extends RouterPlugin { static id = 'hono' - - constructor (...args) { - super(...args) - - this.addSub('apm:hono:request:handle', ({ req }) => { - this.setFramework(req, 'hono', this.config) - }) - - this.addSub('apm:hono:request:route', ({ req, route }) => { - this.setRoute(req, route) - }) - - this.addSub('apm:hono:request:error', ({ req, error }) => { - this.addError(req, error) - }) - } } module.exports = HonoPlugin diff --git a/packages/datadog-plugin-http2/src/server.js b/packages/datadog-plugin-http2/src/server.js index c2bcdc2a7ec..b131a1d123d 100644 --- a/packages/datadog-plugin-http2/src/server.js +++ b/packages/datadog-plugin-http2/src/server.js @@ -63,10 +63,6 @@ class Http2ServerPlugin extends WebPlugin { error (error) { this.addError(error) } - - configure (config) { - return super.configure(this.normalizeConfig(config)) - } } module.exports = Http2ServerPlugin diff --git a/packages/datadog-plugin-koa/src/index.js b/packages/datadog-plugin-koa/src/index.js index 8cc9f1164b4..d1fccc1b867 100644 --- a/packages/datadog-plugin-koa/src/index.js +++ b/packages/datadog-plugin-koa/src/index.js @@ -4,18 +4,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class KoaPlugin extends RouterPlugin { static id = 'koa' - - constructor (...args) { - super(...args) - - this.addSub('apm:koa:request:handle', ({ req }) => { - this.setFramework(req, 'koa', this.config) - }) - - this.addSub('apm:koa:request:route', ({ req, route }) => { - this.setRoute(req, route) - }) - } } module.exports = KoaPlugin diff --git a/packages/datadog-plugin-microgateway-core/src/index.js b/packages/datadog-plugin-microgateway-core/src/index.js index 2f853144a33..f6fc88fa6a8 100644 --- a/packages/datadog-plugin-microgateway-core/src/index.js +++ b/packages/datadog-plugin-microgateway-core/src/index.js @@ -4,22 +4,7 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class MicrogatewayCorePlugin extends RouterPlugin { static id = 'microgateway-core' - - constructor (...args) { - super(...args) - - this.addSub('apm:microgateway-core:request:handle', ({ req }) => { - this.setFramework(req, 'microgateway', this.config) - }) - - this.addSub('apm:microgateway-core:request:route', ({ req, route }) => { - this.setRoute(req, route) - }) - - this.addSub('apm:microgateway-core:request:error', ({ error }) => { - this.addError(error) - }) - } + static framework = 'microgateway' } module.exports = MicrogatewayCorePlugin diff --git a/packages/datadog-plugin-restify/src/index.js b/packages/datadog-plugin-restify/src/index.js index 04328db6165..888463f9aaa 100644 --- a/packages/datadog-plugin-restify/src/index.js +++ b/packages/datadog-plugin-restify/src/index.js @@ -5,18 +5,6 @@ const RouterPlugin = require('../../datadog-plugin-router/src') class RestifyPlugin extends RouterPlugin { static id = 'restify' - constructor (...args) { - super(...args) - - this.addSub('apm:restify:request:handle', ({ req }) => { - this.setFramework(req, 'restify', this.config) - }) - - this.addSub('apm:restify:request:route', ({ req, route }) => { - this.setRoute(req, route) - }) - } - configure (config) { return super.configure({ ...config, diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 30066842dbd..0a20024503e 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -52,6 +52,18 @@ class WebPlugin extends TracingPlugin { this.finish(req) }) + this.addSub(`apm:${this.constructor.id}:request:handle`, ({ req }) => { + this.setFramework(req, this.constructor.framework || this.constructor.id) + }) + + this.addSub(`apm:${this.constructor.id}:request:route`, ({ req, route }) => { + this.setRoute(req, route) + }) + + this.addSub(`apm:${this.constructor.id}:request:error`, ({ req, error }) => { + this.addError(req, error) + }) + this.configure(this.config) } @@ -81,7 +93,7 @@ class WebPlugin extends TracingPlugin { } } - setFramework (req, name, config) { + setFramework (req, name) { const context = this.patch(req) const span = context.span @@ -91,15 +103,13 @@ class WebPlugin extends TracingPlugin { span.context()._tags.component = name span._integrationName = name - this.setConfig(req, config) + this.setConfig(req) } - setConfig (req, config = {}) { + setConfig (req) { const context = contexts.get(req) if (!context) return - context.config = { ...this.config, ...config } - const span = context.span if (!span) return From 5e95e7cefe1837d54b7810b0e0124429bce830b8 Mon Sep 17 00:00:00 2001 From: William Conti Date: Mon, 11 Aug 2025 12:36:57 -0400 Subject: [PATCH 07/28] more fixes --- packages/datadog-plugin-http2/src/server.js | 16 +- .../datadog-plugin-router/test/index.spec.js | 2 +- packages/datadog-plugin-web/src/index.js | 8 +- .../datadog-plugin-web/test/index.spec.js | 2 +- .../test/appsec/api_security_sampler.spec.js | 4 +- packages/dd-trace/test/appsec/graphql.spec.js | 2 +- .../appsec/iast/overhead-controller.spec.js | 2 +- packages/dd-trace/test/appsec/index.spec.js | 4 +- .../dd-trace/test/appsec/rasp/lfi.spec.js | 2 +- .../dd-trace/test/appsec/rasp/utils.spec.js | 2 +- .../dd-trace/test/appsec/reporter.spec.js | 2 +- .../dd-trace/test/appsec/waf/index.spec.js | 2 +- .../dd-trace/test/plugins/util/web.spec.js | 972 ------------------ 13 files changed, 23 insertions(+), 997 deletions(-) delete mode 100644 packages/dd-trace/test/plugins/util/web.spec.js diff --git a/packages/datadog-plugin-http2/src/server.js b/packages/datadog-plugin-http2/src/server.js index b131a1d123d..e0de8e2c5ae 100644 --- a/packages/datadog-plugin-http2/src/server.js +++ b/packages/datadog-plugin-http2/src/server.js @@ -6,24 +6,20 @@ const WebPlugin = require('../../datadog-plugin-web/src') const { COMPONENT } = require('../../dd-trace/src/constants') class Http2ServerPlugin extends WebPlugin { + static id = 'http2' + static prefix = 'apm:http2:server:request' + constructor (tracer, config) { super(tracer, config) - this.addBind('apm:http2:server:response:emit', this.bindEmit) - } + this.addBind('apm:http2:server:response:emit', this.bindEmit.bind(this)) - static id = 'http2' - - static prefix = 'apm:http2:server:request' + this.configure({ service: config.service || this.serviceName() }) + } bindStart (ctx) { const { req, res } = ctx const span = this.startSpan( - this.tracer, - { - ...this.config, - service: this.config.service || this.serviceName() - }, req, res, this.operationName(), diff --git a/packages/datadog-plugin-router/test/index.spec.js b/packages/datadog-plugin-router/test/index.spec.js index 892869d64e0..57e871c4279 100644 --- a/packages/datadog-plugin-router/test/index.spec.js +++ b/packages/datadog-plugin-router/test/index.spec.js @@ -6,7 +6,7 @@ const axios = require('axios') const http = require('http') const { once } = require('events') const agent = require('../../dd-trace/test/plugins/agent') -const web = require('../../dd-trace/src/plugins/util/web') +const web = require('../../datadog-plugin-web/src') const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 0a20024503e..9a23f74fe34 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -39,6 +39,8 @@ const ends = new WeakMap() class WebPlugin extends TracingPlugin { static id = WEB + static kind = SERVER + static type = WEB constructor (...args) { super(...args) @@ -142,7 +144,7 @@ class WebPlugin extends TracingPlugin { context.res = res this.setConfig(req) - this._addRequestTags(context, this.constructor.id) + this._addRequestTags(context, this.constructor.type) return span } @@ -301,7 +303,7 @@ class WebPlugin extends TracingPlugin { const headers = req.headers const childOf = this.tracer.extract(FORMAT_HTTP_HEADERS, headers) - const span = super.startSpan(name, { childOf }, traceCtx) + const span = super.startSpan(name, { childOf, kind: this.constructor.kind, type: this.constructor.type }, traceCtx) return span } @@ -352,7 +354,7 @@ class WebPlugin extends TracingPlugin { if (context.finished && !req.stream) return - this._addRequestTags(context, this.constructor.id) + this._addRequestTags(context, this.constructor.type) this._addResponseTags(context) context.config.hooks.request(context.span, req, res) diff --git a/packages/datadog-plugin-web/test/index.spec.js b/packages/datadog-plugin-web/test/index.spec.js index d2c3c2476be..a487852584b 100644 --- a/packages/datadog-plugin-web/test/index.spec.js +++ b/packages/datadog-plugin-web/test/index.spec.js @@ -22,7 +22,7 @@ const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS const HTTP_USERAGENT = tags.HTTP_USERAGENT const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP -describe('plugins/util/web', () => { +describe('Plugin', () => { let web let tracer let span diff --git a/packages/dd-trace/test/appsec/api_security_sampler.spec.js b/packages/dd-trace/test/appsec/api_security_sampler.spec.js index 064b0373201..64e242a9811 100644 --- a/packages/dd-trace/test/appsec/api_security_sampler.spec.js +++ b/packages/dd-trace/test/appsec/api_security_sampler.spec.js @@ -23,7 +23,7 @@ describe('API Security Sampler', () => { } apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../plugins/util/web': webStub + '../../../datadog-plugin-web/src/index': webStub }) span = { @@ -204,7 +204,7 @@ describe('API Security Sampler', () => { beforeEach(() => { keepTraceStub = sinon.stub() apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../plugins/util/web': webStub, + '../../../datadog-plugin-web/src/index': webStub, '../priority_sampler': { keepTrace: keepTraceStub }, diff --git a/packages/dd-trace/test/appsec/graphql.spec.js b/packages/dd-trace/test/appsec/graphql.spec.js index 401ad5f2b74..e54d0970482 100644 --- a/packages/dd-trace/test/appsec/graphql.spec.js +++ b/packages/dd-trace/test/appsec/graphql.spec.js @@ -2,7 +2,7 @@ const proxyquire = require('proxyquire') const waf = require('../../src/appsec/waf') -const web = require('../../src/plugins/util/web') +const web = require('../../../datadog-plugin-web/src') const { storage } = require('../../../datadog-core') const addresses = require('../../src/appsec/addresses') diff --git a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js index a0d41a53a0a..8d20e8d2d00 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -23,7 +23,7 @@ describe('Overhead controller', () => { } overheadController = proxyquire('../../../src/appsec/iast/overhead-controller', { - '../../plugins/util/web': web + '../../../../datadog-plugin-web/src/index': web }) oceContextKey = overheadController.OVERHEAD_CONTROLLER_CONTEXT_KEY diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 2d314ad31c6..138cf0c6dcc 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -124,7 +124,7 @@ describe('AppSec Index', function () { } apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../plugins/util/web': web + '../../../datadog-plugin-web/src/index': web }) sinon.spy(apiSecuritySampler, 'sampleRequest') @@ -145,7 +145,7 @@ describe('AppSec Index', function () { AppSec = proxyquire('../../src/appsec', { '../log': log, - '../plugins/util/web': web, + '../../../datadog-plugin-web/src/index': web, './blocking': blocking, './user_tracking': UserTracking, './telemetry': appsecTelemetry, diff --git a/packages/dd-trace/test/appsec/rasp/lfi.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.spec.js index b21c6473103..7447f2acc5f 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.spec.js @@ -34,7 +34,7 @@ describe('RASP - lfi.js', () => { lfi = proxyquire('../../../src/appsec/rasp/lfi', { '../../../../datadog-core': { storage: () => legacyStorage }, '../waf': waf, - '../../plugins/util/web': web, + '../../../../datadog-plugin-web/src/index': web, '../blocking': blocking, './fs-plugin': appsecFsPlugin }) diff --git a/packages/dd-trace/test/appsec/rasp/utils.spec.js b/packages/dd-trace/test/appsec/rasp/utils.spec.js index 14b39417bd0..82f399c8659 100644 --- a/packages/dd-trace/test/appsec/rasp/utils.spec.js +++ b/packages/dd-trace/test/appsec/rasp/utils.spec.js @@ -24,7 +24,7 @@ describe('RASP - utils.js', () => { } utils = proxyquire('../../../src/appsec/rasp/utils', { - '../../plugins/util/web': web, + '../../../../datadog-plugin-web/src/index': web, '../stack_trace': stackTrace, '../telemetry': telemetry, '../blocking': { diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index e0e8b089073..09eb8ca0f87 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -59,7 +59,7 @@ describe('reporter', () => { } Reporter = proxyquire('../../src/appsec/reporter', { - '../plugins/util/web': web, + '../../../datadog-plugin-web/src/index': web, './telemetry': telemetry }) }) diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index 9719cb7f1fb..40abce20afc 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -59,7 +59,7 @@ describe('WAF Manager', () => { '../../rate_limiter': function () { return limiterStub }, '../../priority_sampler': { keepTrace }, '../../standalone/product': { ASM: 'ASM' }, - '../../plugins/util/web': webMock, + '../../../../datadog-plugin-web/src/index': webMock, '../telemetry': { updateRateLimitedMetric } }) waf.destroy() diff --git a/packages/dd-trace/test/plugins/util/web.spec.js b/packages/dd-trace/test/plugins/util/web.spec.js deleted file mode 100644 index db734551ba5..00000000000 --- a/packages/dd-trace/test/plugins/util/web.spec.js +++ /dev/null @@ -1,972 +0,0 @@ -'use strict' - -require('../../setup/tap') - -const types = require('../../../../../ext/types') -const kinds = require('../../../../../ext/kinds') -const tags = require('../../../../../ext/tags') -const { USER_REJECT } = require('../../../../../ext/priority') -const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../../../dd-trace/src/constants') - -const WEB = types.WEB -const SERVER = kinds.SERVER -const RESOURCE_NAME = tags.RESOURCE_NAME -const SERVICE_NAME = tags.SERVICE_NAME -const SPAN_TYPE = tags.SPAN_TYPE -const SPAN_KIND = tags.SPAN_KIND -const ERROR = tags.ERROR -const HTTP_METHOD = tags.HTTP_METHOD -const HTTP_URL = tags.HTTP_URL -const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE -const HTTP_ROUTE = tags.HTTP_ROUTE -const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS -const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS -const HTTP_USERAGENT = tags.HTTP_USERAGENT -const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP - -describe('plugins/util/web', () => { - let web - let tracer - let span - let req - let res - let end - let config - let tags - - beforeEach(() => { - // `req` should only have common properties exposed and not things like - // `socket` or `connection` since some libraries rely on fake objects that - // may not have those. - req = { - method: 'GET', - headers: { - host: 'localhost', - date: 'now' - } - } - end = sinon.stub() - res = { - end, - getHeader: sinon.stub(), - getHeaders: sinon.stub().returns({}), - setHeader: sinon.spy(), - writeHead: () => {} - } - res.getHeader.withArgs('server').returns('test') - config = { hooks: {} } - - tracer = require('../../..').init({ plugins: false }) - web = require('../../../src/plugins/util/web') - }) - - beforeEach(() => { - config = web.normalizeConfig(config) - }) - - describe('normalizeConfig', () => { - it('should set the correct defaults', () => { - const config = web.normalizeConfig({}) - - expect(config).to.have.property('headers') - expect(config.headers).to.be.an('array') - expect(config).to.have.property('validateStatus') - expect(config.validateStatus).to.be.a('function') - expect(config.validateStatus(200)).to.equal(true) - expect(config.validateStatus(500)).to.equal(false) - expect(config).to.have.property('hooks') - expect(config.hooks).to.be.an('object') - expect(config.hooks).to.have.property('request') - expect(config.hooks.request).to.be.a('function') - expect(config).to.have.property('queryStringObfuscation', true) - }) - - it('should use the shared config if set', () => { - const config = web.normalizeConfig({ - headers: ['test'], - validateStatus: code => false, - hooks: { - request: () => 'test' - } - }) - - expect(config.headers).to.deep.equal([['test', undefined]]) - expect(config.validateStatus(200)).to.equal(false) - expect(config).to.have.property('hooks') - expect(config.hooks.request()).to.equal('test') - }) - - describe('queryStringObfuscation', () => { - it('should keep booleans as is', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: false - }) - - expect(config).to.have.property('queryStringObfuscation', false) - }) - - it('should change to false when passed empty string', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: '' - }) - - expect(config).to.have.property('queryStringObfuscation', false) - }) - - it('should change to true when passed ".*"', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: '.*' - }) - - expect(config).to.have.property('queryStringObfuscation', true) - }) - - it('should convert to regex when passed valid string', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: 'a*' - }) - - expect(config).to.have.deep.property('queryStringObfuscation', /a*/gi) - }) - - it('should default to true when passed a bad regex', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: '(?)' - }) - - expect(config).to.have.property('queryStringObfuscation', true) - }) - }) - }) - - describe('instrument', () => { - describe('on request start', () => { - it('should set the parent from the request headers', () => { - req.headers = { - 'x-datadog-trace-id': '123', - 'x-datadog-parent-id': '456' - } - - web.instrument(tracer, config, req, res, 'test.request', span => { - expect(span.context()._traceId.toString(10)).to.equal('123') - expect(span.context()._parentId.toString(10)).to.equal('456') - }) - }) - - it('should set the service name', () => { - config.service = 'custom' - - web.instrument(tracer, config, req, res, 'test.request', span => { - expect(span.context()._tags).to.have.property(SERVICE_NAME, 'custom') - }) - }) - - it('should activate a scope with the span', () => { - web.instrument(tracer, config, req, res, 'test.request', span => { - expect(tracer.scope().active()).to.equal(span) - }) - }) - - it('should add request tags to the span', () => { - req.method = 'GET' - req.url = '/user/123' - req.headers['user-agent'] = 'curl' - req.headers['x-forwarded-for'] = '8.8.8.8' - res.statusCode = '200' - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [SPAN_TYPE]: WEB, - [HTTP_URL]: 'http://localhost/user/123', - [HTTP_METHOD]: 'GET', - [SPAN_KIND]: SERVER, - [HTTP_USERAGENT]: 'curl' - }) - }) - }) - - it('should add client ip tag to the span when enabled', () => { - req.headers['x-forwarded-for'] = '8.8.8.8' - - config.clientIpEnabled = true - - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [HTTP_CLIENT_IP]: '8.8.8.8' - }) - }) - }) - - it('should add custom client ip tag to the span when it is configured', () => { - req.headers['X-Forwad-For'] = '8.8.8.8' - - config.clientIpEnabled = true - config.clientIpHeader = 'X-Forwad-For' - - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [HTTP_CLIENT_IP]: '8.8.8.8' - }) - }) - }) - - it('should not add custom client ip tag to the span when it is not configured', () => { - req.headers['X-Forwad-For'] = '8.8.8.8' - - config.clientIpEnabled = true - - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.not.have.property(HTTP_CLIENT_IP) - }) - }) - - it('should not add client ip tag to the span when disabled', () => { - req.headers['x-forwarded-for'] = '8.8.8.8' - - config.clientIpEnabled = false - - web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.not.have.property(HTTP_CLIENT_IP) - }) - }) - - it('should not replace client ip when it exists', () => { - req.headers['x-forwarded-for'] = '8.8.8.8' - - config.clientIpEnabled = true - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - span.setTag(HTTP_CLIENT_IP, '1.1.1.1') - - res.end() - - expect(tags).to.include({ - [HTTP_CLIENT_IP]: '1.1.1.1' - }) - }) - }) - - it('should not add client ip tag when no candidate header is present in request', () => { - config.clientIpEnabled = true - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.not.have.property(HTTP_CLIENT_IP) - }) - }) - - it('should add configured headers to the span tags', () => { - req.headers.req = 'incoming' - req.headers.res = 'outgoing' - config.headers = ['host', 'req:http.req', 'server', 'res:http.res'] - config = web.normalizeConfig(config) - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [`${HTTP_REQUEST_HEADERS}.host`]: 'localhost', - 'http.req': 'incoming', - [`${HTTP_RESPONSE_HEADERS}.server`]: 'test', - 'http.res': 'outgoing' - }) - }) - }) - - it('should only start one span for the entire request', () => { - web.instrument(tracer, config, req, res, 'test.request', span1 => { - web.instrument(tracer, config, req, res, 'test.request', span2 => { - expect(span1).to.equal(span2) - }) - }) - }) - - it('should allow overriding the span name', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { - web.instrument(tracer, config, req, res, 'test2.request', span => { - expect(span.context()._name).to.equal('test2.request') - }) - }) - }) - - it('should allow overriding the span service name', () => { - web.instrument(tracer, config, req, res, 'test.request', span => { - config.service = 'test2' - web.instrument(tracer, config, req, res, 'test.request') - - expect(span.context()._tags).to.have.property('service.name', 'test2') - }) - }) - - it('should only wrap res.end once', () => { - web.instrument(tracer, config, req, res, 'test.request') - const end = res.end - web.instrument(tracer, config, req, res, 'test.request') - - expect(end).to.equal(res.end) - }) - - it('should use the config from the last call', () => { - config.headers = ['host'] - - const override = web.normalizeConfig({ - headers: ['date'] - }) - - web.instrument(tracer, config, req, res, 'test.request', () => { - web.instrument(tracer, override, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [`${HTTP_REQUEST_HEADERS}.date`]: 'now' - }) - }) - }) - }) - - it('should obfuscate the query string from the URL', () => { - const config = web.normalizeConfig({ - queryStringObfuscation: 'secret=.*?(&|$)' - }) - - req.method = 'GET' - req.url = '/user/123?secret=password&foo=bar' - res.statusCode = '200' - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [HTTP_URL]: 'http://localhost/user/123?foo=bar' - }) - }) - }) - - it('should handle CORS preflight', () => { - const headers = [ - 'x-datadog-origin', - 'x-datadog-parent-id', - 'x-datadog-sampled', - 'x-datadog-sampling-priority', - 'x-datadog-trace-id', - 'x-datadog-tags' - ].join(',') - - req.method = 'OPTIONS' - req.headers.origin = 'http://test.com' - req.headers['access-control-request-headers'] = headers - - res.getHeaders.returns({ - 'access-control-allow-origin': 'http://test.com' - }) - - web.instrument(tracer, config, req, res, 'test.request') - - res.writeHead() - - expect(res.setHeader).to.have.been.calledWith('access-control-allow-headers', headers) - }) - - it('should handle CORS preflight with partial headers', () => { - const headers = [ - 'x-datadog-parent-id', - 'x-datadog-trace-id' - ].join(',') - - req.method = 'OPTIONS' - req.headers.origin = 'http://test.com' - req.headers['access-control-request-headers'] = headers - - res.getHeaders.returns({ - 'access-control-allow-origin': 'http://test.com' - }) - - web.instrument(tracer, config, req, res, 'test.request') - - res.writeHead() - - expect(res.setHeader).to.have.been.calledWith('access-control-allow-headers', headers) - }) - - it('should handle CORS preflight when the origin does not match', () => { - const headers = ['x-datadog-trace-id'] - - req.method = 'OPTIONS' - req.headers.origin = 'http://test.com' - req.headers['access-control-request-headers'] = headers - - web.instrument(tracer, config, req, res, 'test.request') - - res.writeHead() - - expect(res.setHeader).to.not.have.been.called - }) - - it('should handle CORS preflight when no header was requested', () => { - req.method = 'OPTIONS' - req.headers.origin = 'http://test.com' - - res.getHeaders.returns({ - 'access-control-allow-origin': 'http://test.com' - }) - - web.instrument(tracer, config, req, res, 'test.request') - - res.writeHead() - - expect(res.setHeader).to.not.have.been.called - }) - - it('should support https', () => { - req.url = '/user/123' - req.headers['user-agent'] = 'curl' - req.headers['x-forwarded-for'] = '8.8.8.8' - req.socket = { encrypted: true } - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [SPAN_TYPE]: WEB, - [HTTP_URL]: 'https://localhost/user/123', - [HTTP_METHOD]: 'GET', - [SPAN_KIND]: SERVER, - [HTTP_USERAGENT]: 'curl' - }) - }) - }) - - it('should support HTTP2 compatibility API', () => { - req.stream = {} - req.method = 'GET' - req.headers = { - ':scheme': 'https', - ':authority': 'localhost', - ':method': 'GET', - ':path': '/user/123', - 'user-agent': 'curl', - 'x-forwarded-for': '8.8.8.8' - } - res.statusCode = '200' - - web.instrument(tracer, config, req, res, 'test.request', span => { - const tags = span.context()._tags - - res.end() - - expect(tags).to.include({ - [SPAN_TYPE]: WEB, - [HTTP_URL]: 'https://localhost/user/123', - [HTTP_METHOD]: 'GET', - [SPAN_KIND]: SERVER, - [HTTP_USERAGENT]: 'curl' - }) - }) - }) - - it('should drop filtered out requests', () => { - config.filter = () => false - - web.instrument(tracer, config, req, res, 'test.request', span => { - const sampling = span.context()._sampling - - res.end() - - expect(sampling).to.have.property('priority', USER_REJECT) - }) - }) - }) - - describe('on request end', () => { - beforeEach(() => { - web.instrument(tracer, config, req, res, 'test.request', reqSpan => { - span = reqSpan - tags = span.context()._tags - }) - }) - - it('should finish the request span', () => { - sinon.spy(span, 'finish') - - res.end() - - expect(span.finish).to.have.been.called - }) - - it('should should only finish once', () => { - sinon.spy(span, 'finish') - - res.end() - res.end() - - expect(span.finish).to.have.been.calledOnce - }) - - it('should finish middleware spans', () => { - web.wrapMiddleware(req, () => {}, 'middleware', () => { - const span = tracer.scope().active() - - sinon.spy(span, 'finish') - - res.end() - - expect(span.finish).to.have.been.called - }) - }) - - it('should execute any beforeEnd handlers', () => { - const spy1 = sinon.spy() - const spy2 = sinon.spy() - - web.beforeEnd(req, spy1) - web.beforeEnd(req, spy2) - - res.end() - - expect(spy1).to.have.been.called - expect(spy2).to.have.been.called - }) - - it('should call the original end', () => { - res.end() - - expect(end).to.have.been.called - }) - - it('should add response tags to the span', () => { - req.method = 'GET' - req.url = '/user/123' - res.statusCode = 200 - - res.end() - - expect(tags).to.include({ - [RESOURCE_NAME]: 'GET', - [HTTP_STATUS_CODE]: 200 - }) - }) - - it('should set the error tag if the request is an error', () => { - res.statusCode = 500 - - res.end() - - expect(tags).to.include({ - [ERROR]: true - }) - }) - - it('should set the error tag if the configured validator returns false', () => { - config.validateStatus = () => false - - res.end() - - expect(tags).to.include({ - [ERROR]: true - }) - }) - - it('should use the user provided route', () => { - span.setTag('http.route', '/custom/route') - - res.end() - - expect(tags).to.include({ - [HTTP_ROUTE]: '/custom/route' - }) - }) - - it('should execute the request end hook', () => { - config.hooks.request = sinon.spy() - - res.end() - - expect(config.hooks.request).to.have.been.calledWith(span, req, res) - }) - - it('should execute multiple end hooks', () => { - config.hooks = { - request: sinon.spy() - } - - web.instrument(tracer, config, req, res, 'test.request', span => { - res.end() - - expect(config.hooks.request).to.have.been.calledWith(span, req, res) - }) - }) - - it('should set the resource name from the http.route tag set in the hooks', () => { - config.hooks = { - request: span => span.setTag('http.route', '/custom/route') - } - - web.instrument(tracer, config, req, res, 'test.request', span => { - res.end() - - expect(tags).to.have.property('resource.name', 'GET /custom/route') - }) - }) - }) - }) - - describe('enterRoute', () => { - beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { - span = tracer.scope().active() - tags = span.context()._tags - }) - }) - - it('should add a route segment that will be added to the span resource name', () => { - req.method = 'GET' - - web.enterRoute(req, '/foo') - web.enterRoute(req, '/bar') - res.end() - - expect(tags).to.have.property(RESOURCE_NAME, 'GET /foo/bar') - expect(tags).to.have.property(HTTP_ROUTE, '/foo/bar') - }) - - it('should only add valid route segments to the span resource name', () => { - req.method = 'GET' - - web.enterRoute(req) - web.enterRoute(req, 1337) - res.end() - - expect(tags).to.have.property(RESOURCE_NAME, 'GET') - expect(tags).to.not.have.property(HTTP_ROUTE) - }) - }) - - describe('exitRoute', () => { - beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', reqSpan => { - span = reqSpan - tags = span.context()._tags - }) - }) - - it('should remove a route segment', () => { - req.method = 'GET' - - web.enterRoute(req, '/foo') - web.enterRoute(req, '/bar') - web.exitRoute(req) - res.end() - - expect(tags).to.have.property(RESOURCE_NAME, 'GET /foo') - }) - }) - - describe('wrapMiddleware', () => { - beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { - span = tracer.scope().active() - tags = span.context()._tags - }) - }) - - it('should activate a scope with the span', (done) => { - const fn = function test () { - expect(tracer.scope().active()).to.not.equal(span) - done() - } - - web.wrapMiddleware(req, fn, 'middleware', () => fn(req, res)) - }) - }) - - describe('finish', () => { - beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { - span = tracer.scope().active() - tags = span.context()._tags - }) - }) - - it('should finish the span of the current middleware', (done) => { - const fn = () => { - const span = tracer.scope().active() - - sinon.spy(span, 'finish') - web.finish(req, fn, 'middleware') - - expect(span.finish).to.have.been.called - - done() - } - - web.wrapMiddleware(req, fn, 'middleware', () => fn(req, res)) - }) - - it('should add an error if provided', (done) => { - const fn = () => { - const span = tracer.scope().active() - const tags = span.context()._tags - const error = new Error('boom') - - sinon.spy(span, 'finish') - web.finish(req, error) - - expect(tags[ERROR_TYPE]).to.equal(error.name) - expect(tags[ERROR_MESSAGE]).to.equal(error.message) - expect(tags[ERROR_STACK]).to.equal(error.stack) - - done() - } - - web.wrapMiddleware(req, fn, 'middleware', () => fn(req, res)) - }) - }) - - describe('root', () => { - it('should return the request root span', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { - const span = tracer.scope().active() - - web.wrapMiddleware(req, () => {}, 'express.middleware', () => { - expect(web.root(req)).to.equal(span) - }) - }) - }) - - it('should return null when not yet instrumented', () => { - expect(web.root(req)).to.be.null - }) - }) - - describe('active', () => { - it('should return the request span by default', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { - expect(web.active(req)).to.equal(tracer.scope().active()) - }) - }) - - it('should return the active middleware span', () => { - web.instrument(tracer, config, req, res, 'test.request', () => { - const span = tracer.scope().active() - - web.wrapMiddleware(req, () => {}, 'express.middleware', () => { - expect(web.active(req)).to.not.be.null - expect(web.active(req)).to.not.equal(span) - }) - }) - }) - - it('should return null when not yet instrumented', () => { - expect(web.active(req)).to.be.null - }) - }) - - describe('addError', () => { - beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { - span = tracer.scope().active() - tags = span.context()._tags - }) - }) - - it('should add an error to the request span', () => { - const error = new Error('boom') - - web.addError(req, error) - web.addStatusError(req, 500) - - expect(tags).to.include({ - [ERROR]: error - }) - }) - - it('should override an existing error', () => { - const error = new Error('boom') - - web.addError(req, new Error('prrr')) - web.addError(req, error) - web.addStatusError(req, 500) - - expect(tags).to.include({ - [ERROR]: error - }) - }) - }) - - describe('addStatusError', () => { - beforeEach(() => { - config = web.normalizeConfig(config) - web.instrument(tracer, config, req, res, 'test.request', () => { - span = tracer.scope().active() - tags = span.context()._tags - }) - }) - - it('should flag the request as an error', () => { - web.addStatusError(req, 500) - - expect(tags).to.include({ - [ERROR]: true - }) - }) - - it('should only flag requests as an error for configured status codes', () => { - config.validateStatus = () => true - - web.addStatusError(req, 500) - - expect(tags).to.not.have.property(ERROR) - }) - }) - - describe('allowlistFilter', () => { - beforeEach(() => { - config = { allowlist: ['/_okay'] } - config = web.normalizeConfig(config) - }) - - it('should not filter the url', () => { - const filtered = config.filter('/_okay') - expect(filtered).to.equal(true) - }) - - it('should filter the url', () => { - const filtered = config.filter('/_notokay') - expect(filtered).to.equal(false) - }) - }) - - describe('whitelistFilter', () => { - beforeEach(() => { - config = { whitelist: ['/_okay'] } - config = web.normalizeConfig(config) - }) - - it('should not filter the url', () => { - const filtered = config.filter('/_okay') - expect(filtered).to.equal(true) - }) - - it('should filter the url', () => { - const filtered = config.filter('/_notokay') - expect(filtered).to.equal(false) - }) - }) - - describe('blocklistFilter', () => { - beforeEach(() => { - config = { blocklist: ['/_notokay'] } - config = web.normalizeConfig(config) - }) - - it('should not filter the url', () => { - const filtered = config.filter('/_okay') - expect(filtered).to.equal(true) - }) - - it('should filter the url', () => { - const filtered = config.filter('/_notokay') - expect(filtered).to.equal(false) - }) - }) - - describe('blacklistFilter', () => { - beforeEach(() => { - config = { blacklist: ['/_notokay'] } - config = web.normalizeConfig(config) - }) - - it('should not filter the url', () => { - const filtered = config.filter('/_okay') - expect(filtered).to.equal(true) - }) - - it('should filter the url', () => { - const filtered = config.filter('/_notokay') - expect(filtered).to.equal(false) - }) - }) - - describe('obfuscateQs', () => { - const url = 'http://perdu.com/path/' - const qs = '?data=secret' - - let config - - beforeEach(() => { - config = { - queryStringObfuscation: /secret/gi - } - }) - - it('should not obfuscate when passed false', () => { - config.queryStringObfuscation = false - - const result = web.obfuscateQs(config, url + qs) - - expect(result).to.equal(url + qs) - }) - - it('should not obfuscate when no querystring is found', () => { - const result = web.obfuscateQs(config, url) - - expect(result).to.equal(url) - }) - - it('should remove the querystring if passed true', () => { - config.queryStringObfuscation = true - - const result = web.obfuscateQs(config, url + qs) - - expect(result).to.equal(url) - }) - - it('should obfuscate only the querystring part of the url', () => { - const result = web.obfuscateQs(config, url + 'secret/' + qs) - - expect(result).to.equal(url + 'secret/?data=') - }) - }) -}) From e62354fe1c5a05862f5a4c5280d8fd33bf64db73 Mon Sep 17 00:00:00 2001 From: William Conti Date: Mon, 11 Aug 2025 13:07:12 -0400 Subject: [PATCH 08/28] more changes --- packages/datadog-plugin-web/src/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 9a23f74fe34..0092124da08 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -42,8 +42,8 @@ class WebPlugin extends TracingPlugin { static kind = SERVER static type = WEB - constructor (...args) { - super(...args) + constructor (tracer, config) { + super(tracer, config) this.addSub('apm:http:server:request:error', ({ req, error }) => { if (error) { this.addError(req, error) @@ -66,7 +66,7 @@ class WebPlugin extends TracingPlugin { this.addError(req, error) }) - this.configure(this.config) + this.configure(config) } configure (config) { @@ -112,6 +112,8 @@ class WebPlugin extends TracingPlugin { const context = contexts.get(req) if (!context) return + context.config = this.config + const span = context.span if (!span) return @@ -143,7 +145,7 @@ class WebPlugin extends TracingPlugin { context.span = span context.res = res - this.setConfig(req) + // this.setConfig(req) this._addRequestTags(context, this.constructor.type) return span From be5aa10b5672a457289cb6ecb667e77215db7f82 Mon Sep 17 00:00:00 2001 From: William Conti Date: Mon, 11 Aug 2025 13:13:28 -0400 Subject: [PATCH 09/28] another change --- packages/datadog-plugin-web/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 0092124da08..b0ab0c42340 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -145,7 +145,7 @@ class WebPlugin extends TracingPlugin { context.span = span context.res = res - // this.setConfig(req) + this.setConfig(req) this._addRequestTags(context, this.constructor.type) return span From 897b0863173d7b668a4283e5aab8518f2363a488 Mon Sep 17 00:00:00 2001 From: William Conti Date: Mon, 11 Aug 2025 15:01:43 -0400 Subject: [PATCH 10/28] more changes --- packages/datadog-instrumentations/src/hapi.js | 2 +- .../test/index.spec.js | 2 +- .../tests/index.spec.js | 268 ------------------ .../src/index.js | 3 +- packages/datadog-plugin-hapi/src/index.js | 6 +- .../src/index.js | 64 ++--- packages/datadog-plugin-web/src/index.js | 21 +- packages/dd-trace/src/plugins/index.js | 1 + .../src/supported-configurations.json | 1 + 9 files changed, 55 insertions(+), 313 deletions(-) delete mode 100644 packages/datadog-plugin-aws-apigateway/tests/index.spec.js diff --git a/packages/datadog-instrumentations/src/hapi.js b/packages/datadog-instrumentations/src/hapi.js index a7f04a6e470..0c229de644c 100644 --- a/packages/datadog-instrumentations/src/hapi.js +++ b/packages/datadog-instrumentations/src/hapi.js @@ -108,7 +108,7 @@ function onPreResponse (request, h) { const req = request.raw.req if (request.response instanceof Error) { - errorChannel.publish(request.response) + errorChannel.publish({ req, error: request.response }) } if (request.route) { diff --git a/packages/datadog-plugin-aws-apigateway/test/index.spec.js b/packages/datadog-plugin-aws-apigateway/test/index.spec.js index 0ebd0bf9446..89c71dc0c25 100644 --- a/packages/datadog-plugin-aws-apigateway/test/index.spec.js +++ b/packages/datadog-plugin-aws-apigateway/test/index.spec.js @@ -232,7 +232,7 @@ describe('Plugin', function () { // we can't force re-init the tracer, so we have to set the config manually const tracer = require('../../dd-trace').init(options) - tracer._config.inferredProxyServicesEnabled = false + tracer._tracer._config.inferredProxyServicesEnabled = false await agent.load( ['http', 'dns', 'net', 'aws-apigateway'], diff --git a/packages/datadog-plugin-aws-apigateway/tests/index.spec.js b/packages/datadog-plugin-aws-apigateway/tests/index.spec.js deleted file mode 100644 index 51969fda544..00000000000 --- a/packages/datadog-plugin-aws-apigateway/tests/index.spec.js +++ /dev/null @@ -1,268 +0,0 @@ -'use strict' - -const agent = require('../../dd-trace/test/plugins/agent') -const { expect } = require('chai') -const axios = require('axios') -const { Agent } = require('http') - -// Create axios instance with no connection pooling -const httpClient = axios.create({ - httpAgent: new Agent({ keepAlive: false }), - timeout: 5000 -}) - -describe('Plugin', function () { - let http - let appListener - let controller - let port - - // tap was throwing timeout errors when trying to use hooks like `before`, so instead we just use this function - // and call before the test starts - const loadTest = async function ({ inferredProxyServicesEnabled = true } = {}) { - const options = { - inferredProxyServicesEnabled, - service: 'aws-server' - } - - await agent.load( - ['http', 'dns', 'net', 'aws-apigateway'], - [{ client: false }, { enabled: false }, { enabled: false }, { enabled: true }], - options - ) - - // we can't force re-init the tracer, so we have to set the config manually - const tracer = require('../../../../dd-trace').init(options) - tracer._tracer._config.inferredProxyServicesEnabled = inferredProxyServicesEnabled - - http = require('http') - - const server = new http.Server(async (req, res) => { - controller && await controller(req, res) - if (req.url === '/error') { - res.statusCode = 500 - res.end(JSON.stringify({ message: 'ERROR' })) - } else { - res.writeHead(200) - res.end(JSON.stringify({ message: 'OK' })) - } - }) - - // Force close connections when server closes - const connections = new Set() - server.on('connection', (connection) => { - connections.add(connection) - connection.on('close', () => { - connections.delete(connection) - }) - }) - - return new Promise((resolve, reject) => { - appListener = server.listen(0, '127.0.0.1', () => { - port = server.address().port - appListener._connections = connections - resolve() - }) - }) - } - - const cleanupTest = async function () { - controller = null - - if (appListener) { - // Force close all existing connections - if (appListener._connections) { - for (const connection of appListener._connections) { - connection.destroy() - } - } - - await new Promise((resolve, reject) => { - appListener.close((err) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - appListener = null - } - - await agent.close() - } - - const inferredHeaders = { - 'x-dd-proxy': 'aws-apigateway', - 'x-dd-proxy-request-time-ms': '1729780025473', - 'x-dd-proxy-path': '/test', - 'x-dd-proxy-httpmethod': 'GET', - 'x-dd-proxy-domain-name': 'example.com', - 'x-dd-proxy-stage': 'dev' - } - - afterEach(async () => { - await cleanupTest() - }) - - describe('without configuration', () => { - it('should create a parent span and a child span for a 200', async () => { - await loadTest({}) - - await httpClient.get(`http://127.0.0.1:${port}/`, { - headers: inferredHeaders - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - - expect(spans.length).to.be.equal(2) - - expect(spans[0]).to.have.property('name', 'aws.apigateway') - expect(spans[0]).to.have.property('service', 'example.com') - expect(spans[0]).to.have.property('resource', 'GET /test') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0].meta).to.have.property('http.url', 'example.com/test') - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('component', 'aws-apigateway') - expect(spans[0].meta).to.have.property('_dd.integration', 'aws-apigateway') - expect(spans[0].metrics).to.have.property('_dd.inferred_span', 1) - expect(spans[0].start.toString()).to.be.equal('1729780025472999936') - - expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) - - expect(spans[1]).to.have.property('name', 'web.request') - expect(spans[1]).to.have.property('service', 'aws-server') - expect(spans[1]).to.have.property('type', 'web') - expect(spans[1]).to.have.property('resource', 'GET') - expect(spans[1].meta).to.have.property('component', 'http') - expect(spans[1].meta).to.have.property('span.kind', 'server') - expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/`) - expect(spans[1].meta).to.have.property('http.method', 'GET') - expect(spans[1].meta).to.have.property('http.status_code', '200') - expect(spans[1].meta).to.have.property('span.kind', 'server') - }) - }) - - it('should create a parent span and a child span for an error', async () => { - await loadTest({}) - - await httpClient.get(`http://127.0.0.1:${port}/error`, { - headers: inferredHeaders, - validateStatus: function (status) { - return status === 500 - } - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - expect(spans.length).to.be.equal(2) - - expect(spans[0]).to.have.property('name', 'aws.apigateway') - expect(spans[0]).to.have.property('service', 'example.com') - expect(spans[0]).to.have.property('resource', 'GET /test') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0].meta).to.have.property('http.url', 'example.com/test') - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '500') - expect(spans[0].meta).to.have.property('component', 'aws-apigateway') - expect(spans[0].error).to.be.equal(1) - expect(spans[0].start.toString()).to.be.equal('1729780025472999936') - expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) - - expect(spans[1]).to.have.property('name', 'web.request') - expect(spans[1]).to.have.property('service', 'aws-server') - expect(spans[1]).to.have.property('type', 'web') - expect(spans[1]).to.have.property('resource', 'GET') - expect(spans[1].meta).to.have.property('component', 'http') - expect(spans[1].meta).to.have.property('span.kind', 'server') - expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/error`) - expect(spans[1].meta).to.have.property('http.method', 'GET') - expect(spans[1].meta).to.have.property('http.status_code', '500') - expect(spans[1].meta).to.have.property('span.kind', 'server') - expect(spans[1].error).to.be.equal(1) - }) - }) - - it('should not create an API Gateway span if all necessary headers are missing', async () => { - await loadTest({}) - - await httpClient.get(`http://127.0.0.1:${port}/no-aws-headers`, { - headers: {} - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - expect(spans.length).to.be.equal(1) - - expect(spans[0]).to.have.property('name', 'web.request') - expect(spans[0]).to.have.property('service', 'aws-server') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0]).to.have.property('resource', 'GET') - expect(spans[0].meta).to.have.property('component', 'http') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/no-aws-headers`) - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].error).to.be.equal(0) - }) - }) - - it('should not create an API Gateway span if missing the proxy system header', async () => { - await loadTest({}) - - // remove x-dd-proxy from headers - const { 'x-dd-proxy': _, ...newHeaders } = inferredHeaders - - await httpClient.get(`http://127.0.0.1:${port}/a-few-aws-headers`, { - headers: newHeaders - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - expect(spans.length).to.be.equal(1) - - expect(spans[0]).to.have.property('name', 'web.request') - expect(spans[0]).to.have.property('service', 'aws-server') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0]).to.have.property('resource', 'GET') - expect(spans[0].meta).to.have.property('component', 'http') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/a-few-aws-headers`) - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].error).to.be.equal(0) - }) - }) - }) - - describe('with configuration', function () { - it('should not create a span when configured to be off', async () => { - await loadTest({ inferredProxyServicesEnabled: false }) - - await httpClient.get(`http://127.0.0.1:${port}/configured-off`, { - headers: inferredHeaders - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - - expect(spans.length).to.be.equal(1) - - expect(spans[0]).to.have.property('name', 'web.request') - expect(spans[0]).to.have.property('service', 'aws-server') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0]).to.have.property('resource', 'GET') - expect(spans[0].meta).to.have.property('component', 'http') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/configured-off`) - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'server') - }) - }) - }) -}) diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index 93daba89e4d..5d9953f8ce5 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -1,7 +1,6 @@ 'use strict' const WebPlugin = require('../../datadog-plugin-web/src') -const serverless = require('../../dd-trace/src/plugins/util/serverless') const triggerMap = { deleteRequest: 'Http', @@ -56,7 +55,7 @@ class AzureFunctionsPlugin extends WebPlugin { context.res = { statusCode: result.status } context.span = ctx.currentStore.span - serverless.finishSpan(context) + this.finishSpan(context) // Fallback for other trigger types } else { super.finish() diff --git a/packages/datadog-plugin-hapi/src/index.js b/packages/datadog-plugin-hapi/src/index.js index 348f0acb8b1..c0bbf5806d4 100644 --- a/packages/datadog-plugin-hapi/src/index.js +++ b/packages/datadog-plugin-hapi/src/index.js @@ -7,7 +7,7 @@ class HapiPlugin extends RouterPlugin { static id = 'hapi' constructor (...args) { - super(...args) + super(...args, false) this._requestSpans = new WeakMap() @@ -19,9 +19,9 @@ class HapiPlugin extends RouterPlugin { this._requestSpans.set(req, span) }) - this.addSub('apm:hapi:request:error', error => { + this.addSub('apm:hapi:request:error', ({ req, error }) => { if (!error || !error.isBoom || !this.config.validateStatus(error.output.statusCode)) { - this.addError(error) + this.addError(req, error) } }) diff --git a/packages/datadog-plugin-inferred-proxy/src/index.js b/packages/datadog-plugin-inferred-proxy/src/index.js index 1ce455b4583..82124100184 100644 --- a/packages/datadog-plugin-inferred-proxy/src/index.js +++ b/packages/datadog-plugin-inferred-proxy/src/index.js @@ -1,8 +1,9 @@ 'use strict' -const WebPlugin = require('../../datadog-plugin-web/src') const log = require('../../dd-trace/src/log') const tags = require('../../../ext/tags') +const dc = require('dc-polyfill') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const RESOURCE_NAME = tags.RESOURCE_NAME const SPAN_TYPE = tags.SPAN_TYPE @@ -16,40 +17,42 @@ const PROXY_HEADER_HTTPMETHOD = 'x-dd-proxy-httpmethod' const PROXY_HEADER_DOMAIN = 'x-dd-proxy-domain-name' const PROXY_HEADER_STAGE = 'x-dd-proxy-stage' -class InferredProxyPlugin extends WebPlugin { +const loadChannel = dc.channel('dd-trace:instrumentation:load') + +class InferredProxyPlugin extends TracingPlugin { static id = 'inferred-proxy' + static supportedProxies = { + 'aws-apigateway': { + spanName: 'aws.apigateway', + component: 'aws-apigateway' + } + } constructor (...args) { super(...args) - - this.supportedProxies = { - 'aws-apigateway': { - spanName: 'aws.apigateway', - component: 'aws-apigateway' - } - } + this.addSub(`apm:${this.constructor.id}:request:handle`, this.startSpan.bind(this)) } - startChildSpan (tracer, config, name, req, traceCtx) { - const headers = req.headers - const reqCtx = this.getContext(req) - let childOf = super.startChildSpan(tracer, config, name, req, traceCtx) - - if (!headers) { - return childOf + static maybeCreateInferredProxySpan (config, req, reqCtx, childOf, traceCtx) { + if (!config?.inferredProxyServicesEnabled) { + return } - - if (!tracer._config?.inferredProxyServicesEnabled) { - return childOf + const proxyContext = InferredProxyPlugin.extractInferredProxyContext(req.headers) + if (!proxyContext) { + return } - const proxyContext = this.extractInferredProxyContext(headers) + const channel = dc.channel(`apm:${proxyContext.proxySystemName}:request:handle`) - if (!proxyContext) { - return childOf + if (!channel.hasSubscribers) { + loadChannel.publish({ name: proxyContext.proxySystemName }) } - const proxySpanInfo = this.supportedProxies[proxyContext.proxySystemName] + return channel.publish({ reqCtx, proxyContext, childOf, traceCtx }) + } + + startSpan ({ reqCtx, proxyContext, childOf, traceCtx }) { + const proxySpanInfo = InferredProxyPlugin.supportedProxies[proxyContext.proxySystemName] log.debug('Successfully extracted inferred span info %s for proxy:', proxyContext, proxyContext.proxySystemName) @@ -59,14 +62,14 @@ class InferredProxyPlugin extends WebPlugin { startTime: proxyContext.requestTime, integrationName: proxySpanInfo.component, meta: { - service: proxyContext.domainName || tracer._config.service, + service: proxyContext.domainName || this.config.service, component: proxySpanInfo.component, [SPAN_TYPE]: 'web', [HTTP_METHOD]: proxyContext.method, [HTTP_URL]: proxyContext.domainName + proxyContext.path, stage: proxyContext.stage } - }, traceCtx, config) + }, traceCtx) reqCtx.inferredProxySpan = span childOf = span @@ -84,12 +87,12 @@ class InferredProxyPlugin extends WebPlugin { return span } - extractInferredProxyContext (headers) { + static extractInferredProxyContext (headers) { if (!(PROXY_HEADER_START_TIME_MS in headers)) { return null } - if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in this.supportedProxies)) { + if (!(PROXY_HEADER_SYSTEM in headers && headers[PROXY_HEADER_SYSTEM] in InferredProxyPlugin.supportedProxies)) { log.debug('Received headers to create inferred proxy span but headers include an unsupported proxy type', headers) return null } @@ -106,12 +109,7 @@ class InferredProxyPlugin extends WebPlugin { } } - finishAll (context) { - super.finishAll(context) - this.finishInferredProxySpan(context) - } - - finishInferredProxySpan (context) { + static finishInferredProxySpan (context) { const { req } = context if (!context.inferredProxySpan) return diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index b0ab0c42340..0a243a6239d 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -10,6 +10,7 @@ const kinds = require('../../../ext/kinds') const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const TracingPlugin = require('../../dd-trace/src/plugins/tracing') +const InferredProxyPlugin = require('../../datadog-plugin-inferred-proxy/src') let extractIp @@ -42,7 +43,7 @@ class WebPlugin extends TracingPlugin { static kind = SERVER static type = WEB - constructor (tracer, config) { + constructor (tracer, config, bindError = true) { super(tracer, config) this.addSub('apm:http:server:request:error', ({ req, error }) => { if (error) { @@ -62,9 +63,11 @@ class WebPlugin extends TracingPlugin { this.setRoute(req, route) }) - this.addSub(`apm:${this.constructor.id}:request:error`, ({ req, error }) => { - this.addError(req, error) - }) + if (bindError) { + this.addSub(`apm:${this.constructor.id}:request:error`, ({ req, error }) => { + this.addError(req, error) + }) + } this.configure(config) } @@ -303,7 +306,13 @@ class WebPlugin extends TracingPlugin { // Extract the parent span from the headers and start a new span as its child startChildSpan (name, req, traceCtx) { const headers = req.headers - const childOf = this.tracer.extract(FORMAT_HTTP_HEADERS, headers) + const reqCtx = this.getContext(req) + let childOf = this.tracer.extract(FORMAT_HTTP_HEADERS, headers) + + InferredProxyPlugin.maybeCreateInferredProxySpan(this._tracerConfig, req, reqCtx, childOf, traceCtx) + if (reqCtx.inferredProxySpan) { + childOf = reqCtx.inferredProxySpan + } const span = super.startSpan(name, { childOf, kind: this.constructor.kind, type: this.constructor.type }, traceCtx) @@ -374,6 +383,8 @@ class WebPlugin extends TracingPlugin { this.finishMiddleware(context) this.finishSpan(context) + + InferredProxyPlugin.finishInferredProxySpan(context) } _obfuscateQs (url, config) { diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 164fe36fde3..0a97c38189c 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -29,6 +29,7 @@ module.exports = { get amqp10 () { return require('../../../datadog-plugin-amqp10/src') }, get amqplib () { return require('../../../datadog-plugin-amqplib/src') }, get avsc () { return require('../../../datadog-plugin-avsc/src') }, + get 'aws-apigateway' () { return require('../../../datadog-plugin-aws-apigateway/src') }, get 'aws-sdk' () { return require('../../../datadog-plugin-aws-sdk/src') }, get bunyan () { return require('../../../datadog-plugin-bunyan/src') }, get 'cassandra-driver' () { return require('../../../datadog-plugin-cassandra-driver/src') }, diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 8e2f5a175c4..6c8e2c08391 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -174,6 +174,7 @@ "DD_TRACE_APOLLO_SUBGRAPH_ENABLED": ["A"], "DD_TRACE_AVSC_ENABLED": ["A"], "DD_TRACE_AWS_ADD_SPAN_POINTERS": ["A"], + "DD_TRACE_AWS_APIGATEWAY_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_ENABLED": ["A"], "DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED": ["A"], From a502928737733e91bf343aa80c572e001ba49e36 Mon Sep 17 00:00:00 2001 From: William Conti Date: Mon, 11 Aug 2025 15:28:25 -0400 Subject: [PATCH 11/28] more fixes --- packages/datadog-plugin-aws-apigateway/test/index.spec.js | 7 +++---- packages/datadog-plugin-web/src/index.js | 6 +++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/datadog-plugin-aws-apigateway/test/index.spec.js b/packages/datadog-plugin-aws-apigateway/test/index.spec.js index 89c71dc0c25..03fd78f6967 100644 --- a/packages/datadog-plugin-aws-apigateway/test/index.spec.js +++ b/packages/datadog-plugin-aws-apigateway/test/index.spec.js @@ -59,14 +59,13 @@ describe('Plugin', function () { service: 'aws-server' } - require('../../dd-trace').init(options) - await agent.load( - ['http', 'dns', 'net', 'aws-apigateway'], - [{ client: false }, { enabled: false }, { enabled: false }, { enabled: true }], + ['http', 'dns', 'net', 'tcp', 'aws-apigateway'], + [{ client: false }, { enabled: false }, { enabled: false }, { enabled: false }, { enabled: true }], options ) + require('../../dd-trace').init(options) http = require('http') const server = new http.Server(async (req, res) => { diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 0a243a6239d..e29883a9b31 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -257,7 +257,7 @@ class WebPlugin extends TracingPlugin { } // Prepare the request for instrumentation. - patch (req) { + static patch (req) { let context = contexts.get(req) if (context) return context @@ -283,6 +283,10 @@ class WebPlugin extends TracingPlugin { return context } + patch (req) { + return WebPlugin.patch(req) + } + // Return the request root span. static root (req) { const context = contexts.get(req) From 80953ded650097dc73c1336c266d8d22dab3c4da Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 12:34:39 -0400 Subject: [PATCH 12/28] fix next --- packages/datadog-plugin-next/src/index.js | 11 ++++++----- packages/datadog-plugin-web/src/index.js | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index 0dd9feecd69..322f7dbbb47 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -1,5 +1,6 @@ 'use strict' +const ServerPlugin = require('../../dd-trace/src/plugins/server') const WebPlugin = require('../../datadog-plugin-web/src') const { storage } = require('../../datadog-core') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') @@ -7,7 +8,7 @@ const { COMPONENT } = require('../../dd-trace/src/constants') const errorPages = new Set(['/404', '/500', '/_error', '/_not-found', '/_not-found/page']) -class NextPlugin extends WebPlugin { +class NextPlugin extends ServerPlugin { static id = 'next' constructor (...args) { @@ -62,15 +63,15 @@ class NextPlugin extends WebPlugin { if (requestError) { // prioritize user-set errors from API routes span.setTag('error', requestError) - this.addError(req, requestError) + WebPlugin.addError(req, requestError) } else if (error) { // general error handling span.setTag('error', error) - this.addError(req, requestError || error) + WebPlugin.addError(req, requestError || error) } else if (!this.config.validateStatus(res.statusCode)) { // where there's no error, we still need to validate status span.setTag('error', true) - this.addError(req, true) + WebPlugin.addError(req, true) } span.addTags({ @@ -118,7 +119,7 @@ class NextPlugin extends WebPlugin { 'resource.name': `${req.method} ${page}`.trim(), 'next.page': page }) - this.setRoute(req, page) + WebPlugin.setRoute(req, page) } configure (config) { diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index e29883a9b31..58cc33018ae 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -184,6 +184,10 @@ class WebPlugin extends TracingPlugin { } setRoute (req, path) { + WebPlugin.setRoute(req, path) + } + + static setRoute (req, path) { const context = contexts.get(req) if (!context) return @@ -345,6 +349,10 @@ class WebPlugin extends TracingPlugin { // Add an error to the request addError (req, error) { + WebPlugin.addError(req, error) + } + + static addError (req, error) { if (error instanceof Error) { const context = contexts.get(req) From ffeba31c92f278d2abc8bf839798fb28243e9edd Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 13:09:48 -0400 Subject: [PATCH 13/28] fix http --- packages/datadog-plugin-http/src/server.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index 4888ece9de1..ca572257aa5 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -15,14 +15,13 @@ class HttpServerPlugin extends WebPlugin { super(...args) this._parentStore = undefined this.addTraceSub('exit', message => this.exit(message)) - this.config = { - ...this.config, - service: this.config.service || this.serviceName(), - } } start ({ req, res, abortController }) { const store = storage('legacy').getStore() + + this.config.service = this.config.service || this.getServiceName() + const span = this.startSpan( req, res, From 1384fdc57247e3533aa54c4b3b11276752b02476 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 13:16:15 -0400 Subject: [PATCH 14/28] more fixes --- packages/datadog-plugin-http/src/server.js | 2 +- packages/dd-trace/test/plugins/plugin-structure.spec.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/datadog-plugin-http/src/server.js b/packages/datadog-plugin-http/src/server.js index ca572257aa5..91db229a9ef 100644 --- a/packages/datadog-plugin-http/src/server.js +++ b/packages/datadog-plugin-http/src/server.js @@ -20,7 +20,7 @@ class HttpServerPlugin extends WebPlugin { start ({ req, res, abortController }) { const store = storage('legacy').getStore() - this.config.service = this.config.service || this.getServiceName() + this.config.service = this.config.service || this.serviceName() const span = this.startSpan( req, diff --git a/packages/dd-trace/test/plugins/plugin-structure.spec.js b/packages/dd-trace/test/plugins/plugin-structure.spec.js index 4f0ec779ec1..65ea4539f9b 100644 --- a/packages/dd-trace/test/plugins/plugin-structure.spec.js +++ b/packages/dd-trace/test/plugins/plugin-structure.spec.js @@ -10,7 +10,9 @@ const path = require('path') const hooks = require('../../../datadog-instrumentations/src/helpers/hooks') const abstractPlugins = [ - 'web' // web is an abstract plugin, and will not have an instrumentation file + 'web', // web is an abstract plugin, and will not have an instrumentation file + 'aws-apigateway', // aws-apigateway is an inferred proxy plugin, and will not have an instrumentation file + 'inferred-proxy' // inferred-proxy is an inferred proxy plugin, and will not have an instrumentation file ] // we have some plugin directories that we don't actually have a tracing plugin for, but exist for special cases @@ -18,7 +20,6 @@ const abstractPlugins = [ const missingPlugins = [ 'datadog-plugin-axios', // we test axios to ensure our functionality works with axios, see: https://github.com/DataDog/dd-trace-js/pull/1469 'datadog-plugin-limitd-client', // limitd-client instrumentation handles trace context propagation, no tracing is done - 'datadog-plugin-mongoose' // mongoose tracing is done through mongodb-core instrumentation ] // instrumentations that do not have a hook, but are still instrumented From 7aefa14e20835868bebd4817fb33ed3a891fbd53 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 13:38:11 -0400 Subject: [PATCH 15/28] more fixes --- .../src/index.js | 2 +- .../datadog-plugin-router/test/index.spec.js | 5 +- .../test/plugins/util/inferred_proxy.spec.js | 270 ------------------ 3 files changed, 3 insertions(+), 274 deletions(-) delete mode 100644 packages/dd-trace/test/plugins/util/inferred_proxy.spec.js diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index 5d9953f8ce5..ba914ee6de6 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -23,7 +23,7 @@ class AzureFunctionsPlugin extends WebPlugin { bindStart (ctx) { const childOf = extractTraceContext(this._tracer, ctx) const meta = getMetaForTrigger(ctx) - const span = this.startSpan(this.operationName(), { + const span = this.startSpan(this.operationName(), null, { childOf, service: this.serviceName(), type: 'serverless', diff --git a/packages/datadog-plugin-router/test/index.spec.js b/packages/datadog-plugin-router/test/index.spec.js index 35cb50498ca..5939d7b0977 100644 --- a/packages/datadog-plugin-router/test/index.spec.js +++ b/packages/datadog-plugin-router/test/index.spec.js @@ -6,7 +6,6 @@ const axios = require('axios') const http = require('http') const { once } = require('events') const agent = require('../../dd-trace/test/plugins/agent') -const web = require('../../datadog-plugin-web/src') const { withVersions } = require('../../dd-trace/test/setup/mocha') const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1) @@ -25,9 +24,9 @@ describe('Plugin', () => { function server (router, errorHandler = defaultErrorHandler) { return http.createServer((req, res) => { - const config = web.normalizeConfig({}) + const web = tracer._pluginManager._pluginsByName.router - web.instrument(tracer, config, req, res, 'web.request') + web.instrument(req, res, 'web.request') return router(req, res, errorHandler(req, res)) }) diff --git a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js b/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js deleted file mode 100644 index 14d85ccb5ce..00000000000 --- a/packages/dd-trace/test/plugins/util/inferred_proxy.spec.js +++ /dev/null @@ -1,270 +0,0 @@ -'use strict' - -require('../../setup/tap') - -const agent = require('../agent') -const { expect } = require('chai') -const axios = require('axios') -const { Agent } = require('http') - -// Create axios instance with no connection pooling -const httpClient = axios.create({ - httpAgent: new Agent({ keepAlive: false }), - timeout: 5000 -}) - -describe('Inferred Proxy Spans', function () { - let http - let appListener - let controller - let port - - // tap was throwing timeout errors when trying to use hooks like `before`, so instead we just use this function - // and call before the test starts - const loadTest = async function ({ inferredProxyServicesEnabled = true } = {}) { - const options = { - inferredProxyServicesEnabled, - service: 'aws-server' - } - - await agent.load( - ['http', 'dns', 'net'], - [{ client: false }, { enabled: false }, { enabled: false }], - options - ) - - // we can't force re-init the tracer, so we have to set the config manually - const tracer = require('../../../../dd-trace').init(options) - tracer._tracer._config.inferredProxyServicesEnabled = inferredProxyServicesEnabled - - http = require('http') - - const server = new http.Server(async (req, res) => { - controller && await controller(req, res) - if (req.url === '/error') { - res.statusCode = 500 - res.end(JSON.stringify({ message: 'ERROR' })) - } else { - res.writeHead(200) - res.end(JSON.stringify({ message: 'OK' })) - } - }) - - // Force close connections when server closes - const connections = new Set() - server.on('connection', (connection) => { - connections.add(connection) - connection.on('close', () => { - connections.delete(connection) - }) - }) - - return new Promise((resolve, reject) => { - appListener = server.listen(0, '127.0.0.1', () => { - port = server.address().port - appListener._connections = connections - resolve() - }) - }) - } - - const cleanupTest = async function () { - controller = null - - if (appListener) { - // Force close all existing connections - if (appListener._connections) { - for (const connection of appListener._connections) { - connection.destroy() - } - } - - await new Promise((resolve, reject) => { - appListener.close((err) => { - if (err) { - reject(err) - } else { - resolve() - } - }) - }) - appListener = null - } - - await agent.close() - } - - const inferredHeaders = { - 'x-dd-proxy': 'aws-apigateway', - 'x-dd-proxy-request-time-ms': '1729780025473', - 'x-dd-proxy-path': '/test', - 'x-dd-proxy-httpmethod': 'GET', - 'x-dd-proxy-domain-name': 'example.com', - 'x-dd-proxy-stage': 'dev' - } - - afterEach(async () => { - await cleanupTest() - }) - - describe('without configuration', () => { - it('should create a parent span and a child span for a 200', async () => { - await loadTest({}) - - await httpClient.get(`http://127.0.0.1:${port}/`, { - headers: inferredHeaders - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - - expect(spans.length).to.be.equal(2) - - expect(spans[0]).to.have.property('name', 'aws.apigateway') - expect(spans[0]).to.have.property('service', 'example.com') - expect(spans[0]).to.have.property('resource', 'GET /test') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0].meta).to.have.property('http.url', 'example.com/test') - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('component', 'aws-apigateway') - expect(spans[0].meta).to.have.property('_dd.integration', 'aws-apigateway') - expect(spans[0].metrics).to.have.property('_dd.inferred_span', 1) - expect(spans[0].start.toString()).to.be.equal('1729780025472999936') - - expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) - - expect(spans[1]).to.have.property('name', 'web.request') - expect(spans[1]).to.have.property('service', 'aws-server') - expect(spans[1]).to.have.property('type', 'web') - expect(spans[1]).to.have.property('resource', 'GET') - expect(spans[1].meta).to.have.property('component', 'http') - expect(spans[1].meta).to.have.property('span.kind', 'server') - expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/`) - expect(spans[1].meta).to.have.property('http.method', 'GET') - expect(spans[1].meta).to.have.property('http.status_code', '200') - expect(spans[1].meta).to.have.property('span.kind', 'server') - }) - }) - - it('should create a parent span and a child span for an error', async () => { - await loadTest({}) - - await httpClient.get(`http://127.0.0.1:${port}/error`, { - headers: inferredHeaders, - validateStatus: function (status) { - return status === 500 - } - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - expect(spans.length).to.be.equal(2) - - expect(spans[0]).to.have.property('name', 'aws.apigateway') - expect(spans[0]).to.have.property('service', 'example.com') - expect(spans[0]).to.have.property('resource', 'GET /test') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0].meta).to.have.property('http.url', 'example.com/test') - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '500') - expect(spans[0].meta).to.have.property('component', 'aws-apigateway') - expect(spans[0].error).to.be.equal(1) - expect(spans[0].start.toString()).to.be.equal('1729780025472999936') - expect(spans[0].span_id.toString()).to.be.equal(spans[1].parent_id.toString()) - - expect(spans[1]).to.have.property('name', 'web.request') - expect(spans[1]).to.have.property('service', 'aws-server') - expect(spans[1]).to.have.property('type', 'web') - expect(spans[1]).to.have.property('resource', 'GET') - expect(spans[1].meta).to.have.property('component', 'http') - expect(spans[1].meta).to.have.property('span.kind', 'server') - expect(spans[1].meta).to.have.property('http.url', `http://127.0.0.1:${port}/error`) - expect(spans[1].meta).to.have.property('http.method', 'GET') - expect(spans[1].meta).to.have.property('http.status_code', '500') - expect(spans[1].meta).to.have.property('span.kind', 'server') - expect(spans[1].error).to.be.equal(1) - }) - }) - - it('should not create an API Gateway span if all necessary headers are missing', async () => { - await loadTest({}) - - await httpClient.get(`http://127.0.0.1:${port}/no-aws-headers`, { - headers: {} - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - expect(spans.length).to.be.equal(1) - - expect(spans[0]).to.have.property('name', 'web.request') - expect(spans[0]).to.have.property('service', 'aws-server') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0]).to.have.property('resource', 'GET') - expect(spans[0].meta).to.have.property('component', 'http') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/no-aws-headers`) - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].error).to.be.equal(0) - }) - }) - - it('should not create an API Gateway span if missing the proxy system header', async () => { - await loadTest({}) - - // remove x-dd-proxy from headers - const { 'x-dd-proxy': _, ...newHeaders } = inferredHeaders - - await httpClient.get(`http://127.0.0.1:${port}/a-few-aws-headers`, { - headers: newHeaders - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - expect(spans.length).to.be.equal(1) - - expect(spans[0]).to.have.property('name', 'web.request') - expect(spans[0]).to.have.property('service', 'aws-server') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0]).to.have.property('resource', 'GET') - expect(spans[0].meta).to.have.property('component', 'http') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/a-few-aws-headers`) - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].error).to.be.equal(0) - }) - }) - }) - - describe('with configuration', function () { - it('should not create a span when configured to be off', async () => { - await loadTest({ inferredProxyServicesEnabled: false }) - - await httpClient.get(`http://127.0.0.1:${port}/configured-off`, { - headers: inferredHeaders - }) - - await agent.assertSomeTraces(traces => { - const spans = traces[0] - - expect(spans.length).to.be.equal(1) - - expect(spans[0]).to.have.property('name', 'web.request') - expect(spans[0]).to.have.property('service', 'aws-server') - expect(spans[0]).to.have.property('type', 'web') - expect(spans[0]).to.have.property('resource', 'GET') - expect(spans[0].meta).to.have.property('component', 'http') - expect(spans[0].meta).to.have.property('span.kind', 'server') - expect(spans[0].meta).to.have.property('http.url', `http://127.0.0.1:${port}/configured-off`) - expect(spans[0].meta).to.have.property('http.method', 'GET') - expect(spans[0].meta).to.have.property('http.status_code', '200') - expect(spans[0].meta).to.have.property('span.kind', 'server') - }) - }) - }) -}) From 76bcb7f7db6f6273cd84f70d5a159695de880831 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 13:54:43 -0400 Subject: [PATCH 16/28] fix azure functions --- packages/datadog-plugin-azure-functions/src/index.js | 11 ++++++----- packages/datadog-plugin-web/src/index.js | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index ba914ee6de6..aa694a013af 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -1,6 +1,7 @@ 'use strict' const WebPlugin = require('../../datadog-plugin-web/src') +const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const triggerMap = { deleteRequest: 'Http', @@ -13,7 +14,7 @@ const triggerMap = { serviceBusTopic: 'ServiceBus', } -class AzureFunctionsPlugin extends WebPlugin { +class AzureFunctionsPlugin extends TracingPlugin { static id = 'azure-functions' static operation = 'invoke' static kind = 'server' @@ -23,7 +24,7 @@ class AzureFunctionsPlugin extends WebPlugin { bindStart (ctx) { const childOf = extractTraceContext(this._tracer, ctx) const meta = getMetaForTrigger(ctx) - const span = this.startSpan(this.operationName(), null, { + const span = this.startSpan(this.operationName(), { childOf, service: this.serviceName(), type: 'serverless', @@ -49,13 +50,13 @@ class AzureFunctionsPlugin extends WebPlugin { headers: Object.fromEntries(httpRequest.headers), url: path } - const context = this.patch(req) + const context = WebPlugin.patch(req) context.config = this.config context.paths = [path] context.res = { statusCode: result.status } context.span = ctx.currentStore.span - this.finishSpan(context) + WebPlugin.finishSpan(context) // Fallback for other trigger types } else { super.finish() @@ -63,7 +64,7 @@ class AzureFunctionsPlugin extends WebPlugin { } configure (config) { - return super.configure(this.normalizeConfig(config)) + return super.configure(WebPlugin.normalizeConfig(config)) } } diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 58cc33018ae..bccb2a3aa89 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -373,6 +373,10 @@ class WebPlugin extends TracingPlugin { } finishSpan (context) { + WebPlugin.finishSpan(context) + } + + static finishSpan (context) { const { req, res } = context if (context.finished && !req.stream) return From ff46aabefcfef69d96c399942af70f9ad23cd953 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 14:02:48 -0400 Subject: [PATCH 17/28] another fix --- packages/datadog-plugin-web/src/index.js | 144 +++++++++++------------ 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index bccb2a3aa89..786920b57e4 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -149,7 +149,7 @@ class WebPlugin extends TracingPlugin { context.res = res this.setConfig(req) - this._addRequestTags(context, this.constructor.type) + _addRequestTags(context) return span } @@ -327,26 +327,6 @@ class WebPlugin extends TracingPlugin { return span } - // Validate a request's status code and then add error tags if necessary - addStatusError (req, statusCode) { - const context = contexts.get(req) - const { span, inferredProxySpan, error } = context - - const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] - const inferredSpanContext = inferredProxySpan?.context() - const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] - - const isValidStatusCode = context.config.validateStatus(statusCode) - - if (!spanHasExistingError && !isValidStatusCode) { - span.setTag(ERROR, error || true) - } - - if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { - inferredProxySpan.setTag(ERROR, error || true) - } - } - // Add an error to the request addError (req, error) { WebPlugin.addError(req, error) @@ -381,8 +361,8 @@ class WebPlugin extends TracingPlugin { if (context.finished && !req.stream) return - this._addRequestTags(context, this.constructor.type) - this._addResponseTags(context) + _addRequestTags(context) + _addResponseTags(context) context.config.hooks.request(context.span, req, res) addResourceTag(context) @@ -403,24 +383,6 @@ class WebPlugin extends TracingPlugin { InferredProxyPlugin.finishInferredProxySpan(context) } - _obfuscateQs (url, config) { - const { queryStringObfuscation } = config - - if (queryStringObfuscation === false) return url - - const i = url.indexOf('?') - if (i === -1) return url - - const path = url.slice(0, i) - if (queryStringObfuscation === true) return path - - let qs = url.slice(i + 1) - - qs = qs.replace(queryStringObfuscation, '') - - return `${path}?${qs}` - } - wrapWriteHead (context) { const { req, res } = context const writeHead = res.writeHead @@ -473,48 +435,86 @@ class WebPlugin extends TracingPlugin { } }) } +} - _addRequestTags (context, spanType) { - const { req, span, inferredProxySpan } = context - const url = extractURL(req) +function _obfuscateQs (url, config) { + const { queryStringObfuscation } = config - span.addTags({ - [HTTP_URL]: this._obfuscateQs(url, context.config), - [HTTP_METHOD]: req.method, - [SPAN_KIND]: SERVER, - [SPAN_TYPE]: spanType, - [HTTP_USERAGENT]: req.headers['user-agent'] - }) + if (queryStringObfuscation === false) return url - // if client ip has already been set by appsec, no need to run it again - if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { - const clientIp = extractIp(context.config, req) + const i = url.indexOf('?') + if (i === -1) return url - if (clientIp) { - span.setTag(HTTP_CLIENT_IP, clientIp) - inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) - } + const path = url.slice(0, i) + if (queryStringObfuscation === true) return path + + let qs = url.slice(i + 1) + + qs = qs.replace(queryStringObfuscation, '') + + return `${path}?${qs}` +} + +function _addRequestTags (context) { + const { req, span, inferredProxySpan } = context + const url = extractURL(req) + + span.addTags({ + [HTTP_URL]: _obfuscateQs(url, context.config), + [HTTP_METHOD]: req.method, + [SPAN_KIND]: SERVER, + [SPAN_TYPE]: SPAN_TYPE, + [HTTP_USERAGENT]: req.headers['user-agent'] + }) + + // if client ip has already been set by appsec, no need to run it again + if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { + const clientIp = extractIp(context.config, req) + + if (clientIp) { + span.setTag(HTTP_CLIENT_IP, clientIp) + inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) } + } + + addHeaders(context) +} + +function _addResponseTags (context) { + const { req, res, paths, span, inferredProxySpan } = context - addHeaders(context) + const route = paths.join('') + if (route) { + span.setTag(HTTP_ROUTE, route) } - _addResponseTags (context) { - const { req, res, paths, span, inferredProxySpan } = context + span.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + inferredProxySpan?.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) - const route = paths.join('') - if (route) { - span.setTag(HTTP_ROUTE, route) - } + addStatusError(req, res.statusCode) +} - span.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - inferredProxySpan?.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) +// Validate a request's status code and then add error tags if necessary +function addStatusError (req, statusCode) { + const context = contexts.get(req) + const { span, inferredProxySpan, error } = context + + const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] + const inferredSpanContext = inferredProxySpan?.context() + const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] + + const isValidStatusCode = context.config.validateStatus(statusCode) + + if (!spanHasExistingError && !isValidStatusCode) { + span.setTag(ERROR, error || true) + } - this.addStatusError(req, res.statusCode) + if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { + inferredProxySpan.setTag(ERROR, error || true) } } From 64f2a00c6584622587249d571cdcf6027c1835b9 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 14:09:10 -0400 Subject: [PATCH 18/28] fix --- packages/datadog-plugin-web/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 786920b57e4..ce5a7392511 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -463,7 +463,7 @@ function _addRequestTags (context) { [HTTP_URL]: _obfuscateQs(url, context.config), [HTTP_METHOD]: req.method, [SPAN_KIND]: SERVER, - [SPAN_TYPE]: SPAN_TYPE, + [SPAN_TYPE]: WebPlugin.constructor.type, [HTTP_USERAGENT]: req.headers['user-agent'] }) From 18ce9e9057d45b05b08be30330c7b3d37944a71b Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 14:11:32 -0400 Subject: [PATCH 19/28] fix supported configurations --- packages/dd-trace/src/supported-configurations.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 6c8e2c08391..8e2f5a175c4 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -174,7 +174,6 @@ "DD_TRACE_APOLLO_SUBGRAPH_ENABLED": ["A"], "DD_TRACE_AVSC_ENABLED": ["A"], "DD_TRACE_AWS_ADD_SPAN_POINTERS": ["A"], - "DD_TRACE_AWS_APIGATEWAY_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_ENABLED": ["A"], "DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED": ["A"], From d12cf907aad026a529b89b4f1a8e7e7b08ebdc76 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 14:13:22 -0400 Subject: [PATCH 20/28] fix type --- packages/datadog-plugin-web/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index ce5a7392511..588c2026de3 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -463,7 +463,7 @@ function _addRequestTags (context) { [HTTP_URL]: _obfuscateQs(url, context.config), [HTTP_METHOD]: req.method, [SPAN_KIND]: SERVER, - [SPAN_TYPE]: WebPlugin.constructor.type, + [SPAN_TYPE]: WebPlugin.type, [HTTP_USERAGENT]: req.headers['user-agent'] }) From 74e4cede3c186d6c3ea6afe31d9bab8df77a5029 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 14:42:55 -0400 Subject: [PATCH 21/28] fix supported configs --- packages/datadog-plugin-web/src/index.js | 6 ++++++ packages/dd-trace/src/supported-configurations.json | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 588c2026de3..cac995c15c9 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -78,6 +78,11 @@ class WebPlugin extends TracingPlugin { // Ensure the configuration has the correct structure and defaults. normalizeConfig (config) { + return WebPlugin.normalizeConfig(config) + } + + + static normalizeConfig (config) { const headers = getHeadersToRecord(config) const validateStatus = getStatusValidator(config) const hooks = getHooks(config) @@ -97,6 +102,7 @@ class WebPlugin extends TracingPlugin { queryStringObfuscation } } + } setFramework (req, name) { const context = this.patch(req) diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 8e2f5a175c4..6c8e2c08391 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -174,6 +174,7 @@ "DD_TRACE_APOLLO_SUBGRAPH_ENABLED": ["A"], "DD_TRACE_AVSC_ENABLED": ["A"], "DD_TRACE_AWS_ADD_SPAN_POINTERS": ["A"], + "DD_TRACE_AWS_APIGATEWAY_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_BATCH_PROPAGATION_ENABLED": ["A"], "DD_TRACE_AWS_SDK_AWS_ENABLED": ["A"], "DD_TRACE_AWS_SDK_BATCH_PROPAGATION_ENABLED": ["A"], From ce4c4cfc3affa3e04116d2480981eb0f31d5ca65 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 14:48:17 -0400 Subject: [PATCH 22/28] fix lint --- packages/datadog-plugin-web/src/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index cac995c15c9..8921dde0f49 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -81,7 +81,6 @@ class WebPlugin extends TracingPlugin { return WebPlugin.normalizeConfig(config) } - static normalizeConfig (config) { const headers = getHeadersToRecord(config) const validateStatus = getStatusValidator(config) @@ -102,7 +101,6 @@ class WebPlugin extends TracingPlugin { queryStringObfuscation } } - } setFramework (req, name) { const context = this.patch(req) From b915d97efb993e4640a2805d66c7c1786d474e7a Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 15:14:14 -0400 Subject: [PATCH 23/28] refactor again --- .../src/index.js | 8 +- packages/datadog-plugin-web/src/index.js | 423 ++---------------- packages/datadog-plugin-web/src/utils.js | 377 ++++++++++++++++ .../datadog-plugin-web/test/index.spec.js | 17 +- .../src/appsec/api_security_sampler.js | 6 +- packages/dd-trace/src/appsec/graphql.js | 4 +- packages/dd-trace/src/appsec/iast/index.js | 6 +- .../src/appsec/iast/overhead-controller.js | 4 +- packages/dd-trace/src/appsec/index.js | 20 +- packages/dd-trace/src/appsec/rasp/index.js | 4 +- packages/dd-trace/src/appsec/rasp/utils.js | 4 +- packages/dd-trace/src/appsec/reporter.js | 10 +- packages/dd-trace/src/appsec/waf/index.js | 4 +- .../test/appsec/api_security_sampler.spec.js | 9 +- .../appsec/iast/overhead-controller.spec.js | 2 +- packages/dd-trace/test/appsec/index.spec.js | 9 +- .../dd-trace/test/appsec/rasp/lfi.spec.js | 2 +- .../dd-trace/test/appsec/rasp/utils.spec.js | 2 +- .../dd-trace/test/appsec/reporter.spec.js | 2 +- .../dd-trace/test/appsec/waf/index.spec.js | 2 +- 20 files changed, 481 insertions(+), 434 deletions(-) create mode 100644 packages/datadog-plugin-web/src/utils.js diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index aa694a013af..d85915f8dfd 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -1,6 +1,6 @@ 'use strict' -const WebPlugin = require('../../datadog-plugin-web/src') +const web = require('../../datadog-plugin-web/src/utils') const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const triggerMap = { @@ -50,13 +50,13 @@ class AzureFunctionsPlugin extends TracingPlugin { headers: Object.fromEntries(httpRequest.headers), url: path } - const context = WebPlugin.patch(req) + const context = web.patch(req) context.config = this.config context.paths = [path] context.res = { statusCode: result.status } context.span = ctx.currentStore.span - WebPlugin.finishSpan(context) + web.finishSpan(context) // Fallback for other trigger types } else { super.finish() @@ -64,7 +64,7 @@ class AzureFunctionsPlugin extends TracingPlugin { } configure (config) { - return super.configure(WebPlugin.normalizeConfig(config)) + return super.configure(web.normalizeConfig(config)) } } diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 8921dde0f49..9e4f871eb9a 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -1,42 +1,35 @@ 'use strict' -const uniq = require('../../datadog-core/src/utils/src/uniq') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') -const FORMAT_HTTP_HEADERS = 'http_headers' -const log = require('../../dd-trace/src/log') -const tags = require('../../../ext/tags') -const types = require('../../../ext/types') -const kinds = require('../../../ext/kinds') -const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') -const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') +const { + ERROR_MESSAGE, + ERROR_TYPE, + ERROR_STACK +} = require('../../dd-trace/src/constants') const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const InferredProxyPlugin = require('../../datadog-plugin-inferred-proxy/src') - -let extractIp - -const WEB = types.WEB -const SERVER = kinds.SERVER -const RESOURCE_NAME = tags.RESOURCE_NAME -const SERVICE_NAME = tags.SERVICE_NAME -const SPAN_TYPE = tags.SPAN_TYPE -const SPAN_KIND = tags.SPAN_KIND -const ERROR = tags.ERROR -const HTTP_METHOD = tags.HTTP_METHOD -const HTTP_URL = tags.HTTP_URL -const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE -const HTTP_ROUTE = tags.HTTP_ROUTE -const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS -const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS -const HTTP_USERAGENT = tags.HTTP_USERAGENT -const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP -const MANUAL_DROP = tags.MANUAL_DROP - -const HTTP2_HEADER_AUTHORITY = ':authority' -const HTTP2_HEADER_SCHEME = ':scheme' -const HTTP2_HEADER_PATH = ':path' - -const contexts = new WeakMap() -const ends = new WeakMap() +const web = require('./utils') + +const { + contexts, + ends, + normalizeConfig, + setRoute, + patch, + root, + getContext, + addError, + finishSpan, + addAllowHeaders, + isOriginAllowed, + reactivate, + WEB, + SERVER, + RESOURCE_NAME, + MANUAL_DROP, + SERVICE_NAME, + FORMAT_HTTP_HEADERS, +} = web class WebPlugin extends TracingPlugin { static id = WEB @@ -78,28 +71,7 @@ class WebPlugin extends TracingPlugin { // Ensure the configuration has the correct structure and defaults. normalizeConfig (config) { - return WebPlugin.normalizeConfig(config) - } - - static normalizeConfig (config) { - const headers = getHeadersToRecord(config) - const validateStatus = getStatusValidator(config) - const hooks = getHooks(config) - const filter = urlFilter.getFilter(config) - const middleware = getMiddlewareSetting(config) - const queryStringObfuscation = getQsObfuscator(config) - - extractIp = config.clientIpEnabled && require('../../dd-trace/src/plugins/util/ip_extractor').extractIp - - return { - ...config, - headers, - validateStatus, - hooks, - filter, - middleware, - queryStringObfuscation - } + return normalizeConfig(config) } setFramework (req, name) { @@ -153,7 +125,7 @@ class WebPlugin extends TracingPlugin { context.res = res this.setConfig(req) - _addRequestTags(context) + web._addRequestTags(context) return span } @@ -188,15 +160,7 @@ class WebPlugin extends TracingPlugin { } setRoute (req, path) { - WebPlugin.setRoute(req, path) - } - - static setRoute (req, path) { - const context = contexts.get(req) - - if (!context) return - - context.paths = [path] + setRoute(req, path) } // Remove the current route segment. @@ -216,7 +180,9 @@ class WebPlugin extends TracingPlugin { if (config.middleware === false) return this.bindAndWrapMiddlewareErrors(fn, req, tracer, childOf) - const span = super.startSpan(name, { childOf }, traceCtx) + const span = super.startSpan(name, { + childOf + }, traceCtx) analyticsSampler.sample(span, config.measured) @@ -264,45 +230,13 @@ class WebPlugin extends TracingPlugin { contexts.get(req).beforeEnd.push(callback) } - // Prepare the request for instrumentation. - static patch (req) { - let context = contexts.get(req) - - if (context) return context - - context = req.stream && contexts.get(req.stream) - - if (context) { - contexts.set(req, context) - return context - } - - context = { - req, - span: null, - paths: [], - middleware: [], - beforeEnd: [], - config: this.config - } - - contexts.set(req, context) - - return context - } - patch (req) { - return WebPlugin.patch(req) + return patch(req) } // Return the request root span. - static root (req) { - const context = contexts.get(req) - return context ? context.span : null - } - root (req) { - return WebPlugin.root(req) + return root(req) } // Return the active span. @@ -326,24 +260,18 @@ class WebPlugin extends TracingPlugin { childOf = reqCtx.inferredProxySpan } - const span = super.startSpan(name, { childOf, kind: this.constructor.kind, type: this.constructor.type }, traceCtx) + const span = super.startSpan(name, { + childOf, + kind: this.constructor.kind, + type: this.constructor.type + }, traceCtx) return span } // Add an error to the request addError (req, error) { - WebPlugin.addError(req, error) - } - - static addError (req, error) { - if (error instanceof Error) { - const context = contexts.get(req) - - if (context) { - context.error = error - } - } + addError(req, error) } finishMiddleware (context) { @@ -357,22 +285,7 @@ class WebPlugin extends TracingPlugin { } finishSpan (context) { - WebPlugin.finishSpan(context) - } - - static finishSpan (context) { - const { req, res } = context - - if (context.finished && !req.stream) return - - _addRequestTags(context) - _addResponseTags(context) - - context.config.hooks.request(context.span, req, res) - addResourceTag(context) - - context.span.finish() - context.finished = true + finishSpan(context) } finishAll (context) { @@ -388,7 +301,10 @@ class WebPlugin extends TracingPlugin { } wrapWriteHead (context) { - const { req, res } = context + const { + req, + res + } = context const writeHead = res.writeHead return function (statusCode, statusMessage, headers) { @@ -403,12 +319,8 @@ class WebPlugin extends TracingPlugin { } } - static getContext (req) { - return contexts.get(req) - } - getContext (req) { - return WebPlugin.getContext(req) + return getContext(req) } wrapRes (context, req, res, end) { @@ -441,244 +353,5 @@ class WebPlugin extends TracingPlugin { } } -function _obfuscateQs (url, config) { - const { queryStringObfuscation } = config - - if (queryStringObfuscation === false) return url - - const i = url.indexOf('?') - if (i === -1) return url - - const path = url.slice(0, i) - if (queryStringObfuscation === true) return path - - let qs = url.slice(i + 1) - - qs = qs.replace(queryStringObfuscation, '') - - return `${path}?${qs}` -} - -function _addRequestTags (context) { - const { req, span, inferredProxySpan } = context - const url = extractURL(req) - - span.addTags({ - [HTTP_URL]: _obfuscateQs(url, context.config), - [HTTP_METHOD]: req.method, - [SPAN_KIND]: SERVER, - [SPAN_TYPE]: WebPlugin.type, - [HTTP_USERAGENT]: req.headers['user-agent'] - }) - - // if client ip has already been set by appsec, no need to run it again - if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { - const clientIp = extractIp(context.config, req) - - if (clientIp) { - span.setTag(HTTP_CLIENT_IP, clientIp) - inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) - } - } - - addHeaders(context) -} - -function _addResponseTags (context) { - const { req, res, paths, span, inferredProxySpan } = context - - const route = paths.join('') - if (route) { - span.setTag(HTTP_ROUTE, route) - } - - span.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - inferredProxySpan?.addTags({ - [HTTP_STATUS_CODE]: res.statusCode - }) - - addStatusError(req, res.statusCode) -} - -// Validate a request's status code and then add error tags if necessary -function addStatusError (req, statusCode) { - const context = contexts.get(req) - const { span, inferredProxySpan, error } = context - - const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] - const inferredSpanContext = inferredProxySpan?.context() - const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] - - const isValidStatusCode = context.config.validateStatus(statusCode) - - if (!spanHasExistingError && !isValidStatusCode) { - span.setTag(ERROR, error || true) - } - - if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { - inferredProxySpan.setTag(ERROR, error || true) - } -} - -function addAllowHeaders (req, res, headers) { - const allowHeaders = splitHeader(headers['access-control-allow-headers']) - const requestHeaders = splitHeader(req.headers['access-control-request-headers']) - const contextHeaders = [ - 'x-datadog-origin', - 'x-datadog-parent-id', - 'x-datadog-sampled', // Deprecated, but still accept it in case it's sent. - 'x-datadog-sampling-priority', - 'x-datadog-trace-id', - 'x-datadog-tags' - ] - - for (const header of contextHeaders) { - if (requestHeaders.includes(header)) { - allowHeaders.push(header) - } - } - - if (allowHeaders.length > 0) { - res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(',')) - } -} - -function isOriginAllowed (req, headers) { - const origin = req.headers.origin - const allowOrigin = headers['access-control-allow-origin'] - - return origin && (allowOrigin === '*' || allowOrigin === origin) -} - -function splitHeader (str) { - return typeof str === 'string' ? str.split(/\s*,\s*/) : [] -} - -function reactivate (req, fn) { - const context = contexts.get(req) - - return context - ? context.tracer.scope().activate(context.span, fn) - : fn() -} - -function addResourceTag (context) { - const { req, span } = context - const tags = span.context()._tags - - if (tags['resource.name']) return - - const resource = [req.method, tags[HTTP_ROUTE]] - .filter(Boolean) - .join(' ') - - span.setTag(RESOURCE_NAME, resource) -} - -function addHeaders (context) { - const { req, res, config, span, inferredProxySpan } = context - - config.headers.forEach(([key, tag]) => { - const reqHeader = req.headers[key] - const resHeader = res.getHeader(key) - - if (reqHeader) { - span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) - inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) - } - - if (resHeader) { - span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) - inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) - } - }) -} - -function extractURL (req) { - const headers = req.headers - - if (req.stream) { - return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}` - } - const protocol = getProtocol(req) - return `${protocol}://${req.headers.host}${req.originalUrl || req.url}` -} - -function getProtocol (req) { - if (req.socket && req.socket.encrypted) return 'https' - if (req.connection && req.connection.encrypted) return 'https' - - return 'http' -} - -function getHeadersToRecord (config) { - if (Array.isArray(config.headers)) { - try { - return config.headers - .map(h => h.split(':')) - .map(([key, tag]) => [key.toLowerCase(), tag]) - } catch (err) { - log.error('Web plugin error getting headers', err) - } - } else if (config.hasOwnProperty('headers')) { - log.error('Expected `headers` to be an array of strings.') - } - return [] -} - -function getStatusValidator (config) { - if (typeof config.validateStatus === 'function') { - return config.validateStatus - } else if (config.hasOwnProperty('validateStatus')) { - log.error('Expected `validateStatus` to be a function.') - } - return code => code < 500 -} - -const noop = () => {} - -function getHooks (config) { - const request = config.hooks?.request ?? noop - - return { request } -} - -function getMiddlewareSetting (config) { - if (config && typeof config.middleware === 'boolean') { - return config.middleware - } else if (config && config.hasOwnProperty('middleware')) { - log.error('Expected `middleware` to be a boolean.') - } - - return true -} - -function getQsObfuscator (config) { - const obfuscator = config.queryStringObfuscation - - if (typeof obfuscator === 'boolean') { - return obfuscator - } - - if (typeof obfuscator === 'string') { - if (obfuscator === '') return false // disable obfuscator - - if (obfuscator === '.*') return true // optimize full redact - - try { - return new RegExp(obfuscator, 'gi') - } catch (err) { - log.error('Web plugin error getting qs obfuscator', err) - } - } - - if (config.hasOwnProperty('queryStringObfuscation')) { - log.error('Expected `queryStringObfuscation` to be a regex string or boolean.') - } - - return true -} - module.exports = WebPlugin +module.exports.static = web diff --git a/packages/datadog-plugin-web/src/utils.js b/packages/datadog-plugin-web/src/utils.js new file mode 100644 index 00000000000..1f8ffa6aa3b --- /dev/null +++ b/packages/datadog-plugin-web/src/utils.js @@ -0,0 +1,377 @@ +'use strict' + +const uniq = require('../../datadog-core/src/utils/src/uniq') +const log = require('../../dd-trace/src/log') +const tags = require('../../../ext/tags') +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') +const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') +const { ERROR_MESSAGE } = require('../../dd-trace/src/constants') + +let extractIp + +const WEB = types.WEB +const SERVER = kinds.SERVER +const RESOURCE_NAME = tags.RESOURCE_NAME +const SPAN_TYPE = tags.SPAN_TYPE +const SPAN_KIND = tags.SPAN_KIND +const ERROR = tags.ERROR +const HTTP_METHOD = tags.HTTP_METHOD +const HTTP_URL = tags.HTTP_URL +const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE +const HTTP_ROUTE = tags.HTTP_ROUTE +const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS +const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS +const HTTP_USERAGENT = tags.HTTP_USERAGENT +const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP + +const HTTP2_HEADER_AUTHORITY = ':authority' +const HTTP2_HEADER_SCHEME = ':scheme' +const HTTP2_HEADER_PATH = ':path' + +const contexts = new WeakMap() +const ends = new WeakMap() + +function normalizeConfig (config) { + const headers = getHeadersToRecord(config) + const validateStatus = getStatusValidator(config) + const hooks = getHooks(config) + const filter = urlFilter.getFilter(config) + const middleware = getMiddlewareSetting(config) + const queryStringObfuscation = getQsObfuscator(config) + + extractIp = config.clientIpEnabled && require('../../dd-trace/src/plugins/util/ip_extractor').extractIp + + return { + ...config, + headers, + validateStatus, + hooks, + filter, + middleware, + queryStringObfuscation + } +} + +function setRoute (req, path) { + const context = contexts.get(req) + + if (!context) return + + context.paths = [path] +} + +function patch (req) { + let context = contexts.get(req) + + if (context) return context + + context = req.stream && contexts.get(req.stream) + + if (context) { + contexts.set(req, context) + return context + } + + context = { + req, + span: null, + paths: [], + middleware: [], + beforeEnd: [], + config: this.config + } + + contexts.set(req, context) + + return context +} + +function root (req) { + const context = contexts.get(req) + return context ? context.span : null +} + +function getContext (req) { + return contexts.get(req) +} + +function addError (req, error) { + if (error instanceof Error) { + const context = contexts.get(req) + + if (context) { + context.error = error + } + } +} + +function finishSpan (context) { + const { req, res } = context + + if (context.finished && !req.stream) return + + _addRequestTags(context) + _addResponseTags(context) + + context.config.hooks.request(context.span, req, res) + addResourceTag(context) + + context.span.finish() + context.finished = true +} + +function _obfuscateQs (url, config) { + const { queryStringObfuscation } = config + + if (queryStringObfuscation === false) return url + + const i = url.indexOf('?') + if (i === -1) return url + + const path = url.slice(0, i) + if (queryStringObfuscation === true) return path + + let qs = url.slice(i + 1) + + qs = qs.replace(queryStringObfuscation, '') + + return `${path}?${qs}` +} + +function _addRequestTags (context) { + const { req, span, inferredProxySpan } = context + const url = extractURL(req) + + span.addTags({ + [HTTP_URL]: _obfuscateQs(url, context.config), + [HTTP_METHOD]: req.method, + [SPAN_KIND]: SERVER, + [SPAN_TYPE]: WEB, + [HTTP_USERAGENT]: req.headers['user-agent'] + }) + + // if client ip has already been set by appsec, no need to run it again + if (extractIp && !span.context()._tags.hasOwnProperty(HTTP_CLIENT_IP)) { + const clientIp = extractIp(context.config, req) + + if (clientIp) { + span.setTag(HTTP_CLIENT_IP, clientIp) + inferredProxySpan?.setTag(HTTP_CLIENT_IP, clientIp) + } + } + + addHeaders(context) +} + +function _addResponseTags (context) { + const { req, res, paths, span, inferredProxySpan } = context + + const route = paths.join('') + if (route) { + span.setTag(HTTP_ROUTE, route) + } + + span.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + inferredProxySpan?.addTags({ + [HTTP_STATUS_CODE]: res.statusCode + }) + + addStatusError(req, res.statusCode) +} + +function addStatusError (req, statusCode) { + const context = contexts.get(req) + const { span, inferredProxySpan, error } = context + + const spanHasExistingError = span.context()._tags.error || span.context()._tags[ERROR_MESSAGE] + const inferredSpanContext = inferredProxySpan?.context() + const inferredSpanHasExistingError = inferredSpanContext?._tags.error || inferredSpanContext?._tags[ERROR_MESSAGE] + + const isValidStatusCode = context.config.validateStatus(statusCode) + + if (!spanHasExistingError && !isValidStatusCode) { + span.setTag(ERROR, error || true) + } + + if (inferredProxySpan && !inferredSpanHasExistingError && !isValidStatusCode) { + inferredProxySpan.setTag(ERROR, error || true) + } +} + +function addAllowHeaders (req, res, headers) { + const allowHeaders = splitHeader(headers['access-control-allow-headers']) + const requestHeaders = splitHeader(req.headers['access-control-request-headers']) + const contextHeaders = [ + 'x-datadog-origin', + 'x-datadog-parent-id', + 'x-datadog-sampled', // Deprecated, but still accept it in case it's sent. + 'x-datadog-sampling-priority', + 'x-datadog-trace-id', + 'x-datadog-tags' + ] + + for (const header of contextHeaders) { + if (requestHeaders.includes(header)) { + allowHeaders.push(header) + } + } + + if (allowHeaders.length > 0) { + res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(',')) + } +} + +function isOriginAllowed (req, headers) { + const origin = req.headers.origin + const allowOrigin = headers['access-control-allow-origin'] + + return origin && (allowOrigin === '*' || allowOrigin === origin) +} + +function splitHeader (str) { + return typeof str === 'string' ? str.split(/\s*,\s*/) : [] +} + +function reactivate (req, fn) { + const context = contexts.get(req) + + return context + ? context.tracer.scope().activate(context.span, fn) + : fn() +} + +function addResourceTag (context) { + const { req, span } = context + const tags = span.context()._tags + + if (tags['resource.name']) return + + const resource = [req.method, tags[HTTP_ROUTE]] + .filter(Boolean) + .join(' ') + + span.setTag(RESOURCE_NAME, resource) +} + +function addHeaders (context) { + const { req, res, config, span, inferredProxySpan } = context + + config.headers.forEach(([key, tag]) => { + const reqHeader = req.headers[key] + const resHeader = res.getHeader(key) + + if (reqHeader) { + span.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) + inferredProxySpan?.setTag(tag || `${HTTP_REQUEST_HEADERS}.${key}`, reqHeader) + } + + if (resHeader) { + span.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) + inferredProxySpan?.setTag(tag || `${HTTP_RESPONSE_HEADERS}.${key}`, resHeader) + } + }) +} + +function extractURL (req) { + const headers = req.headers + + if (req.stream) { + return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}` + } + const protocol = getProtocol(req) + return `${protocol}://${req.headers.host}${req.originalUrl || req.url}` +} + +function getProtocol (req) { + if (req.socket && req.socket.encrypted) return 'https' + if (req.connection && req.connection.encrypted) return 'https' + + return 'http' +} + +function getHeadersToRecord (config) { + if (Array.isArray(config.headers)) { + try { + return config.headers + .map(h => h.split(':')) + .map(([key, tag]) => [key.toLowerCase(), tag]) + } catch (err) { + log.error('Web plugin error getting headers', err) + } + } else if (config.hasOwnProperty('headers')) { + log.error('Expected `headers` to be an array of strings.') + } + return [] +} + +function getStatusValidator (config) { + if (typeof config.validateStatus === 'function') { + return config.validateStatus + } else if (config.hasOwnProperty('validateStatus')) { + log.error('Expected `validateStatus` to be a function.') + } + return code => code < 500 +} + +const noop = () => {} + +function getHooks (config) { + const request = config.hooks?.request ?? noop + + return { request } +} + +function getMiddlewareSetting (config) { + if (config && typeof config.middleware === 'boolean') { + return config.middleware + } else if (config && config.hasOwnProperty('middleware')) { + log.error('Expected `middleware` to be a boolean.') + } + + return true +} + +function getQsObfuscator (config) { + const obfuscator = config.queryStringObfuscation + + if (typeof obfuscator === 'boolean') { + return obfuscator + } + + if (typeof obfuscator === 'string') { + if (obfuscator === '') return false // disable obfuscator + + if (obfuscator === '.*') return true // optimize full redact + + try { + return new RegExp(obfuscator, 'gi') + } catch (err) { + log.error('Web plugin error getting qs obfuscator', err) + } + } + + if (config.hasOwnProperty('queryStringObfuscation')) { + log.error('Expected `queryStringObfuscation` to be a regex string or boolean.') + } + + return true +} + +module.exports = { + normalizeConfig, + setRoute, + patch, + root, + getContext, + addError, + finishSpan, + _addRequestTags, + addAllowHeaders, + isOriginAllowed, + reactivate, + contexts, + ends +} diff --git a/packages/datadog-plugin-web/test/index.spec.js b/packages/datadog-plugin-web/test/index.spec.js index a487852584b..00826f6a149 100644 --- a/packages/datadog-plugin-web/test/index.spec.js +++ b/packages/datadog-plugin-web/test/index.spec.js @@ -24,6 +24,7 @@ const HTTP_CLIENT_IP = tags.HTTP_CLIENT_IP describe('Plugin', () => { let web + let webUtils let tracer let span let req @@ -57,11 +58,13 @@ describe('Plugin', () => { tracer = require('../../..').init({ plugins: false }) const WebPlugin = require('../src') web = new WebPlugin(tracer, config) + + webUtils = require('../src/utils') }) describe('normalizeConfig', () => { it('should set the correct defaults', () => { - const config = web.normalizeConfig({}) + const config = webUtils.normalizeConfig({}) expect(config).to.have.property('headers') expect(config.headers).to.be.an('array') @@ -77,7 +80,7 @@ describe('Plugin', () => { }) it('should use the shared config if set', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ headers: ['test'], validateStatus: code => false, hooks: { @@ -93,7 +96,7 @@ describe('Plugin', () => { describe('queryStringObfuscation', () => { it('should keep booleans as is', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: false }) @@ -101,7 +104,7 @@ describe('Plugin', () => { }) it('should change to false when passed empty string', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: '' }) @@ -109,7 +112,7 @@ describe('Plugin', () => { }) it('should change to true when passed ".*"', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: '.*' }) @@ -117,7 +120,7 @@ describe('Plugin', () => { }) it('should convert to regex when passed valid string', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: 'a*' }) @@ -125,7 +128,7 @@ describe('Plugin', () => { }) it('should default to true when passed a bad regex', () => { - const config = web.normalizeConfig({ + const config = webUtils.normalizeConfig({ queryStringObfuscation: '(?)' }) diff --git a/packages/dd-trace/src/appsec/api_security_sampler.js b/packages/dd-trace/src/appsec/api_security_sampler.js index a1dbbd7e870..e223a63c66b 100644 --- a/packages/dd-trace/src/appsec/api_security_sampler.js +++ b/packages/dd-trace/src/appsec/api_security_sampler.js @@ -1,7 +1,7 @@ 'use strict' const TTLCache = require('@isaacs/ttlcache') -const WebPlugin = require('../../../datadog-plugin-web/src') +const web = require('../../../datadog-plugin-web/src/utils') const log = require('../log') const { AUTO_REJECT, USER_REJECT } = require('../../../../ext/priority') const { keepTrace } = require('../priority_sampler') @@ -42,7 +42,7 @@ function sampleRequest (req, res, force = false) { const key = computeKey(req, res) if (!key || isSampled(key)) return false - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return false if (asmStandaloneEnabled) { @@ -71,7 +71,7 @@ function isSampled (key) { } function computeKey (req, res) { - const route = WebPlugin.getContext(req)?.paths?.join('') || '' + const route = web.getContext(req)?.paths?.join('') || '' const method = req.method const status = res.statusCode diff --git a/packages/dd-trace/src/appsec/graphql.js b/packages/dd-trace/src/appsec/graphql.js index 6a109184ad4..2c6aa6c9129 100644 --- a/packages/dd-trace/src/appsec/graphql.js +++ b/packages/dd-trace/src/appsec/graphql.js @@ -10,7 +10,7 @@ const { const log = require('../log') const waf = require('./waf') const addresses = require('./addresses') -const WebPlugin = require('../../../datadog-plugin-web/src') +const web = require('../../../datadog-plugin-web/src/utils') const { startGraphqlResolve, graphqlMiddlewareChannel, @@ -89,7 +89,7 @@ function beforeWriteApolloGraphqlResponse ({ abortController, abortData }) { const requestData = graphqlRequestData.get(req) if (requestData?.blocked) { - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return try { diff --git a/packages/dd-trace/src/appsec/iast/index.js b/packages/dd-trace/src/appsec/iast/index.js index 7d4d000cd3c..a6e255e1aba 100644 --- a/packages/dd-trace/src/appsec/iast/index.js +++ b/packages/dd-trace/src/appsec/iast/index.js @@ -2,7 +2,7 @@ const vulnerabilityReporter = require('./vulnerability-reporter') const { enableAllAnalyzers, disableAllAnalyzers } = require('./analyzers') -const WebPlugin = require('../../../../datadog-plugin-web/src') +const web = require('../../../../datadog-plugin-web/src/utils') const { storage } = require('../../../../datadog-core') const overheadController = require('./overhead-controller') const dc = require('dc-polyfill') @@ -65,7 +65,7 @@ function onIncomingHttpRequestStart (data) { if (data?.req) { const store = storage('legacy').getStore() if (store) { - const topContext = WebPlugin.getContext(data.req) + const topContext = web.getContext(data.req) if (topContext) { const rootSpan = topContext.span const isRequestAcquired = overheadController.acquireRequest(rootSpan) @@ -89,7 +89,7 @@ function onIncomingHttpRequestStart (data) { function onIncomingHttpRequestEnd (data) { if (data?.req) { const store = storage('legacy').getStore() - const topContext = WebPlugin.getContext(data.req) + const topContext = web.getContext(data.req) const iastContext = iastContextFunctions.getIastContext(store, topContext) if (iastContext?.rootSpan) { const storedHeaders = collectedResponseHeaders.get(data.res) || {} diff --git a/packages/dd-trace/src/appsec/iast/overhead-controller.js b/packages/dd-trace/src/appsec/iast/overhead-controller.js index 8e566772965..d7a0862f77a 100644 --- a/packages/dd-trace/src/appsec/iast/overhead-controller.js +++ b/packages/dd-trace/src/appsec/iast/overhead-controller.js @@ -1,7 +1,7 @@ 'use strict' const { LRUCache } = require('lru-cache') -const WebPlugin = require('../../../../datadog-plugin-web/src') +const web = require('../../../../datadog-plugin-web/src/utils') const vulnerabilities = require('./vulnerabilities') const OVERHEAD_CONTROLLER_CONTEXT_KEY = 'oce' @@ -97,7 +97,7 @@ function _getContext (iastContext) { if (iastContext?.[OVERHEAD_CONTROLLER_CONTEXT_KEY]) { const oceContext = iastContext[OVERHEAD_CONTROLLER_CONTEXT_KEY] if (!oceContext.webContext) { - oceContext.webContext = WebPlugin.getContext(iastContext.req) + oceContext.webContext = web.getContext(iastContext.req) oceContext.method = iastContext.req?.method } diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index 44fc84865b9..ba982907d70 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -31,7 +31,7 @@ const addresses = require('./addresses') const Reporter = require('./reporter') const appsecTelemetry = require('./telemetry') const apiSecuritySampler = require('./api_security_sampler') -const WebPlugin = require('../../../datadog-plugin-web/src') +const web = require('../../../datadog-plugin-web/src/utils') const { extractIp } = require('../plugins/util/ip_extractor') const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking') @@ -111,7 +111,7 @@ function onRequestBodyParsed ({ req, res, body, abortController }) { req = store?.req } - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return const results = waf.run({ @@ -126,7 +126,7 @@ function onRequestBodyParsed ({ req, res, body, abortController }) { function onRequestCookieParser ({ req, res, abortController, cookies }) { if (!cookies || typeof cookies !== 'object') return - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return const results = waf.run({ @@ -139,7 +139,7 @@ function onRequestCookieParser ({ req, res, abortController, cookies }) { } function incomingHttpStartTranslator ({ req, res, abortController }) { - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return const clientIp = extractIp(config, req) @@ -209,7 +209,7 @@ function incomingHttpEndTranslator ({ req, res }) { function onPassportVerify ({ framework, login, user, success, abortController }) { const store = storage('legacy').getStore() - const rootSpan = store?.req && WebPlugin.root(store.req) + const rootSpan = store?.req && web.root(store.req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onPassportVerify') @@ -223,7 +223,7 @@ function onPassportVerify ({ framework, login, user, success, abortController }) function onPassportDeserializeUser ({ user, abortController }) { const store = storage('legacy').getStore() - const rootSpan = store?.req && WebPlugin.root(store.req) + const rootSpan = store?.req && web.root(store.req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onPassportDeserializeUser') @@ -236,7 +236,7 @@ function onPassportDeserializeUser ({ user, abortController }) { } function onExpressSession ({ req, res, sessionId, abortController }) { - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) { log.warn('[ASM] No rootSpan found in onExpressSession') return @@ -262,7 +262,7 @@ function onRequestQueryParsed ({ req, res, query, abortController }) { req = store?.req } - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return const results = waf.run({ @@ -275,7 +275,7 @@ function onRequestQueryParsed ({ req, res, query, abortController }) { } function onRequestProcessParams ({ req, res, abortController, params }) { - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return if (!params || typeof params !== 'object' || !Object.keys(params).length) return @@ -317,7 +317,7 @@ function onResponseWriteHead ({ req, res, abortController, statusCode, responseH return } - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return responseHeaders = { ...responseHeaders } diff --git a/packages/dd-trace/src/appsec/rasp/index.js b/packages/dd-trace/src/appsec/rasp/index.js index 2d5bf08d1c3..09bdba8faef 100644 --- a/packages/dd-trace/src/appsec/rasp/index.js +++ b/packages/dd-trace/src/appsec/rasp/index.js @@ -1,6 +1,6 @@ 'use strict' -const WebPlugin = require('../../../../datadog-plugin-web/src') +const web = require('../../../../datadog-plugin-web/src/utils') const { setUncaughtExceptionCaptureCallbackStart, expressMiddlewareError } = require('../channels') const { block, isBlocked } = require('../blocking') const ssrf = require('./ssrf') @@ -87,7 +87,7 @@ function blockOnDatadogRaspAbortError ({ error }) { const { req, res, blockingAction, raspRule, ruleTriggered } = abortError if (!isBlocked(res)) { - const blocked = block(req, res, WebPlugin.root(req), null, blockingAction) + const blocked = block(req, res, web.root(req), null, blockingAction) if (ruleTriggered) { updateRaspRuleMatchMetricTags(req, raspRule, true, blocked) } diff --git a/packages/dd-trace/src/appsec/rasp/utils.js b/packages/dd-trace/src/appsec/rasp/utils.js index 80861a0c06b..c81a0465d24 100644 --- a/packages/dd-trace/src/appsec/rasp/utils.js +++ b/packages/dd-trace/src/appsec/rasp/utils.js @@ -1,6 +1,6 @@ 'use strict' -const WebPlugin = require('../../../../datadog-plugin-web/src') +const web = require('../../../../datadog-plugin-web/src/utils') const { getCallsiteFrames, reportStackTrace, canReportStackTrace } = require('../stack_trace') const { getBlockingAction } = require('../blocking') const log = require('../../log') @@ -36,7 +36,7 @@ function handleResult (result, req, res, abortController, config, raspRule) { const { enabled, maxDepth, maxStackTraces } = config.appsec.stackTrace - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) const ruleTriggered = !!result?.events?.length diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index 813da668d98..a7a392adb4c 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -4,7 +4,7 @@ const dc = require('dc-polyfill') const zlib = require('zlib') const { storage } = require('../../../datadog-core') -const WebPlugin = require('../../../datadog-plugin-web/src') +const web = require('../../../datadog-plugin-web/src/utils') const { ipHeaderList } = require('../plugins/util/ip_extractor') const { incrementWafInitMetric, @@ -276,7 +276,7 @@ function reportWafConfigUpdate (product, rcConfigId, diagnostics, wafVersion) { function reportMetrics (metrics, raspRule) { const store = storage('legacy').getStore() - const rootSpan = store?.req && WebPlugin.root(store.req) + const rootSpan = store?.req && web.root(store.req) if (!rootSpan) return @@ -310,7 +310,7 @@ function reportTruncationMetrics (rootSpan, metrics) { function reportAttack (attackData) { const store = storage('legacy').getStore() const req = store?.req - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return const currentTags = rootSpan.context()._tags @@ -426,7 +426,7 @@ function reportAttributes (attributes) { if (!attributes) return const req = storage('legacy').getStore()?.req - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return @@ -443,7 +443,7 @@ function reportAttributes (attributes) { } function finishRequest (req, res, storedResponseHeaders) { - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) if (!rootSpan) return if (metricsQueue.size) { diff --git a/packages/dd-trace/src/appsec/waf/index.js b/packages/dd-trace/src/appsec/waf/index.js index a5e6c11ece6..6acd254567c 100644 --- a/packages/dd-trace/src/appsec/waf/index.js +++ b/packages/dd-trace/src/appsec/waf/index.js @@ -6,7 +6,7 @@ const Reporter = require('../reporter') const Limiter = require('../../rate_limiter') const { keepTrace } = require('../../priority_sampler') const { ASM } = require('../../standalone/product') -const WebPlugin = require('../../../../datadog-plugin-web/src') +const web = require('../../../../datadog-plugin-web/src/utils') const { updateRateLimitedMetric } = require('../telemetry') class WafUpdateError extends Error { @@ -112,7 +112,7 @@ function run (data, req, raspRule) { if (result?.keep) { if (limiter.isAllowed()) { - const rootSpan = WebPlugin.root(req) + const rootSpan = web.root(req) keepTrace(rootSpan, ASM) } else { updateRateLimitedMetric(req) diff --git a/packages/dd-trace/test/appsec/api_security_sampler.spec.js b/packages/dd-trace/test/appsec/api_security_sampler.spec.js index 64e242a9811..b4d2f67c219 100644 --- a/packages/dd-trace/test/appsec/api_security_sampler.spec.js +++ b/packages/dd-trace/test/appsec/api_security_sampler.spec.js @@ -16,14 +16,11 @@ describe('API Security Sampler', () => { webStub = { root: sinon.stub(), - getContext: sinon.stub(), - _prioritySampler: { - isSampled: sinon.stub() - } + getContext: sinon.stub() } apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../../../datadog-plugin-web/src/index': webStub + '../../../datadog-plugin-web/src/utils': webStub }) span = { @@ -204,7 +201,7 @@ describe('API Security Sampler', () => { beforeEach(() => { keepTraceStub = sinon.stub() apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../../../datadog-plugin-web/src/index': webStub, + '../../../datadog-plugin-web/src/utils': webStub, '../priority_sampler': { keepTrace: keepTraceStub }, diff --git a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js index 8d20e8d2d00..66083c762db 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -23,7 +23,7 @@ describe('Overhead controller', () => { } overheadController = proxyquire('../../../src/appsec/iast/overhead-controller', { - '../../../../datadog-plugin-web/src/index': web + '../../../../datadog-plugin-web/src/utils': web }) oceContextKey = overheadController.OVERHEAD_CONTROLLER_CONTEXT_KEY diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 138cf0c6dcc..eb31ad8804d 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -91,10 +91,7 @@ describe('AppSec Index', function () { web = { root: sinon.stub(), - getContext: sinon.stub(), - _prioritySampler: { - isSampled: sinon.stub() - } + getContext: sinon.stub() } blocking = { @@ -124,7 +121,7 @@ describe('AppSec Index', function () { } apiSecuritySampler = proxyquire('../../src/appsec/api_security_sampler', { - '../../../datadog-plugin-web/src/index': web + '../../../datadog-plugin-web/src/utils': web }) sinon.spy(apiSecuritySampler, 'sampleRequest') @@ -145,7 +142,7 @@ describe('AppSec Index', function () { AppSec = proxyquire('../../src/appsec', { '../log': log, - '../../../datadog-plugin-web/src/index': web, + '../../../datadog-plugin-web/src/utils': web, './blocking': blocking, './user_tracking': UserTracking, './telemetry': appsecTelemetry, diff --git a/packages/dd-trace/test/appsec/rasp/lfi.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.spec.js index 7447f2acc5f..1622efd38a3 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.spec.js @@ -34,7 +34,7 @@ describe('RASP - lfi.js', () => { lfi = proxyquire('../../../src/appsec/rasp/lfi', { '../../../../datadog-core': { storage: () => legacyStorage }, '../waf': waf, - '../../../../datadog-plugin-web/src/index': web, + '../../../../datadog-plugin-web/src/utils': web, '../blocking': blocking, './fs-plugin': appsecFsPlugin }) diff --git a/packages/dd-trace/test/appsec/rasp/utils.spec.js b/packages/dd-trace/test/appsec/rasp/utils.spec.js index 82f399c8659..3e4f8c2a193 100644 --- a/packages/dd-trace/test/appsec/rasp/utils.spec.js +++ b/packages/dd-trace/test/appsec/rasp/utils.spec.js @@ -24,7 +24,7 @@ describe('RASP - utils.js', () => { } utils = proxyquire('../../../src/appsec/rasp/utils', { - '../../../../datadog-plugin-web/src/index': web, + '../../../../datadog-plugin-web/src/utils': web, '../stack_trace': stackTrace, '../telemetry': telemetry, '../blocking': { diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 09eb8ca0f87..ff55a4db00d 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -59,7 +59,7 @@ describe('reporter', () => { } Reporter = proxyquire('../../src/appsec/reporter', { - '../../../datadog-plugin-web/src/index': web, + '../../../datadog-plugin-web/src/utils': web, './telemetry': telemetry }) }) diff --git a/packages/dd-trace/test/appsec/waf/index.spec.js b/packages/dd-trace/test/appsec/waf/index.spec.js index 40abce20afc..8362cbc5a75 100644 --- a/packages/dd-trace/test/appsec/waf/index.spec.js +++ b/packages/dd-trace/test/appsec/waf/index.spec.js @@ -59,7 +59,7 @@ describe('WAF Manager', () => { '../../rate_limiter': function () { return limiterStub }, '../../priority_sampler': { keepTrace }, '../../standalone/product': { ASM: 'ASM' }, - '../../../../datadog-plugin-web/src/index': webMock, + '../../../../datadog-plugin-web/src/utils': webMock, '../telemetry': { updateRateLimitedMetric } }) waf.destroy() From b396139e53bee87d7e10d34230e5a834843cb13b Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 15:21:38 -0400 Subject: [PATCH 24/28] more fixes --- .../src/index.js | 2 +- packages/datadog-plugin-web/src/index.js | 18 +++++++++++------- packages/datadog-plugin-web/src/utils.js | 6 +++--- packages/dd-trace/test/appsec/index.spec.js | 10 +++++----- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index d85915f8dfd..c9dc911754e 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -50,7 +50,7 @@ class AzureFunctionsPlugin extends TracingPlugin { headers: Object.fromEntries(httpRequest.headers), url: path } - const context = web.patch(req) + const context = web.patch(req, this.config) context.config = this.config context.paths = [path] context.res = { statusCode: result.status } diff --git a/packages/datadog-plugin-web/src/index.js b/packages/datadog-plugin-web/src/index.js index 9e4f871eb9a..0f57481001c 100644 --- a/packages/datadog-plugin-web/src/index.js +++ b/packages/datadog-plugin-web/src/index.js @@ -9,6 +9,9 @@ const { const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const InferredProxyPlugin = require('../../datadog-plugin-inferred-proxy/src') const web = require('./utils') +const tags = require('../../../ext/tags') +const types = require('../../../ext/types') +const kinds = require('../../../ext/kinds') const { contexts, @@ -23,14 +26,15 @@ const { addAllowHeaders, isOriginAllowed, reactivate, - WEB, - SERVER, - RESOURCE_NAME, - MANUAL_DROP, - SERVICE_NAME, - FORMAT_HTTP_HEADERS, } = web +const WEB = types.WEB +const SERVER = kinds.SERVER +const RESOURCE_NAME = tags.RESOURCE_NAME +const MANUAL_DROP = tags.MANUAL_DROP +const SERVICE_NAME = tags.SERVICE_NAME +const FORMAT_HTTP_HEADERS = 'http_headers' + class WebPlugin extends TracingPlugin { static id = WEB static kind = SERVER @@ -231,7 +235,7 @@ class WebPlugin extends TracingPlugin { } patch (req) { - return patch(req) + return patch(req, this.config) } // Return the request root span. diff --git a/packages/datadog-plugin-web/src/utils.js b/packages/datadog-plugin-web/src/utils.js index 1f8ffa6aa3b..b9131aedebe 100644 --- a/packages/datadog-plugin-web/src/utils.js +++ b/packages/datadog-plugin-web/src/utils.js @@ -61,7 +61,7 @@ function setRoute (req, path) { context.paths = [path] } -function patch (req) { +function patch (req, config) { let context = contexts.get(req) if (context) return context @@ -79,7 +79,7 @@ function patch (req) { paths: [], middleware: [], beforeEnd: [], - config: this.config + config } contexts.set(req, context) @@ -373,5 +373,5 @@ module.exports = { isOriginAllowed, reactivate, contexts, - ends + ends, } diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index eb31ad8804d..cb110784fcc 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -413,7 +413,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') @@ -451,7 +451,7 @@ describe('AppSec Index', function () { 'content-length': '15' } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') sinon.stub(waf, 'disposeContext') @@ -496,7 +496,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') @@ -542,7 +542,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') AppSec.incomingHttpEndTranslator({ req, res }) @@ -608,7 +608,7 @@ describe('AppSec Index', function () { statusCode: 201 } - web.patch(req) + web.patch(req, config) sinon.stub(Reporter, 'finishRequest') AppSec.incomingHttpEndTranslator({ req, res }) From ee613d8e5cf2185964c03d57d64da00e43b1188d Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 15:30:25 -0400 Subject: [PATCH 25/28] more fixes --- packages/datadog-plugin-next/src/index.js | 10 +++++----- packages/dd-trace/src/plugins/util/serverless.js | 9 --------- packages/dd-trace/test/appsec/graphql.spec.js | 2 +- 3 files changed, 6 insertions(+), 15 deletions(-) delete mode 100644 packages/dd-trace/src/plugins/util/serverless.js diff --git a/packages/datadog-plugin-next/src/index.js b/packages/datadog-plugin-next/src/index.js index 322f7dbbb47..d0e19b1e106 100644 --- a/packages/datadog-plugin-next/src/index.js +++ b/packages/datadog-plugin-next/src/index.js @@ -1,7 +1,7 @@ 'use strict' const ServerPlugin = require('../../dd-trace/src/plugins/server') -const WebPlugin = require('../../datadog-plugin-web/src') +const web = require('../../datadog-plugin-web/src/utils') const { storage } = require('../../datadog-core') const analyticsSampler = require('../../dd-trace/src/analytics_sampler') const { COMPONENT } = require('../../dd-trace/src/constants') @@ -63,15 +63,15 @@ class NextPlugin extends ServerPlugin { if (requestError) { // prioritize user-set errors from API routes span.setTag('error', requestError) - WebPlugin.addError(req, requestError) + web.addError(req, requestError) } else if (error) { // general error handling span.setTag('error', error) - WebPlugin.addError(req, requestError || error) + web.addError(req, requestError || error) } else if (!this.config.validateStatus(res.statusCode)) { // where there's no error, we still need to validate status span.setTag('error', true) - WebPlugin.addError(req, true) + web.addError(req, true) } span.addTags({ @@ -119,7 +119,7 @@ class NextPlugin extends ServerPlugin { 'resource.name': `${req.method} ${page}`.trim(), 'next.page': page }) - WebPlugin.setRoute(req, page) + web.setRoute(req, page) } configure (config) { diff --git a/packages/dd-trace/src/plugins/util/serverless.js b/packages/dd-trace/src/plugins/util/serverless.js deleted file mode 100644 index 43958314705..00000000000 --- a/packages/dd-trace/src/plugins/util/serverless.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const types = require('../../../../../ext/types') -const web = require('./web') - -const serverless = { ...web } -serverless.TYPE = types.SERVERLESS - -module.exports = serverless diff --git a/packages/dd-trace/test/appsec/graphql.spec.js b/packages/dd-trace/test/appsec/graphql.spec.js index e54d0970482..e067d2848f5 100644 --- a/packages/dd-trace/test/appsec/graphql.spec.js +++ b/packages/dd-trace/test/appsec/graphql.spec.js @@ -2,7 +2,7 @@ const proxyquire = require('proxyquire') const waf = require('../../src/appsec/waf') -const web = require('../../../datadog-plugin-web/src') +const web = require('../../../datadog-plugin-web/src/utils') const { storage } = require('../../../datadog-core') const addresses = require('../../src/appsec/addresses') From 5f99e6861d8addf7818e79a08918565f76497248 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 15:41:47 -0400 Subject: [PATCH 26/28] fix other appsec failure --- packages/dd-trace/test/appsec/index.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index cb110784fcc..8252d8bae12 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -663,7 +663,6 @@ describe('AppSec Index', function () { } web.root.returns(span) - web._prioritySampler.isSampled.returns(true) AppSec.incomingHttpEndTranslator({ req, res }) From 1399cc239fa400670aa3629561bfd411ff4fdf80 Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 16:25:31 -0400 Subject: [PATCH 27/28] use specific system tests branch --- .github/workflows/system-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index 92ba0e32c9a..1fa9de6e339 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -37,10 +37,12 @@ jobs: contents: read packages: write with: + _system_tests_dev_mode: true + _system_tests_library_target_branch: conti/fix-js-web-err library: nodejs binaries_artifact: system_tests_binaries - desired_execution_time: 300 # 5 minutes + desired_execution_time: 300 # 5 minutes scenarios_groups: tracer-release - excluded_scenarios: APM_TRACING_E2E_OTEL,APM_TRACING_E2E_SINGLE_SPAN # require AWS and datadog credentials + excluded_scenarios: APM_TRACING_E2E_OTEL,APM_TRACING_E2E_SINGLE_SPAN # require AWS and datadog credentials parametric_job_count: 8 skip_empty_scenarios: true From 936d94ada53fca669e67be5e9cc3fe9cff6c126b Mon Sep 17 00:00:00 2001 From: William Conti Date: Tue, 12 Aug 2025 16:33:16 -0400 Subject: [PATCH 28/28] use correct branch --- .github/workflows/system-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/system-tests.yml b/.github/workflows/system-tests.yml index 1fa9de6e339..50ed589f684 100644 --- a/.github/workflows/system-tests.yml +++ b/.github/workflows/system-tests.yml @@ -37,8 +37,7 @@ jobs: contents: read packages: write with: - _system_tests_dev_mode: true - _system_tests_library_target_branch: conti/fix-js-web-err + ref: conti/fix-js-web-err library: nodejs binaries_artifact: system_tests_binaries desired_execution_time: 300 # 5 minutes