From b456550ce0a4867433688bb572f6bb940fe4f485 Mon Sep 17 00:00:00 2001 From: Ida Liu <119438987+ida613@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:39:07 -0500 Subject: [PATCH 01/61] fix baggage extraction (#4935) --- .../src/opentracing/propagation/text_map.js | 8 ++++---- .../test/opentracing/propagation/text_map.spec.js | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index afca1081110..b117ae0ae5e 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -339,11 +339,11 @@ class TextMapPropagator { context._links.push(link) } } + } - if (this._config.tracePropagationStyle.extract.includes('baggage') && carrier.baggage) { - context = context || new DatadogSpanContext() - this._extractBaggageItems(carrier, context) - } + if (this._hasPropagationStyle('extract', 'baggage') && carrier.baggage) { + context = context || new DatadogSpanContext() + this._extractBaggageItems(carrier, context) } return context || this._extractSqsdContext(carrier) diff --git a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js index 4598ffeda76..c6247330a69 100644 --- a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js @@ -406,7 +406,6 @@ describe('TextMapPropagator', () => { }) it('should extract otel baggage items with special characters', () => { - process.env.DD_TRACE_BAGGAGE_ENABLED = true config = new Config() propagator = new TextMapPropagator(config) const carrier = { @@ -452,6 +451,20 @@ describe('TextMapPropagator', () => { expect(spanContextD._baggageItems).to.deep.equal({}) }) + it('should extract baggage when it is the only propagation style', () => { + config = new Config({ + tracePropagationStyle: { + extract: ['baggage'] + } + }) + propagator = new TextMapPropagator(config) + const carrier = { + baggage: 'foo=bar' + } + const spanContext = propagator.extract(carrier) + expect(spanContext._baggageItems).to.deep.equal({ foo: 'bar' }) + }) + it('should convert signed IDs to unsigned', () => { textMap['x-datadog-trace-id'] = '-123' textMap['x-datadog-parent-id'] = '-456' From d19f3b03ade62a86232469b7459073f9a7a5a5a2 Mon Sep 17 00:00:00 2001 From: Carles Capell <107924659+CarlesDD@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:36:24 +0100 Subject: [PATCH 02/61] Fix IAST standalone sampling priority propagation (#4927) * WIP * Disable vuln deduplication in OCE test * Test vuln deduplication on the fly * Skip vuln dedup in multiple sends test * Fix lint issues * Remove multiple send test * Move on the fly span creation for vulns out of req to addVulnerability method * Move finish out-of-request span * Update packages/dd-trace/src/appsec/iast/vulnerability-reporter.js Co-authored-by: Igor Unanua --------- Co-authored-by: Igor Unanua --- .../src/appsec/iast/vulnerability-reporter.js | 69 +++++++++---------- .../appsec/iast/overhead-controller.spec.js | 10 ++- .../iast/vulnerability-reporter.spec.js | 49 ++++++------- 3 files changed, 62 insertions(+), 66 deletions(-) diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index e2d1619b118..05aea14cf02 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -17,13 +17,36 @@ let resetVulnerabilityCacheTimer let deduplicationEnabled = true function addVulnerability (iastContext, vulnerability) { - if (vulnerability && vulnerability.evidence && vulnerability.type && - vulnerability.location) { - if (iastContext && iastContext.rootSpan) { + if (vulnerability?.evidence && vulnerability?.type && vulnerability?.location) { + if (deduplicationEnabled && isDuplicatedVulnerability(vulnerability)) return + + VULNERABILITY_HASHES.set(`${vulnerability.type}${vulnerability.hash}`, true) + + let span = iastContext?.rootSpan + + if (!span && tracer) { + span = tracer.startSpan('vulnerability', { + type: 'vulnerability' + }) + + vulnerability.location.spanId = span.context().toSpanId() + + span.addTags({ + [IAST_ENABLED_TAG_KEY]: 1 + }) + } + + if (!span) return + + keepTrace(span, SAMPLING_MECHANISM_APPSEC) + standalone.sample(span) + + if (iastContext?.rootSpan) { iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || [] iastContext[VULNERABILITIES_KEY].push(vulnerability) } else { - sendVulnerabilities([vulnerability]) + sendVulnerabilities([vulnerability], span) + span.finish() } } } @@ -34,36 +57,17 @@ function isValidVulnerability (vulnerability) { vulnerability.location && vulnerability.location.spanId } -function sendVulnerabilities (vulnerabilities, rootSpan) { +function sendVulnerabilities (vulnerabilities, span) { if (vulnerabilities && vulnerabilities.length) { - let span = rootSpan - if (!span && tracer) { - span = tracer.startSpan('vulnerability', { - type: 'vulnerability' - }) - vulnerabilities.forEach((vulnerability) => { - vulnerability.location.spanId = span.context().toSpanId() - }) - span.addTags({ - [IAST_ENABLED_TAG_KEY]: 1 - }) - } - if (span && span.addTags) { - const validAndDedupVulnerabilities = deduplicateVulnerabilities(vulnerabilities).filter(isValidVulnerability) - const jsonToSend = vulnerabilitiesFormatter.toJson(validAndDedupVulnerabilities) + const validatedVulnerabilities = vulnerabilities.filter(isValidVulnerability) + const jsonToSend = vulnerabilitiesFormatter.toJson(validatedVulnerabilities) if (jsonToSend.vulnerabilities.length > 0) { const tags = {} // TODO: Store this outside of the span and set the tag in the exporter. tags[IAST_JSON_TAG_KEY] = JSON.stringify(jsonToSend) span.addTags(tags) - - keepTrace(span, SAMPLING_MECHANISM_APPSEC) - - standalone.sample(span) - - if (!rootSpan) span.finish() } } } @@ -86,17 +90,8 @@ function stopClearCacheTimer () { } } -function deduplicateVulnerabilities (vulnerabilities) { - if (!deduplicationEnabled) return vulnerabilities - const deduplicated = vulnerabilities.filter((vulnerability) => { - const key = `${vulnerability.type}${vulnerability.hash}` - if (!VULNERABILITY_HASHES.get(key)) { - VULNERABILITY_HASHES.set(key, true) - return true - } - return false - }) - return deduplicated +function isDuplicatedVulnerability (vulnerability) { + return VULNERABILITY_HASHES.get(`${vulnerability.type}${vulnerability.hash}`) } function start (config, _tracer) { diff --git a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js index 7bde02537d9..c5003be25ad 100644 --- a/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js +++ b/packages/dd-trace/test/appsec/iast/overhead-controller.spec.js @@ -331,7 +331,8 @@ describe('Overhead controller', () => { iast: { enabled: true, requestSampling: 100, - maxConcurrentRequests: 2 + maxConcurrentRequests: 2, + deduplicationEnabled: false } } }) @@ -365,7 +366,6 @@ describe('Overhead controller', () => { } else if (url === SECOND_REQUEST) { setImmediate(() => { requestResolvers[FIRST_REQUEST]() - vulnerabilityReporter.clearCache() }) } }) @@ -373,7 +373,6 @@ describe('Overhead controller', () => { if (url === FIRST_REQUEST) { setImmediate(() => { requestResolvers[SECOND_REQUEST]() - vulnerabilityReporter.clearCache() }) } }) @@ -388,7 +387,8 @@ describe('Overhead controller', () => { iast: { enabled: true, requestSampling: 100, - maxConcurrentRequests: 2 + maxConcurrentRequests: 2, + deduplicationEnabled: false } } }) @@ -435,7 +435,6 @@ describe('Overhead controller', () => { requestResolvers[FIRST_REQUEST]() } else if (url === FIFTH_REQUEST) { requestResolvers[SECOND_REQUEST]() - vulnerabilityReporter.clearCache() } }) testRequestEventEmitter.on(TEST_REQUEST_FINISHED, (url) => { @@ -444,7 +443,6 @@ describe('Overhead controller', () => { axios.get(`http://localhost:${serverConfig.port}${FIFTH_REQUEST}`).then().catch(done) } else if (url === SECOND_REQUEST) { setImmediate(() => { - vulnerabilityReporter.clearCache() requestResolvers[THIRD_REQUEST]() requestResolvers[FOURTH_REQUEST]() requestResolvers[FIFTH_REQUEST]() diff --git a/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js b/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js index 1f4516218af..2ebe646a2d8 100644 --- a/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js +++ b/packages/dd-trace/test/appsec/iast/vulnerability-reporter.spec.js @@ -52,14 +52,14 @@ describe('vulnerability-reporter', () => { expect(iastContext.vulnerabilities).to.be.an('array') }) - it('should add multiple vulnerabilities', () => { + it('should deduplicate same vulnerabilities', () => { addVulnerability(iastContext, vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, -555)) addVulnerability(iastContext, vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 888)) addVulnerability(iastContext, vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 123)) - expect(iastContext.vulnerabilities).to.have.length(3) + expect(iastContext.vulnerabilities).to.have.length(1) }) it('should add in the context evidence properties', () => { @@ -260,7 +260,12 @@ describe('vulnerability-reporter', () => { '[{"value":"SELECT id FROM u WHERE email = \'"},{"value":"joe@mail.com","source":1},{"value":"\';"}]},' + '"location":{"spanId":888,"path":"filename.js","line":99}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + + expect(prioritySampler.setPriority).to.have.been.calledTwice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should send multiple vulnerabilities with same tainted source', () => { @@ -313,7 +318,12 @@ describe('vulnerability-reporter', () => { '[{"value":"UPDATE u SET name=\'"},{"value":"joe","source":0},{"value":"\' WHERE id=1;"}]},' + '"location":{"spanId":888,"path":"filename.js","line":99}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + + expect(prioritySampler.setPriority).to.have.been.calledTwice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should send once with multiple vulnerabilities', () => { @@ -334,7 +344,13 @@ describe('vulnerability-reporter', () => { '{"type":"INSECURE_HASHING","hash":1755238473,"evidence":{"value":"md5"},' + '"location":{"spanId":-5,"path":"/path/to/file3.js","line":3}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority).to.have.been.calledThrice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.thirdCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should send once vulnerability with one vulnerability', () => { @@ -366,23 +382,6 @@ describe('vulnerability-reporter', () => { expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) - it('should not send duplicated vulnerabilities in multiple sends', () => { - const iastContext = { rootSpan: span } - addVulnerability(iastContext, - vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 888, - { path: 'filename.js', line: 88 })) - addVulnerability(iastContext, - vulnerabilityAnalyzer._createVulnerability('INSECURE_HASHING', { value: 'sha1' }, 888, - { path: 'filename.js', line: 88 })) - sendVulnerabilities(iastContext.vulnerabilities, span) - sendVulnerabilities(iastContext.vulnerabilities, span) - expect(span.addTags).to.have.been.calledOnceWithExactly({ - '_dd.iast.json': '{"sources":[],"vulnerabilities":[{"type":"INSECURE_HASHING","hash":3410512691,' + - '"evidence":{"value":"sha1"},"location":{"spanId":888,"path":"filename.js","line":88}}]}' - }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - it('should not deduplicate vulnerabilities if not enabled', () => { start({ iast: { @@ -401,7 +400,11 @@ describe('vulnerability-reporter', () => { '{"type":"INSECURE_HASHING","hash":3410512691,"evidence":{"value":"sha1"},"location":' + '{"spanId":888,"path":"filename.js","line":88}}]}' }) - expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority).to.have.been.calledTwice + expect(prioritySampler.setPriority.firstCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(prioritySampler.setPriority.secondCall) + .to.have.been.calledWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) }) it('should add _dd.p.appsec trace tag with standalone enabled', () => { From 5c6d12624b3cfca29a49c9ef57d6890a71ea5555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 27 Nov 2024 11:40:46 +0100 Subject: [PATCH 03/61] =?UTF-8?q?[test=20optimization]=C2=A0Add=20Dynamic?= =?UTF-8?q?=20Instrumentation=20to=20jest=20retries=20=20(#4876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE-3rdparty.csv | 1 + .../dynamic-instrumentation/dependency.js | 7 + .../test-hit-breakpoint.js | 15 ++ .../test-not-hit-breakpoint.js | 17 ++ integration-tests/jest/jest.spec.js | 207 +++++++++++++++++- package.json | 1 + packages/datadog-instrumentations/src/jest.js | 14 +- packages/datadog-plugin-jest/src/index.js | 77 ++++++- .../dynamic-instrumentation/index.js | 9 +- .../dynamic-instrumentation/worker/index.js | 38 +++- .../exporters/ci-visibility-exporter.js | 28 ++- .../src/debugger/devtools_client/state.js | 2 +- packages/dd-trace/src/plugin_manager.js | 6 +- packages/dd-trace/src/plugins/ci_plugin.js | 6 + packages/dd-trace/src/plugins/util/test.js | 35 ++- packages/dd-trace/src/proxy.js | 5 + yarn.lock | 2 +- 17 files changed, 444 insertions(+), 26 deletions(-) create mode 100644 integration-tests/ci-visibility/dynamic-instrumentation/dependency.js create mode 100644 integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js create mode 100644 integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index f078d0aa4ae..f8147f23e35 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -31,6 +31,7 @@ require,retry,MIT,Copyright 2011 Tim Koschützki Felix Geisendörfer require,rfdc,MIT,Copyright 2019 David Mark Clements require,semver,ISC,Copyright Isaac Z. Schlueter and Contributors require,shell-quote,mit,Copyright (c) 2013 James Halliday +require,source-map,BSD-3-Clause,Copyright (c) 2009-2011, Mozilla Foundation and contributors dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) dev,@types/node,MIT,Copyright Authors dev,autocannon,MIT,Copyright 2016 Matteo Collina diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/dependency.js b/integration-tests/ci-visibility/dynamic-instrumentation/dependency.js new file mode 100644 index 00000000000..b53ebf22f97 --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/dependency.js @@ -0,0 +1,7 @@ +module.exports = function (a, b) { + const localVariable = 2 + if (a > 10) { + throw new Error('a is too big') + } + return a + b + localVariable - localVariable +} diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js b/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js new file mode 100644 index 00000000000..fdecdb06edb --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js @@ -0,0 +1,15 @@ +/* eslint-disable */ +const sum = require('./dependency') + +// TODO: instead of retrying through jest, this should be retried with auto test retries +jest.retryTimes(1) + +describe('dynamic-instrumentation', () => { + it('retries with DI', () => { + expect(sum(11, 3)).toEqual(14) + }) + + it('is not retried', () => { + expect(sum(1, 2)).toEqual(3) + }) +}) diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js b/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js new file mode 100644 index 00000000000..a4a75aab832 --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js @@ -0,0 +1,17 @@ +/* eslint-disable */ +const sum = require('./dependency') + +// TODO: instead of retrying through jest, this should be retried with auto test retries +jest.retryTimes(1) + +let count = 0 +describe('dynamic-instrumentation', () => { + it('retries with DI', () => { + const willFail = count++ === 0 + if (willFail) { + expect(sum(11, 3)).toEqual(14) // only throws the first time + } else { + expect(sum(1, 2)).toEqual(3) + } + }) +}) diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index 27b70329533..c1f13db9c4d 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -33,7 +33,11 @@ const { TEST_SOURCE_START, TEST_CODE_OWNERS, TEST_SESSION_NAME, - TEST_LEVEL_EVENT_TYPES + TEST_LEVEL_EVENT_TYPES, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_LINE } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -2399,4 +2403,205 @@ describe('jest CommonJS', () => { }) }) }) + + context('dynamic instrumentation', () => { + it('does not activate dynamic instrumentation if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: true, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: true, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + + const notRetriedTest = tests.find(test => test.meta[TEST_NAME].includes('is not retried')) + + assert.notProperty(notRetriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '3' + }, + localVariable: { + type: 'number', + value: '2' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-hit-breakpoint', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: true, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'dynamic-instrumentation/test-not-hit-breakpoint', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + }) }) diff --git a/package.json b/package.json index 26fe1a5fabe..f39bcd5a68a 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "rfdc": "^1.3.1", "semver": "^7.5.4", "shell-quote": "^1.8.1", + "source-map": "^0.7.4", "tlhunter-sorted-set": "^0.1.0" }, "devDependencies": { diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index b17a4137c96..440021f03de 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -237,7 +237,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { name: removeEfdStringFromTestName(testName), suite: this.testSuite, testSourceFile: this.testSourceFile, - runner: 'jest-circus', displayName: this.displayName, testParameters, frameworkVersion: jestVersion, @@ -274,13 +273,18 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } if (event.name === 'test_done') { + const probe = {} const asyncResource = asyncResources.get(event.test) asyncResource.runInAsyncScope(() => { let status = 'pass' if (event.test.errors && event.test.errors.length) { status = 'fail' - const formattedError = formatJestError(event.test.errors[0]) - testErrCh.publish(formattedError) + const numRetries = this.global[RETRY_TIMES] + const numTestExecutions = event.test?.invocations + const willBeRetried = numRetries > 0 && numTestExecutions - 1 < numRetries + + const error = formatJestError(event.test.errors[0]) + testErrCh.publish({ error, willBeRetried, probe, numTestExecutions }) } testRunFinishCh.publish({ status, @@ -302,6 +306,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } }) + if (probe.setProbePromise) { + await probe.setProbePromise + } } if (event.name === 'test_skip' || event.name === 'test_todo') { const asyncResource = new AsyncResource('bound-anonymous-fn') @@ -310,7 +317,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { name: getJestTestName(event.test), suite: this.testSuite, testSourceFile: this.testSourceFile, - runner: 'jest-circus', displayName: this.displayName, frameworkVersion: jestVersion, testStartLine: getTestLineStart(event.test.asyncError, this.testSuite) diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index 4362094b0be..0b3f87f0e6e 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -22,7 +22,14 @@ const { TEST_EARLY_FLAKE_ABORT_REASON, JEST_DISPLAY_NAME, TEST_IS_RUM_ACTIVE, - TEST_BROWSER_DRIVER + TEST_BROWSER_DRIVER, + getFileAndLineNumberFromError, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE, + getTestSuitePath, + TEST_NAME } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const id = require('../../dd-trace/src/id') @@ -39,6 +46,7 @@ const { } = require('../../dd-trace/src/ci-visibility/telemetry') const isJestWorker = !!process.env.JEST_WORKER_ID +const debuggerParameterPerTest = new Map() // https://github.com/facebook/jest/blob/d6ad15b0f88a05816c2fe034dd6900d28315d570/packages/jest-worker/src/types.ts#L38 const CHILD_MESSAGE_END = 2 @@ -301,6 +309,29 @@ class JestPlugin extends CiPlugin { const span = this.startTestSpan(test) this.enter(span, store) + + const { name: testName } = test + + const debuggerParameters = debuggerParameterPerTest.get(testName) + + // If we have a debugger probe, we need to add the snapshot id to the span + if (debuggerParameters) { + const spanContext = span.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + span.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + span.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + span.setTag(DI_DEBUG_ERROR_FILE, file) + span.setTag(DI_DEBUG_ERROR_LINE, line) + } }) this.addSub('ci:jest:test:finish', ({ status, testStartLine }) => { @@ -326,13 +357,19 @@ class JestPlugin extends CiPlugin { finishAllTraceSpans(span) }) - this.addSub('ci:jest:test:err', (error) => { + this.addSub('ci:jest:test:err', ({ error, willBeRetried, probe }) => { if (error) { const store = storage.getStore() if (store && store.span) { const span = store.span span.setTag(TEST_STATUS, 'fail') span.setTag('error', error) + if (willBeRetried && this.di) { + // if we use numTestExecutions, we have to remove the breakpoint after each execution + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(error, probe) + debuggerParameterPerTest.set(testName, debuggerParameters) + } } } }) @@ -344,11 +381,43 @@ class JestPlugin extends CiPlugin { }) } + // TODO: If the test finishes and the probe is not hit, we should remove the breakpoint + addDiProbe (err, probe) { + const [file, line] = getFileAndLineNumberFromError(err) + + const relativePath = getTestSuitePath(file, this.repositoryRoot) + + const [ + snapshotId, + setProbePromise, + hitProbePromise + ] = this.di.addLineProbe({ file: relativePath, line }) + + probe.setProbePromise = setProbePromise + + hitProbePromise.then(({ snapshot }) => { + // TODO: handle race conditions for this.retriedTestIds + const { traceId, spanId } = this.retriedTestIds + this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, { + debugger: { snapshot }, + dd: { + trace_id: traceId, + span_id: spanId + } + }) + }) + + return { + snapshotId, + file: relativePath, + line + } + } + startTestSpan (test) { const { suite, name, - runner, displayName, testParameters, frameworkVersion, @@ -360,7 +429,7 @@ class JestPlugin extends CiPlugin { } = test const extraTags = { - [JEST_TEST_RUNNER]: runner, + [JEST_TEST_RUNNER]: 'jest-circus', [TEST_PARAMETERS]: testParameters, [TEST_FRAMEWORK_VERSION]: frameworkVersion } diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index 97323d02407..ef65489e60d 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -73,8 +73,7 @@ class TestVisDynamicInstrumentation { // Allow the parent to exit even if the worker is still running this.worker.unref() - this.breakpointSetChannel.port2.on('message', (message) => { - const { probeId } = message + this.breakpointSetChannel.port2.on('message', ({ probeId }) => { const resolve = probeIdToResolveBreakpointSet.get(probeId) if (resolve) { resolve() @@ -82,8 +81,7 @@ class TestVisDynamicInstrumentation { } }).unref() - this.breakpointHitChannel.port2.on('message', (message) => { - const { snapshot } = message + this.breakpointHitChannel.port2.on('message', ({ snapshot }) => { const { probe: { id: probeId } } = snapshot const resolve = probeIdToResolveBreakpointHit.get(probeId) if (resolve) { @@ -91,6 +89,9 @@ class TestVisDynamicInstrumentation { probeIdToResolveBreakpointHit.delete(probeId) } }).unref() + + this.worker.on('error', (err) => log.error(err)) + this.worker.on('messageerror', (err) => log.error(err)) } } diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js index 4bef76e6343..fbcb52da239 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js @@ -1,6 +1,8 @@ 'use strict' - +const sourceMap = require('source-map') +const path = require('path') const { workerData: { breakpointSetChannel, breakpointHitChannel } } = require('worker_threads') + // TODO: move debugger/devtools_client/session to common place const session = require('../../../debugger/devtools_client/session') // TODO: move debugger/devtools_client/snapshot to common place @@ -69,14 +71,20 @@ async function addBreakpoint (snapshotId, probe) { const script = findScriptFromPartialPath(file) if (!script) throw new Error(`No loaded script found for ${file}`) - const [path, scriptId] = script + const [path, scriptId, sourceMapURL] = script log.debug(`Adding breakpoint at ${path}:${line}`) + let generatedPosition = { line } + + if (sourceMapURL && sourceMapURL.startsWith('data:')) { + generatedPosition = await processScriptWithInlineSourceMap({ file, line, sourceMapURL }) + } + const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { scriptId, - lineNumber: line - 1 + lineNumber: generatedPosition.line } }) @@ -88,3 +96,27 @@ function start () { sessionStarted = true return session.post('Debugger.enable') // return instead of await to reduce number of promises created } + +async function processScriptWithInlineSourceMap (params) { + const { file, line, sourceMapURL } = params + + // Extract the base64-encoded source map + const base64SourceMap = sourceMapURL.split('base64,')[1] + + // Decode the base64 source map + const decodedSourceMap = Buffer.from(base64SourceMap, 'base64').toString('utf8') + + // Parse the source map + const consumer = await new sourceMap.SourceMapConsumer(decodedSourceMap) + + // Map to the generated position + const generatedPosition = consumer.generatedPositionFor({ + source: path.basename(file), // this needs to be the file, not the filepath + line, + column: 0 + }) + + consumer.destroy() + + return generatedPosition +} diff --git a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js index f555603e0cb..0a12d5f8c5a 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +++ b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js @@ -73,6 +73,9 @@ class CiVisibilityExporter extends AgentInfoExporter { if (this._coverageWriter) { this._coverageWriter.flush() } + if (this._logsWriter) { + this._logsWriter.flush() + } }) } @@ -302,13 +305,28 @@ class CiVisibilityExporter extends AgentInfoExporter { if (!this._isInitialized) { return done() } - this._writer.flush(() => { - if (this._coverageWriter) { - this._coverageWriter.flush(done) - } else { + + // TODO: safe to do them at once? Or do we want to do them one by one? + const writers = [ + this._writer, + this._coverageWriter, + this._logsWriter + ].filter(writer => writer) + + let remaining = writers.length + + if (remaining === 0) { + return done() + } + + const onFlushComplete = () => { + remaining -= 1 + if (remaining === 0) { done() } - }) + } + + writers.forEach(writer => writer.flush(onFlushComplete)) } exportUncodedCoverages () { diff --git a/packages/dd-trace/src/debugger/devtools_client/state.js b/packages/dd-trace/src/debugger/devtools_client/state.js index c409a69f6b7..a69a37067f4 100644 --- a/packages/dd-trace/src/debugger/devtools_client/state.js +++ b/packages/dd-trace/src/debugger/devtools_client/state.js @@ -57,6 +57,6 @@ module.exports = { session.on('Debugger.scriptParsed', ({ params }) => { scriptUrls.set(params.scriptId, params.url) if (params.url.startsWith('file:')) { - scriptIds.push([params.url, params.scriptId]) + scriptIds.push([params.url, params.scriptId, params.sourceMapURL]) } }) diff --git a/packages/dd-trace/src/plugin_manager.js b/packages/dd-trace/src/plugin_manager.js index e9daea9b60b..74cc656048b 100644 --- a/packages/dd-trace/src/plugin_manager.js +++ b/packages/dd-trace/src/plugin_manager.js @@ -138,7 +138,8 @@ module.exports = class PluginManager { clientIpEnabled, memcachedCommandEnabled, ciVisibilityTestSessionName, - ciVisAgentlessLogSubmissionEnabled + ciVisAgentlessLogSubmissionEnabled, + isTestDynamicInstrumentationEnabled } = this._tracerConfig const sharedConfig = { @@ -149,7 +150,8 @@ module.exports = class PluginManager { url, headers: headerTags || [], ciVisibilityTestSessionName, - ciVisAgentlessLogSubmissionEnabled + ciVisAgentlessLogSubmissionEnabled, + isTestDynamicInstrumentationEnabled } if (logInjection !== undefined) { diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index d4c9f32bc68..f6692fa4b23 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -180,6 +180,12 @@ module.exports = class CiPlugin extends Plugin { configure (config) { super.configure(config) + + if (config.isTestDynamicInstrumentationEnabled) { + const testVisibilityDynamicInstrumentation = require('../ci-visibility/dynamic-instrumentation') + this.di = testVisibilityDynamicInstrumentation + } + this.testEnvironmentMetadata = getTestEnvironmentMetadata(this.constructor.id, this.config) const { diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 6c0dde70cfb..8719c916915 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -106,6 +106,13 @@ const TEST_LEVEL_EVENT_TYPES = [ 'test_session_end' ] +// Dynamic instrumentation - Test optimization integration tags +const DI_ERROR_DEBUG_INFO_CAPTURED = 'error.debug_info_captured' +// TODO: for the moment we'll only use a single snapshot id, so `0` is hardcoded +const DI_DEBUG_ERROR_SNAPSHOT_ID = '_dd.debug.error.0.snapshot_id' +const DI_DEBUG_ERROR_FILE = '_dd.debug.error.0.file' +const DI_DEBUG_ERROR_LINE = '_dd.debug.error.0.line' + module.exports = { TEST_CODE_OWNERS, TEST_SESSION_NAME, @@ -181,7 +188,12 @@ module.exports = { TEST_BROWSER_VERSION, getTestSessionName, TEST_LEVEL_EVENT_TYPES, - getNumFromKnownTests + getNumFromKnownTests, + getFileAndLineNumberFromError, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19 @@ -637,3 +649,24 @@ function getNumFromKnownTests (knownTests) { return totalNumTests } + +function getFileAndLineNumberFromError (error) { + // Split the stack trace into individual lines + const stackLines = error.stack.split('\n') + + // The top frame is usually the second line + const topFrame = stackLines[1] + + // Regular expression to match the file path, line number, and column number + const regex = /\s*at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/ + const match = topFrame.match(regex) + + if (match) { + const filePath = match[1] + const lineNumber = Number(match[2]) + const columnNumber = Number(match[3]) + + return [filePath, lineNumber, columnNumber] + } + return [] +} diff --git a/packages/dd-trace/src/proxy.js b/packages/dd-trace/src/proxy.js index 5c113399601..81d003eebb7 100644 --- a/packages/dd-trace/src/proxy.js +++ b/packages/dd-trace/src/proxy.js @@ -181,6 +181,11 @@ class Tracer extends NoopProxy { ) } } + + if (config.isTestDynamicInstrumentationEnabled) { + const testVisibilityDynamicInstrumentation = require('./ci-visibility/dynamic-instrumentation') + testVisibilityDynamicInstrumentation.start() + } } catch (e) { log.error(e) } diff --git a/yarn.lock b/yarn.lock index 2e4b4c17ce5..0efe56a17c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4530,7 +4530,7 @@ source-map@^0.6.0, source-map@^0.6.1: source-map@^0.7.4: version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== spawn-wrap@^2.0.0: From 82c489b5480101c17cc41d20fde4cec4f976500f Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Wed, 27 Nov 2024 16:01:28 -0500 Subject: [PATCH 04/61] add runtime version to crash report metadata (#4948) --- packages/dd-trace/src/crashtracking/crashtracker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dd-trace/src/crashtracking/crashtracker.js b/packages/dd-trace/src/crashtracking/crashtracker.js index 72759001b1d..fc42195c953 100644 --- a/packages/dd-trace/src/crashtracking/crashtracker.js +++ b/packages/dd-trace/src/crashtracking/crashtracker.js @@ -79,6 +79,7 @@ class Crashtracker { 'language:javascript', `library_version:${pkg.version}`, 'runtime:nodejs', + `runtime_version:${process.versions.node}`, 'severity:crash' ] } From ac1920755519f880867cd1799535f3fd540a6a0d Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Wed, 27 Nov 2024 16:01:55 -0500 Subject: [PATCH 05/61] update guardrails to report telemetry in old node versions (#4949) --- .github/workflows/project.yml | 4 +- init.js | 72 +---------------- integration-tests/init.spec.js | 26 +------ .../src/helpers/register.js | 2 +- packages/dd-trace/src/guardrails/index.js | 67 ++++++++++++++++ packages/dd-trace/src/guardrails/log.js | 32 ++++++++ packages/dd-trace/src/guardrails/telemetry.js | 78 +++++++++++++++++++ packages/dd-trace/src/guardrails/util.js | 10 +++ .../dd-trace/src/telemetry/init-telemetry.js | 75 ------------------ 9 files changed, 195 insertions(+), 171 deletions(-) create mode 100644 packages/dd-trace/src/guardrails/index.js create mode 100644 packages/dd-trace/src/guardrails/log.js create mode 100644 packages/dd-trace/src/guardrails/telemetry.js create mode 100644 packages/dd-trace/src/guardrails/util.js delete mode 100644 packages/dd-trace/src/telemetry/init-telemetry.js diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 92a97c56457..c58392833d2 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -34,7 +34,7 @@ jobs: integration-guardrails: strategy: matrix: - version: [12.0.0, 12, 14.0.0, 14, 16.0.0, 16, 18.0.0, 18.1.0, 20.0.0, 22.0.0] + version: [12, 14.0.0, 14, 16.0.0, 16, 18.0.0, 18.1.0, 20.0.0, 22.0.0] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -47,7 +47,7 @@ jobs: integration-guardrails-unsupported: strategy: matrix: - version: ['0.8', '0.10', '0.12', '4', '6', '8', '10'] + version: ['0.8', '0.10', '0.12', '4', '6', '8', '10', '12.0.0'] runs-on: ubuntu-latest env: DD_INJECTION_ENABLED: 'true' diff --git a/init.js b/init.js index d9286b0307f..625d493b3b1 100644 --- a/init.js +++ b/init.js @@ -2,72 +2,8 @@ /* eslint-disable no-var */ -var nodeVersion = require('./version') -var NODE_MAJOR = nodeVersion.NODE_MAJOR -var NODE_MINOR = nodeVersion.NODE_MINOR +var guard = require('./packages/dd-trace/src/guardrails') -// We use several things that are not supported by older versions of Node: -// - AsyncLocalStorage -// - The `semver` module -// - dc-polyfill -// - Mocha (for testing) -// and probably others. -// TODO: Remove all these dependencies so that we can report telemetry. -if ((NODE_MAJOR === 12 && NODE_MINOR >= 17) || NODE_MAJOR > 12) { - var path = require('path') - var Module = require('module') - var semver = require('semver') - var log = require('./packages/dd-trace/src/log') - var isTrue = require('./packages/dd-trace/src/util').isTrue - var telemetry = require('./packages/dd-trace/src/telemetry/init-telemetry') - - var initBailout = false - var clobberBailout = false - var forced = isTrue(process.env.DD_INJECT_FORCE) - - if (process.env.DD_INJECTION_ENABLED) { - // If we're running via single-step install, and we're not in the app's - // node_modules, then we should not initialize the tracer. This prevents - // single-step-installed tracer from clobbering the manually-installed tracer. - var resolvedInApp - var entrypoint = process.argv[1] - try { - resolvedInApp = Module.createRequire(entrypoint).resolve('dd-trace') - } catch (e) { - // Ignore. If we can't resolve the module, we assume it's not in the app. - } - if (resolvedInApp) { - var ourselves = path.join(__dirname, 'index.js') - if (ourselves !== resolvedInApp) { - clobberBailout = true - } - } - - // If we're running via single-step install, and the runtime doesn't match - // the engines field in package.json, then we should not initialize the tracer. - if (!clobberBailout) { - var engines = require('./package.json').engines - var version = process.versions.node - if (!semver.satisfies(version, engines.node)) { - initBailout = true - telemetry([ - { name: 'abort', tags: ['reason:incompatible_runtime'] }, - { name: 'abort.runtime', tags: [] } - ]) - log.info('Aborting application instrumentation due to incompatible_runtime.') - log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') - if (forced) { - log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') - } - } - } - } - - if (!clobberBailout && (!initBailout || forced)) { - var tracer = require('.') - tracer.init() - module.exports = tracer - telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) - log.info('Application instrumentation bootstrapping complete') - } -} +module.exports = guard(function () { + return require('.').init() +}) diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index 3c37004f607..03a17d5f4c7 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -20,7 +20,6 @@ const telemetryGood = ['complete', 'injection_forced:false'] const { engines } = require('../package.json') const supportedRange = engines.node const currentVersionIsSupported = semver.satisfies(process.versions.node, supportedRange) -const currentVersionCanLog = semver.satisfies(process.versions.node, '>=12.17.0') // These are on by default in release tests, so we'll turn them off for // more fine-grained control of these variables in these tests. @@ -84,30 +83,7 @@ function testRuntimeVersionChecks (arg, filename) { } } - if (!currentVersionCanLog) { - context('when node version is too low for AsyncLocalStorage', () => { - useEnv({ NODE_OPTIONS }) - - it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => - doTest('false\n')) - context('with DD_INJECTION_ENABLED', () => { - useEnv({ DD_INJECTION_ENABLED }) - - context('without debug', () => { - it('should not initialize the tracer', () => doTest('false\n')) - it('should not, if DD_INJECT_FORCE', () => doTestForced('false\n')) - }) - context('with debug', () => { - useEnv({ DD_TRACE_DEBUG }) - - it('should not initialize the tracer', () => - doTest('false\n')) - it('should initialize the tracer, if DD_INJECT_FORCE', () => - doTestForced('false\n')) - }) - }) - }) - } else if (!currentVersionIsSupported) { + if (!currentVersionIsSupported) { context('when node version is less than engines field', () => { useEnv({ NODE_OPTIONS }) diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 4b4185423c0..171db91e224 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -7,7 +7,7 @@ const Hook = require('./hook') const requirePackageJson = require('../../../dd-trace/src/require-package-json') const log = require('../../../dd-trace/src/log') const checkRequireCache = require('../check_require_cache') -const telemetry = require('../../../dd-trace/src/telemetry/init-telemetry') +const telemetry = require('../../../dd-trace/src/guardrails/telemetry') const { DD_TRACE_DISABLED_INSTRUMENTATIONS = '', diff --git a/packages/dd-trace/src/guardrails/index.js b/packages/dd-trace/src/guardrails/index.js new file mode 100644 index 00000000000..249b9343a39 --- /dev/null +++ b/packages/dd-trace/src/guardrails/index.js @@ -0,0 +1,67 @@ +'use strict' + +/* eslint-disable no-var */ + +var path = require('path') +var Module = require('module') +var isTrue = require('./util').isTrue +var log = require('./log') +var telemetry = require('./telemetry') +var nodeVersion = require('../../../../version') + +var NODE_MAJOR = nodeVersion.NODE_MAJOR + +// TODO: Test telemetry for Node <12. For now only bailout is tested for those. +function guard (fn) { + var initBailout = false + var clobberBailout = false + var forced = isTrue(process.env.DD_INJECT_FORCE) + + if (process.env.DD_INJECTION_ENABLED) { + // If we're running via single-step install, and we're not in the app's + // node_modules, then we should not initialize the tracer. This prevents + // single-step-installed tracer from clobbering the manually-installed tracer. + var resolvedInApp + var entrypoint = process.argv[1] + try { + resolvedInApp = Module.createRequire(entrypoint).resolve('dd-trace') + } catch (e) { + // Ignore. If we can't resolve the module, we assume it's not in the app. + } + if (resolvedInApp) { + var ourselves = path.normalize(path.join(__dirname, '..', '..', '..', '..', 'index.js')) + if (ourselves !== resolvedInApp) { + clobberBailout = true + } + } + + // If we're running via single-step install, and the runtime doesn't match + // the engines field in package.json, then we should not initialize the tracer. + if (!clobberBailout) { + var engines = require('../../../../package.json').engines + var minMajor = parseInt(engines.node.replace(/[^0-9]/g, '')) + var version = process.versions.node + if (NODE_MAJOR < minMajor) { + initBailout = true + telemetry([ + { name: 'abort', tags: ['reason:incompatible_runtime'] }, + { name: 'abort.runtime', tags: [] } + ]) + log.info('Aborting application instrumentation due to incompatible_runtime.') + log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') + if (forced) { + log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') + } + } + } + } + + if (!clobberBailout && (!initBailout || forced)) { + var result = fn() + telemetry('complete', ['injection_forced:' + (forced && initBailout ? 'true' : 'false')]) + log.info('Application instrumentation bootstrapping complete') + return result + } +} + +module.exports = guard diff --git a/packages/dd-trace/src/guardrails/log.js b/packages/dd-trace/src/guardrails/log.js new file mode 100644 index 00000000000..dd74e5bdbf0 --- /dev/null +++ b/packages/dd-trace/src/guardrails/log.js @@ -0,0 +1,32 @@ +'use strict' + +/* eslint-disable no-var */ +/* eslint-disable no-console */ + +var isTrue = require('./util').isTrue + +var DD_TRACE_DEBUG = process.env.DD_TRACE_DEBUG +var DD_TRACE_LOG_LEVEL = process.env.DD_TRACE_LOG_LEVEL + +var logLevels = { + trace: 20, + debug: 20, + info: 30, + warn: 40, + error: 50, + critical: 50, + off: 100 +} + +var logLevel = isTrue(DD_TRACE_DEBUG) + ? Number(DD_TRACE_LOG_LEVEL) || logLevels.debug + : logLevels.off + +var log = { + debug: logLevel <= 20 ? console.debug.bind(console) : function () {}, + info: logLevel <= 30 ? console.info.bind(console) : function () {}, + warn: logLevel <= 40 ? console.warn.bind(console) : function () {}, + error: logLevel <= 50 ? console.error.bind(console) : function () {} +} + +module.exports = log diff --git a/packages/dd-trace/src/guardrails/telemetry.js b/packages/dd-trace/src/guardrails/telemetry.js new file mode 100644 index 00000000000..0c73e1f0bce --- /dev/null +++ b/packages/dd-trace/src/guardrails/telemetry.js @@ -0,0 +1,78 @@ +'use strict' + +/* eslint-disable no-var */ +/* eslint-disable object-shorthand */ + +var fs = require('fs') +var spawn = require('child_process').spawn +var tracerVersion = require('../../../../package.json').version +var log = require('./log') + +module.exports = sendTelemetry + +if (!process.env.DD_INJECTION_ENABLED) { + module.exports = function () {} +} + +if (!process.env.DD_TELEMETRY_FORWARDER_PATH) { + module.exports = function () {} +} + +if (!fs.existsSync(process.env.DD_TELEMETRY_FORWARDER_PATH)) { + module.exports = function () {} +} + +var metadata = { + language_name: 'nodejs', + language_version: process.versions.node, + runtime_name: 'nodejs', + runtime_version: process.versions.node, + tracer_version: tracerVersion, + pid: process.pid +} + +var seen = [] +function hasSeen (point) { + if (point.name === 'abort') { + // This one can only be sent once, regardless of tags + return seen.includes('abort') + } + if (point.name === 'abort.integration') { + // For now, this is the only other one we want to dedupe + var compiledPoint = point.name + point.tags.join('') + return seen.includes(compiledPoint) + } + return false +} + +function sendTelemetry (name, tags) { + var points = name + if (typeof name === 'string') { + points = [{ name: name, tags: tags || [] }] + } + if (['1', 'true', 'True'].indexOf(process.env.DD_INJECT_FORCE) !== -1) { + points = points.filter(function (p) { return ['error', 'complete'].includes(p.name) }) + } + points = points.filter(function (p) { return !hasSeen(p) }) + for (var i = 0; i < points.length; i++) { + points[i].name = 'library_entrypoint.' + points[i].name + } + if (points.length === 0) { + return + } + var proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], { + stdio: 'pipe' + }) + proc.on('error', function () { + log.error('Failed to spawn telemetry forwarder') + }) + proc.on('exit', function (code) { + if (code !== 0) { + log.error('Telemetry forwarder exited with code ' + code) + } + }) + proc.stdin.on('error', function () { + log.error('Failed to write telemetry data to telemetry forwarder') + }) + proc.stdin.end(JSON.stringify({ metadata: metadata, points: points })) +} diff --git a/packages/dd-trace/src/guardrails/util.js b/packages/dd-trace/src/guardrails/util.js new file mode 100644 index 00000000000..9aa60713573 --- /dev/null +++ b/packages/dd-trace/src/guardrails/util.js @@ -0,0 +1,10 @@ +'use strict' + +/* eslint-disable object-shorthand */ + +function isTrue (str) { + str = String(str).toLowerCase() + return str === 'true' || str === '1' +} + +module.exports = { isTrue: isTrue } diff --git a/packages/dd-trace/src/telemetry/init-telemetry.js b/packages/dd-trace/src/telemetry/init-telemetry.js deleted file mode 100644 index a126ecc6238..00000000000 --- a/packages/dd-trace/src/telemetry/init-telemetry.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict' - -const fs = require('fs') -const { spawn } = require('child_process') -const tracerVersion = require('../../../../package.json').version -const log = require('../log') - -module.exports = sendTelemetry - -if (!process.env.DD_INJECTION_ENABLED) { - module.exports = () => {} -} - -if (!process.env.DD_TELEMETRY_FORWARDER_PATH) { - module.exports = () => {} -} - -if (!fs.existsSync(process.env.DD_TELEMETRY_FORWARDER_PATH)) { - module.exports = () => {} -} - -const metadata = { - language_name: 'nodejs', - language_version: process.versions.node, - runtime_name: 'nodejs', - runtime_version: process.versions.node, - tracer_version: tracerVersion, - pid: process.pid -} - -const seen = [] -function hasSeen (point) { - if (point.name === 'abort') { - // This one can only be sent once, regardless of tags - return seen.includes('abort') - } - if (point.name === 'abort.integration') { - // For now, this is the only other one we want to dedupe - const compiledPoint = point.name + point.tags.join('') - return seen.includes(compiledPoint) - } - return false -} - -function sendTelemetry (name, tags = []) { - let points = name - if (typeof name === 'string') { - points = [{ name, tags }] - } - if (['1', 'true', 'True'].includes(process.env.DD_INJECT_FORCE)) { - points = points.filter(p => ['error', 'complete'].includes(p.name)) - } - points = points.filter(p => !hasSeen(p)) - points.forEach(p => { - p.name = `library_entrypoint.${p.name}` - }) - if (points.length === 0) { - return - } - const proc = spawn(process.env.DD_TELEMETRY_FORWARDER_PATH, ['library_entrypoint'], { - stdio: 'pipe' - }) - proc.on('error', () => { - log.error('Failed to spawn telemetry forwarder') - }) - proc.on('exit', (code) => { - if (code !== 0) { - log.error(`Telemetry forwarder exited with code ${code}`) - } - }) - proc.stdin.on('error', () => { - log.error('Failed to write telemetry data to telemetry forwarder') - }) - proc.stdin.end(JSON.stringify({ metadata, points })) -} From 63b6cf8465655f2917101ea3e02d45e4a5f1622f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 28 Nov 2024 11:55:10 +0100 Subject: [PATCH 06/61] [test optimization] Fix logic to bypass jest's require cache (#4950) --- .../office-addin-mock/dependency.js | 7 +++ .../ci-visibility/office-addin-mock/test.js | 6 +++ integration-tests/jest/jest.spec.js | 50 ++++++++++++++++++- packages/datadog-instrumentations/src/jest.js | 8 ++- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 integration-tests/ci-visibility/office-addin-mock/dependency.js create mode 100644 integration-tests/ci-visibility/office-addin-mock/test.js diff --git a/integration-tests/ci-visibility/office-addin-mock/dependency.js b/integration-tests/ci-visibility/office-addin-mock/dependency.js new file mode 100644 index 00000000000..363131a422a --- /dev/null +++ b/integration-tests/ci-visibility/office-addin-mock/dependency.js @@ -0,0 +1,7 @@ +require('office-addin-mock') + +function sum (a, b) { + return a + b +} + +module.exports = sum diff --git a/integration-tests/ci-visibility/office-addin-mock/test.js b/integration-tests/ci-visibility/office-addin-mock/test.js new file mode 100644 index 00000000000..50a3b6c2e28 --- /dev/null +++ b/integration-tests/ci-visibility/office-addin-mock/test.js @@ -0,0 +1,6 @@ +const sum = require('./dependency') +const { expect } = require('chai') + +test('can sum', () => { + expect(sum(1, 2)).to.equal(3) +}) diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index c1f13db9c4d..933c0cdb162 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -61,7 +61,13 @@ describe('jest CommonJS', () => { let testOutput = '' before(async function () { - sandbox = await createSandbox(['jest', 'chai@v4', 'jest-jasmine2', 'jest-environment-jsdom'], true) + sandbox = await createSandbox([ + 'jest', + 'chai@v4', + 'jest-jasmine2', + 'jest-environment-jsdom', + 'office-addin-mock' + ], true) cwd = sandbox.folder startupTestFile = path.join(cwd, testFile) }) @@ -2604,4 +2610,46 @@ describe('jest CommonJS', () => { }) }) }) + + // This happens when using office-addin-mock + context('a test imports a file whose name includes a library we should bypass jest require cache for', () => { + it('does not crash', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false, + early_flake_detection: { + enabled: false + } + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + + assert.equal(tests.length, 1) + }) + + childProcess = exec(runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: 'office-addin-mock/test' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + eventsPromise.then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + }) }) diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 440021f03de..0841ab4783a 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -861,12 +861,18 @@ addHook({ const LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE = [ 'selenium-webdriver', + 'selenium-webdriver/chrome', + 'selenium-webdriver/edge', + 'selenium-webdriver/safari', + 'selenium-webdriver/firefox', + 'selenium-webdriver/ie', + 'selenium-webdriver/chromium', 'winston' ] function shouldBypassJestRequireEngine (moduleName) { return ( - LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.some(library => moduleName.includes(library)) + LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.includes(moduleName) ) } From 2ad4cd0414555a8eca5851987845e0b5f35baec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 28 Nov 2024 12:35:44 +0100 Subject: [PATCH 07/61] [test optimization] Do not init on package managers (#4946) --- ci/init.js | 16 ++++ integration-tests/test-api-manual.spec.js | 6 +- .../test-optimization-startup.spec.js | 84 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 integration-tests/test-optimization-startup.spec.js diff --git a/ci/init.js b/ci/init.js index b54e29abd4d..7b15ed15151 100644 --- a/ci/init.js +++ b/ci/init.js @@ -1,11 +1,22 @@ /* eslint-disable no-console */ const tracer = require('../packages/dd-trace') const { isTrue } = require('../packages/dd-trace/src/util') +const log = require('../packages/dd-trace/src/log') const isJestWorker = !!process.env.JEST_WORKER_ID const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID const isMochaWorker = !!process.env.MOCHA_WORKER_ID +const packageManagers = [ + 'npm', + 'yarn', + 'pnpm' +] + +const isPackageManager = () => { + return packageManagers.some(packageManager => process.argv[1]?.includes(`bin/${packageManager}`)) +} + const options = { startupLogs: false, isCiVisibility: true, @@ -14,6 +25,11 @@ const options = { let shouldInit = true +if (isPackageManager()) { + log.debug('dd-trace is not initialized in a package manager.') + shouldInit = false +} + const isAgentlessEnabled = isTrue(process.env.DD_CIVISIBILITY_AGENTLESS_ENABLED) if (isAgentlessEnabled) { diff --git a/integration-tests/test-api-manual.spec.js b/integration-tests/test-api-manual.spec.js index 419c7c736c5..c403168206a 100644 --- a/integration-tests/test-api-manual.spec.js +++ b/integration-tests/test-api-manual.spec.js @@ -10,24 +10,20 @@ const { getCiVisAgentlessConfig } = require('./helpers') const { FakeCiVisIntake } = require('./ci-visibility-intake') -const webAppServer = require('./ci-visibility/web-app-server') const { TEST_STATUS } = require('../packages/dd-trace/src/plugins/util/test') describe('test-api-manual', () => { - let sandbox, cwd, receiver, childProcess, webAppPort + let sandbox, cwd, receiver, childProcess before(async () => { sandbox = await createSandbox([], true) cwd = sandbox.folder - webAppPort = await getPort() - webAppServer.listen(webAppPort) }) after(async () => { await sandbox.remove() - await new Promise(resolve => webAppServer.close(resolve)) }) beforeEach(async function () { diff --git a/integration-tests/test-optimization-startup.spec.js b/integration-tests/test-optimization-startup.spec.js new file mode 100644 index 00000000000..a15d49cf8ef --- /dev/null +++ b/integration-tests/test-optimization-startup.spec.js @@ -0,0 +1,84 @@ +'use strict' + +const { exec } = require('child_process') + +const getPort = require('get-port') +const { assert } = require('chai') + +const { createSandbox } = require('./helpers') +const { FakeCiVisIntake } = require('./ci-visibility-intake') + +const packageManagers = ['yarn', 'npm', 'pnpm'] + +describe('test optimization startup', () => { + let sandbox, cwd, receiver, childProcess, processOutput + + before(async () => { + sandbox = await createSandbox(packageManagers, true) + cwd = sandbox.folder + }) + + after(async () => { + await sandbox.remove() + }) + + beforeEach(async function () { + processOutput = '' + const port = await getPort() + receiver = await new FakeCiVisIntake(port).start() + }) + + afterEach(async () => { + childProcess.kill() + await receiver.stop() + }) + + packageManagers.forEach(packageManager => { + it(`skips initialization for ${packageManager}`, (done) => { + childProcess = exec(`node ./node_modules/.bin/${packageManager} -v`, + { + cwd, + env: { + ...process.env, + NODE_OPTIONS: '-r dd-trace/ci/init', + DD_TRACE_DEBUG: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.stdout.on('data', (chunk) => { + processOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + assert.include(processOutput, 'dd-trace is not initialized in a package manager') + done() + }) + }) + }) + + it('does not skip initialization for non package managers', (done) => { + childProcess = exec('node -e "console.log(\'hello!\')"', + { + cwd, + env: { + ...process.env, + NODE_OPTIONS: '-r dd-trace/ci/init', + DD_TRACE_DEBUG: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.stdout.on('data', (chunk) => { + processOutput += chunk.toString() + }) + + childProcess.on('exit', () => { + assert.include(processOutput, 'hello!') + assert.notInclude(processOutput, 'dd-trace is not initialized in a package manager') + done() + }) + }) +}) From ec3f21089adc21443f33075d078e2e0827a38bc2 Mon Sep 17 00:00:00 2001 From: Ugaitz Urien Date: Thu, 28 Nov 2024 14:55:30 +0100 Subject: [PATCH 08/61] Fix original url instanceOf url.URL (#4955) --- packages/datadog-instrumentations/src/url.js | 4 ++++ packages/datadog-instrumentations/test/url.spec.js | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/datadog-instrumentations/src/url.js b/packages/datadog-instrumentations/src/url.js index 18edb0079e3..67bef7e8947 100644 --- a/packages/datadog-instrumentations/src/url.js +++ b/packages/datadog-instrumentations/src/url.js @@ -59,6 +59,10 @@ addHook({ name: names }, function (url) { isURL: true }) } + + static [Symbol.hasInstance] (instance) { + return instance instanceof URL + } } }) diff --git a/packages/datadog-instrumentations/test/url.spec.js b/packages/datadog-instrumentations/test/url.spec.js index defb8f08193..57b99e5f897 100644 --- a/packages/datadog-instrumentations/test/url.spec.js +++ b/packages/datadog-instrumentations/test/url.spec.js @@ -1,6 +1,7 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') +const { assert } = require('chai') const { channel } = require('../src/helpers/instrument') const names = ['url', 'node:url'] @@ -68,6 +69,13 @@ names.forEach(name => { }, sinon.match.any) }) + it('instanceof should work also for original instances', () => { + const OriginalUrl = Object.getPrototypeOf(url.URL) + const originalUrl = new OriginalUrl('https://www.datadoghq.com') + + assert.isTrue(originalUrl instanceof url.URL) + }) + ;['host', 'origin', 'hostname'].forEach(property => { it(`should publish on get ${property}`, () => { const urlObject = new url.URL('/path', 'https://www.datadoghq.com') From b6c11a6c72f5eeb97dab2f92da6974c8485282a8 Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Thu, 28 Nov 2024 11:53:08 -0500 Subject: [PATCH 09/61] use weakmap to avoid references from node to datadog stores (#4953) --- packages/datadog-core/src/storage.js | 41 ++++++++++++++++++- packages/datadog-core/test/storage.spec.js | 13 ++++++ packages/dd-trace/src/llmobs/storage.js | 5 +-- .../src/opentelemetry/context_manager.js | 4 +- .../profilers/event_plugins/event.js | 4 +- 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/packages/datadog-core/src/storage.js b/packages/datadog-core/src/storage.js index d28420ed259..15c9fff239c 100644 --- a/packages/datadog-core/src/storage.js +++ b/packages/datadog-core/src/storage.js @@ -2,12 +2,47 @@ const { AsyncLocalStorage } = require('async_hooks') +class DatadogStorage { + constructor () { + this._storage = new AsyncLocalStorage() + } + + disable () { + this._storage.disable() + } + + enterWith (store) { + const handle = {} + stores.set(handle, store) + this._storage.enterWith(handle) + } + + exit (callback, ...args) { + this._storage.exit(callback, ...args) + } + + getStore () { + const handle = this._storage.getStore() + return stores.get(handle) + } + + run (store, fn, ...args) { + const prior = this._storage.getStore() + this.enterWith(store) + try { + return Reflect.apply(fn, null, args) + } finally { + this._storage.enterWith(prior) + } + } +} + const storages = Object.create(null) -const legacyStorage = new AsyncLocalStorage() +const legacyStorage = new DatadogStorage() const storage = function (namespace) { if (!storages[namespace]) { - storages[namespace] = new AsyncLocalStorage() + storages[namespace] = new DatadogStorage() } return storages[namespace] } @@ -18,4 +53,6 @@ storage.exit = legacyStorage.exit.bind(legacyStorage) storage.getStore = legacyStorage.getStore.bind(legacyStorage) storage.run = legacyStorage.run.bind(legacyStorage) +const stores = new WeakMap() + module.exports = storage diff --git a/packages/datadog-core/test/storage.spec.js b/packages/datadog-core/test/storage.spec.js index 89839f1fca3..e5bca4e7d5d 100644 --- a/packages/datadog-core/test/storage.spec.js +++ b/packages/datadog-core/test/storage.spec.js @@ -3,6 +3,7 @@ require('../../dd-trace/test/setup/tap') const { expect } = require('chai') +const { executionAsyncResource } = require('async_hooks') const storage = require('../src/storage') describe('storage', () => { @@ -47,4 +48,16 @@ describe('storage', () => { it('should return the same storage for a namespace', () => { expect(storage('test')).to.equal(testStorage) }) + + it('should not have its store referenced by the underlying async resource', () => { + const resource = executionAsyncResource() + + testStorage.enterWith({ internal: 'internal' }) + + for (const sym of Object.getOwnPropertySymbols(resource)) { + if (sym.toString() === 'Symbol(kResourceStore)' && resource[sym]) { + expect(resource[sym]).to.not.have.property('internal') + } + } + }) }) diff --git a/packages/dd-trace/src/llmobs/storage.js b/packages/dd-trace/src/llmobs/storage.js index 1362aaf966e..82202c18174 100644 --- a/packages/dd-trace/src/llmobs/storage.js +++ b/packages/dd-trace/src/llmobs/storage.js @@ -1,7 +1,6 @@ 'use strict' -// TODO: remove this and use namespaced storage once available -const { AsyncLocalStorage } = require('async_hooks') -const storage = new AsyncLocalStorage() +const { storage: createStorage } = require('../../../datadog-core') +const storage = createStorage('llmobs') module.exports = { storage } diff --git a/packages/dd-trace/src/opentelemetry/context_manager.js b/packages/dd-trace/src/opentelemetry/context_manager.js index fba84eef9f4..430626bbd7e 100644 --- a/packages/dd-trace/src/opentelemetry/context_manager.js +++ b/packages/dd-trace/src/opentelemetry/context_manager.js @@ -1,6 +1,6 @@ 'use strict' -const { AsyncLocalStorage } = require('async_hooks') +const { storage } = require('../../../datadog-core') const { trace, ROOT_CONTEXT } = require('@opentelemetry/api') const DataDogSpanContext = require('../opentracing/span_context') @@ -9,7 +9,7 @@ const tracer = require('../../') class ContextManager { constructor () { - this._store = new AsyncLocalStorage() + this._store = storage('opentelemetry') } active () { diff --git a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js index f47a3468f78..73d3214e231 100644 --- a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js @@ -1,4 +1,4 @@ -const { AsyncLocalStorage } = require('async_hooks') +const { storage } = require('../../../../../datadog-core') const TracingPlugin = require('../../../plugins/tracing') const { performance } = require('perf_hooks') @@ -8,7 +8,7 @@ class EventPlugin extends TracingPlugin { constructor (eventHandler) { super() this.eventHandler = eventHandler - this.store = new AsyncLocalStorage() + this.store = storage('profiling') this.entryType = this.constructor.entryType } From ccc13e260b8d12e2a196a5913d2f2e7c1fc9201d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Mon, 2 Dec 2024 10:35:24 +0100 Subject: [PATCH 10/61] =?UTF-8?q?[test=20optimization]=C2=A0Add=20Dynamic?= =?UTF-8?q?=20Instrumentation=20to=20mocha=20retries=20=20(#4944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dynamic-instrumentation/is-jest.js | 7 + .../test-hit-breakpoint.js | 15 +- .../test-not-hit-breakpoint.js | 15 +- integration-tests/jest/jest.spec.js | 6 +- integration-tests/mocha/mocha.spec.js | 220 +++++++++++++++++- .../src/mocha/utils.js | 3 +- packages/datadog-plugin-jest/src/index.js | 35 --- packages/datadog-plugin-mocha/src/index.js | 38 ++- .../dynamic-instrumentation/worker/index.js | 4 +- packages/dd-trace/src/plugins/ci_plugin.js | 39 +++- 10 files changed, 330 insertions(+), 52 deletions(-) create mode 100644 integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js b/integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js new file mode 100644 index 00000000000..483b2a543d3 --- /dev/null +++ b/integration-tests/ci-visibility/dynamic-instrumentation/is-jest.js @@ -0,0 +1,7 @@ +module.exports = function () { + try { + return typeof jest !== 'undefined' + } catch (e) { + return false + } +} diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js b/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js index fdecdb06edb..ed2e3d14e51 100644 --- a/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js +++ b/integration-tests/ci-visibility/dynamic-instrumentation/test-hit-breakpoint.js @@ -1,15 +1,22 @@ /* eslint-disable */ const sum = require('./dependency') +const isJest = require('./is-jest') +const { expect } = require('chai') // TODO: instead of retrying through jest, this should be retried with auto test retries -jest.retryTimes(1) +if (isJest()) { + jest.retryTimes(1) +} describe('dynamic-instrumentation', () => { - it('retries with DI', () => { - expect(sum(11, 3)).toEqual(14) + it('retries with DI', function () { + if (this.retries) { + this.retries(1) + } + expect(sum(11, 3)).to.equal(14) }) it('is not retried', () => { - expect(sum(1, 2)).toEqual(3) + expect(1 + 2).to.equal(3) }) }) diff --git a/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js b/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js index a4a75aab832..7960852a52c 100644 --- a/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js +++ b/integration-tests/ci-visibility/dynamic-instrumentation/test-not-hit-breakpoint.js @@ -1,17 +1,24 @@ /* eslint-disable */ const sum = require('./dependency') +const isJest = require('./is-jest') +const { expect } = require('chai') // TODO: instead of retrying through jest, this should be retried with auto test retries -jest.retryTimes(1) +if (isJest()) { + jest.retryTimes(1) +} let count = 0 describe('dynamic-instrumentation', () => { - it('retries with DI', () => { + it('retries with DI', function () { + if (this.retries) { + this.retries(1) + } const willFail = count++ === 0 if (willFail) { - expect(sum(11, 3)).toEqual(14) // only throws the first time + expect(sum(11, 3)).to.equal(14) // only throws the first time } else { - expect(sum(1, 2)).toEqual(3) + expect(sum(1, 2)).to.equal(3) } }) }) diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index 933c0cdb162..7bdf04ec071 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -2416,7 +2416,7 @@ describe('jest CommonJS', () => { itr_enabled: false, code_coverage: false, tests_skipping: false, - flaky_test_retries_enabled: true, + flaky_test_retries_enabled: false, early_flake_detection: { enabled: false } @@ -2468,7 +2468,7 @@ describe('jest CommonJS', () => { itr_enabled: false, code_coverage: false, tests_skipping: false, - flaky_test_retries_enabled: true, + flaky_test_retries_enabled: false, early_flake_detection: { enabled: false } @@ -2558,7 +2558,7 @@ describe('jest CommonJS', () => { itr_enabled: false, code_coverage: false, tests_skipping: false, - flaky_test_retries_enabled: true, + flaky_test_retries_enabled: false, early_flake_detection: { enabled: false } diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index 69763845044..f777792c44b 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -35,7 +35,11 @@ const { TEST_CODE_OWNERS, TEST_SESSION_NAME, TEST_LEVEL_EVENT_TYPES, - TEST_EARLY_FLAKE_ABORT_REASON + TEST_EARLY_FLAKE_ABORT_REASON, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_LINE } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -2144,4 +2148,218 @@ describe('mocha CommonJS', function () { }) }) }) + + context('dynamic instrumentation', () => { + it('does not activate dynamic instrumentation if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + runTestsWithCoverageCommand, + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-hit-breakpoint' + ]) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', (code) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(code, 0) + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + + const notRetriedTest = tests.find(test => test.meta[TEST_NAME].includes('is not retried')) + + assert.notProperty(notRetriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '3' + }, + localVariable: { + type: 'number', + value: '2' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }, 5000) + + childProcess = exec( + 'node ./ci-visibility/run-mocha.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-hit-breakpoint' + ]), + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/dynamic-instrumentation/dependency.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + 'node ./ci-visibility/run-mocha.js', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TESTS_TO_RUN: JSON.stringify([ + './dynamic-instrumentation/test-not-hit-breakpoint' + ]), + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + }) }) diff --git a/packages/datadog-instrumentations/src/mocha/utils.js b/packages/datadog-instrumentations/src/mocha/utils.js index 2b51fd6e73b..ce462f13256 100644 --- a/packages/datadog-instrumentations/src/mocha/utils.js +++ b/packages/datadog-instrumentations/src/mocha/utils.js @@ -284,8 +284,9 @@ function getOnTestRetryHandler () { const asyncResource = getTestAsyncResource(test) if (asyncResource) { const isFirstAttempt = test._currentRetry === 0 + const willBeRetried = test._currentRetry < test._retries asyncResource.runInAsyncScope(() => { - testRetryCh.publish({ isFirstAttempt, err }) + testRetryCh.publish({ isFirstAttempt, err, willBeRetried }) }) } const key = getTestToArKey(test) diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index 0b3f87f0e6e..f2494da264d 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -23,12 +23,10 @@ const { JEST_DISPLAY_NAME, TEST_IS_RUM_ACTIVE, TEST_BROWSER_DRIVER, - getFileAndLineNumberFromError, DI_ERROR_DEBUG_INFO_CAPTURED, DI_DEBUG_ERROR_SNAPSHOT_ID, DI_DEBUG_ERROR_FILE, DI_DEBUG_ERROR_LINE, - getTestSuitePath, TEST_NAME } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') @@ -381,39 +379,6 @@ class JestPlugin extends CiPlugin { }) } - // TODO: If the test finishes and the probe is not hit, we should remove the breakpoint - addDiProbe (err, probe) { - const [file, line] = getFileAndLineNumberFromError(err) - - const relativePath = getTestSuitePath(file, this.repositoryRoot) - - const [ - snapshotId, - setProbePromise, - hitProbePromise - ] = this.di.addLineProbe({ file: relativePath, line }) - - probe.setProbePromise = setProbePromise - - hitProbePromise.then(({ snapshot }) => { - // TODO: handle race conditions for this.retriedTestIds - const { traceId, spanId } = this.retriedTestIds - this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, { - debugger: { snapshot }, - dd: { - trace_id: traceId, - span_id: spanId - } - }) - }) - - return { - snapshotId, - file: relativePath, - line - } - } - startTestSpan (test) { const { suite, diff --git a/packages/datadog-plugin-mocha/src/index.js b/packages/datadog-plugin-mocha/src/index.js index 03d201f17b8..302f52ccfb3 100644 --- a/packages/datadog-plugin-mocha/src/index.js +++ b/packages/datadog-plugin-mocha/src/index.js @@ -30,7 +30,12 @@ const { TEST_SUITE, MOCHA_IS_PARALLEL, TEST_IS_RUM_ACTIVE, - TEST_BROWSER_DRIVER + TEST_BROWSER_DRIVER, + TEST_NAME, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -47,6 +52,8 @@ const { const id = require('../../dd-trace/src/id') const log = require('../../dd-trace/src/log') +const debuggerParameterPerTest = new Map() + function getTestSuiteLevelVisibilityTags (testSuiteSpan) { const testSuiteSpanContext = testSuiteSpan.context() const suiteTags = { @@ -185,6 +192,28 @@ class MochaPlugin extends CiPlugin { const store = storage.getStore() const span = this.startTestSpan(testInfo) + const { testName } = testInfo + + const debuggerParameters = debuggerParameterPerTest.get(testName) + + if (debuggerParameters) { + const spanContext = span.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + span.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + span.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + span.setTag(DI_DEBUG_ERROR_FILE, file) + span.setTag(DI_DEBUG_ERROR_LINE, line) + } + this.enter(span, store) }) @@ -242,7 +271,7 @@ class MochaPlugin extends CiPlugin { } }) - this.addSub('ci:mocha:test:retry', ({ isFirstAttempt, err }) => { + this.addSub('ci:mocha:test:retry', ({ isFirstAttempt, willBeRetried, err }) => { const store = storage.getStore() const span = store?.span if (span) { @@ -265,6 +294,11 @@ class MochaPlugin extends CiPlugin { browserDriver: spanTags[TEST_BROWSER_DRIVER] } ) + if (willBeRetried && this.di) { + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(err) + debuggerParameterPerTest.set(testName, debuggerParameters) + } span.finish() finishAllTraceSpans(span) diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js index fbcb52da239..0ba8d01f53c 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js @@ -76,15 +76,17 @@ async function addBreakpoint (snapshotId, probe) { log.debug(`Adding breakpoint at ${path}:${line}`) let generatedPosition = { line } + let hasSourceMap = false if (sourceMapURL && sourceMapURL.startsWith('data:')) { + hasSourceMap = true generatedPosition = await processScriptWithInlineSourceMap({ file, line, sourceMapURL }) } const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { scriptId, - lineNumber: generatedPosition.line + lineNumber: hasSourceMap ? generatedPosition.line : generatedPosition.line - 1 } }) diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index f6692fa4b23..dccf518eb1e 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -21,7 +21,9 @@ const { ITR_CORRELATION_ID, TEST_SOURCE_FILE, TEST_LEVEL_EVENT_TYPES, - TEST_SUITE + TEST_SUITE, + getFileAndLineNumberFromError, + getTestSuitePath } = require('./util/test') const Plugin = require('./plugin') const { COMPONENT } = require('../constants') @@ -289,4 +291,39 @@ module.exports = class CiPlugin extends Plugin { return testSpan } + + // TODO: If the test finishes and the probe is not hit, we should remove the breakpoint + addDiProbe (err, probe) { + const [file, line] = getFileAndLineNumberFromError(err) + + const relativePath = getTestSuitePath(file, this.repositoryRoot) + + const [ + snapshotId, + setProbePromise, + hitProbePromise + ] = this.di.addLineProbe({ file: relativePath, line }) + + if (probe) { // not all frameworks may sync with the set probe promise + probe.setProbePromise = setProbePromise + } + + hitProbePromise.then(({ snapshot }) => { + // TODO: handle race conditions for this.retriedTestIds + const { traceId, spanId } = this.retriedTestIds + this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, { + debugger: { snapshot }, + dd: { + trace_id: traceId, + span_id: spanId + } + }) + }) + + return { + snapshotId, + file: relativePath, + line + } + } } From 865654c9cd8fdb8745a7871ff8b7e1372859579e Mon Sep 17 00:00:00 2001 From: Ugaitz Urien Date: Mon, 2 Dec 2024 10:59:29 +0100 Subject: [PATCH 11/61] Protect req.socket.remoteAddress in appsec reporter (#4954) --- packages/dd-trace/src/appsec/reporter.js | 4 +++- packages/dd-trace/test/appsec/reporter.spec.js | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/dd-trace/src/appsec/reporter.js b/packages/dd-trace/src/appsec/reporter.js index be038279dc8..57519e5bc79 100644 --- a/packages/dd-trace/src/appsec/reporter.js +++ b/packages/dd-trace/src/appsec/reporter.js @@ -148,7 +148,9 @@ function reportAttack (attackData) { newTags['_dd.appsec.json'] = '{"triggers":' + attackData + '}' } - newTags['network.client.ip'] = req.socket.remoteAddress + if (req.socket) { + newTags['network.client.ip'] = req.socket.remoteAddress + } rootSpan.addTags(newTags) } diff --git a/packages/dd-trace/test/appsec/reporter.spec.js b/packages/dd-trace/test/appsec/reporter.spec.js index 757884c3566..cd7cc9a1581 100644 --- a/packages/dd-trace/test/appsec/reporter.spec.js +++ b/packages/dd-trace/test/appsec/reporter.spec.js @@ -223,6 +223,22 @@ describe('reporter', () => { storage.disable() }) + it('should add tags to request span when socket is not there', () => { + delete req.socket + + const result = Reporter.reportAttack('[{"rule":{},"rule_matches":[{}]}]') + + expect(result).to.not.be.false + expect(web.root).to.have.been.calledOnceWith(req) + + expect(span.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.event': 'true', + '_dd.origin': 'appsec', + '_dd.appsec.json': '{"triggers":[{"rule":{},"rule_matches":[{}]}]}' + }) + expect(prioritySampler.setPriority).to.have.been.calledOnceWithExactly(span, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + }) + it('should add tags to request span', () => { const result = Reporter.reportAttack('[{"rule":{},"rule_matches":[{}]}]') expect(result).to.not.be.false From c9be2d49aba745ff7016ac81ce737acbbe80cd7f Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Mon, 2 Dec 2024 15:45:50 -0500 Subject: [PATCH 12/61] fix(config): test for completeness of config telemetry (#4941) * fix(config): test for completeness of config telemetry * fully case sensitive checks * handle blocked key prefixes * handle aggregation and nodejs specific rules * Update to latest config rules * Run eslint * Apply new config mappings * revert .gitignore * Update config_norm_rules.json --- packages/dd-trace/test/config.spec.js | 79 ++ .../telemetry/config_aggregation_list.json | 24 + .../fixtures/telemetry/config_norm_rules.json | 741 ++++++++++++++++++ .../telemetry/config_prefix_block_list.json | 243 ++++++ .../telemetry/nodejs_config_rules.json | 175 +++++ 5 files changed, 1262 insertions(+) create mode 100644 packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json create mode 100644 packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json create mode 100644 packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json create mode 100644 packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 1720c4a5c91..503c2675a95 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -28,6 +28,14 @@ describe('Config', () => { const BLOCKED_TEMPLATE_GRAPHQL_PATH = require.resolve('./fixtures/config/appsec-blocked-graphql-template.json') const BLOCKED_TEMPLATE_GRAPHQL = readFileSync(BLOCKED_TEMPLATE_GRAPHQL_PATH, { encoding: 'utf8' }) const DD_GIT_PROPERTIES_FILE = require.resolve('./fixtures/config/git.properties') + const CONFIG_NORM_RULES_PATH = require.resolve('./fixtures/telemetry/config_norm_rules.json') + const CONFIG_NORM_RULES = readFileSync(CONFIG_NORM_RULES_PATH, { encoding: 'utf8' }) + const CONFIG_PREFIX_BLOCK_LIST_PATH = require.resolve('./fixtures/telemetry/config_prefix_block_list.json') + const CONFIG_PREFIX_BLOCK_LIST = readFileSync(CONFIG_PREFIX_BLOCK_LIST_PATH, { encoding: 'utf8' }) + const CONFIG_AGGREGATION_LIST_PATH = require.resolve('./fixtures/telemetry/config_aggregation_list.json') + const CONFIG_AGGREGATION_LIST = readFileSync(CONFIG_AGGREGATION_LIST_PATH, { encoding: 'utf8' }) + const NODEJS_CONFIG_RULES_PATH = require.resolve('./fixtures/telemetry/nodejs_config_rules.json') + const NODEJS_CONFIG_RULES = readFileSync(NODEJS_CONFIG_RULES_PATH, { encoding: 'utf8' }) function reloadLoggerAndConfig () { log = proxyquire('../src/log', {}) @@ -2258,5 +2266,76 @@ describe('Config', () => { expect(taggingConfig).to.have.property('responsesEnabled', true) expect(taggingConfig).to.have.property('maxDepth', 7) }) + + it('config_norm_rules completeness', () => { + // ⚠️ Did this test just fail? Read here! ⚠️ + // + // Some files are manually copied from dd-go from/to the following paths + // from: https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-payload/static/ + // to: packages/dd-trace/test/fixtures/telemetry/ + // files: + // - config_norm_rules.json + // - config_prefix_block_list.json + // - config_aggregation_list.json + // - nodejs_config_rules.json + // + // If this test fails, it means that a telemetry key was found in config.js that does not + // exist in any of the files listed above in dd-go + // The impact is that telemetry will not be reported to the Datadog backend won't be unusable + // + // To fix this, you must update dd-go to either + // 1) Add an exact config key to match config_norm_rules.json + // 2) Add a prefix that matches the config keys to config_prefix_block_list.json + // 3) Add a prefix rule that fits an existing prefix to config_aggregation_list.json + // 4) (Discouraged) Add a language-specific rule to nodejs_config_rules.json + // + // Once dd-go is updated, you can copy over the files to this repo and merge them in as part of your changes + + function getKeysInDotNotation (obj, parentKey = '') { + const keys = [] + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const fullKey = parentKey ? `${parentKey}.${key}` : key + + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + keys.push(...getKeysInDotNotation(obj[key], fullKey)) + } else { + keys.push(fullKey) + } + } + } + + return keys + } + + const config = new Config() + + const libraryConfigKeys = getKeysInDotNotation(config).sort() + + const nodejsConfigRules = JSON.parse(NODEJS_CONFIG_RULES) + const configNormRules = JSON.parse(CONFIG_NORM_RULES) + const configPrefixBlockList = JSON.parse(CONFIG_PREFIX_BLOCK_LIST) + const configAggregationList = JSON.parse(CONFIG_AGGREGATION_LIST) + + const allowedConfigKeys = [ + ...Object.keys(configNormRules), + ...Object.keys(nodejsConfigRules.normalization_rules) + ] + const blockedConfigKeyPrefixes = [...configPrefixBlockList, ...nodejsConfigRules.prefix_block_list] + const configAggregationPrefixes = [ + ...Object.keys(configAggregationList), + ...Object.keys(nodejsConfigRules.reduce_rules) + ] + + const missingConfigKeys = libraryConfigKeys.filter(key => { + const isAllowed = allowedConfigKeys.includes(key) + const isBlocked = blockedConfigKeyPrefixes.some(prefix => key.startsWith(prefix)) + const isReduced = configAggregationPrefixes.some(prefix => key.startsWith(prefix)) + return !isAllowed && !isBlocked && !isReduced + }) + + expect(missingConfigKeys).to.be.empty + }) }) }) diff --git a/packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json b/packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json new file mode 100644 index 00000000000..b23fc7ff760 --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/config_aggregation_list.json @@ -0,0 +1,24 @@ +{ + "tags": "tags", + "global_tag_": "global_tags", + "trace_global_tags": "trace_global_tags", + "DD_TAGS": "tags", + "trace_span_tags": "trace_span_tags", + "http_client_tag_headers": "http_client_tag_headers", + "DD_TRACE_HEADER_TAGS": "trace_header_tags", + "trace_header_tags": "trace_header_tags", + "_options.headertags": "trace_header_tags", + "trace_request_header_tags": "trace_request_header_tags", + "trace_response_header_tags": "trace_response_header_tags", + "trace_request_header_tags_comma_allowed": "trace_request_header_tags", + "trace.header_tags": "trace_header_tags", + "DD_TRACE_GRPC_TAGS": "trace_grpc_tags", + "DD_TRACE_SERVICE_MAPPING": "trace_service_mappings", + "service_mapping": "trace_service_mappings", + "serviceMapping.": "trace_service_mappings", + "logger.": "logger_configs", + "sampler.rules.": "sampler_rules", + "sampler.spansamplingrules.": "sampler_span_sampling_rules", + "appsec.rules.rules": "appsec_rules", + "installSignature": "install_signature" +} diff --git a/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json b/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json new file mode 100644 index 00000000000..f00fbc27dcb --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/config_norm_rules.json @@ -0,0 +1,741 @@ +{ + "aas_app_type": "aas_app_type", + "aas_configuration_error": "aas_configuration_error", + "aas_functions_runtime_version": "aas_functions_runtime_version", + "aas_siteextensions_version": "aas_site_extensions_version", + "activity_listener_enabled": "activity_listener_enabled", + "agent_transport": "agent_transport", + "DD_AGENT_TRANSPORT": "agent_transport", + "agent_url": "trace_agent_url", + "analytics_enabled": "analytics_enabled", + "autoload_no_compile": "autoload_no_compile", + "cloud_hosting": "cloud_hosting_provider", + "code_hotspots_enabled": "code_hotspots_enabled", + "data_streams_enabled": "data_streams_enabled", + "dsmEnabled": "data_streams_enabled", + "enabled": "trace_enabled", + "environment_fulltrust_appdomain": "environment_fulltrust_appdomain_enabled", + "logInjection_enabled": "logs_injection_enabled", + "partialflush_enabled": "trace_partial_flush_enabled", + "partialflush_minspans": "trace_partial_flush_min_spans", + "platform": "platform", + "profiler_loaded": "profiler_loaded", + "routetemplate_expansion_enabled": "trace_route_template_expansion_enabled", + "routetemplate_resourcenames_enabled": "trace_route_template_resource_names_enabled", + "runtimemetrics_enabled": "runtime_metrics_enabled", + "runtime.metrics.enabled": "runtime_metrics_enabled", + "sample_rate": "trace_sample_rate", + "sampling_rules": "trace_sample_rules", + "span_sampling_rules": "span_sample_rules", + "spanattributeschema": "trace_span_attribute_schema", + "security_enabled": "appsec_enabled", + "stats_computation_enabled": "trace_stats_computation_enabled", + "native_tracer_version": "native_tracer_version", + "managed_tracer_framework": "managed_tracer_framework", + "wcf_obfuscation_enabled": "trace_wcf_obfuscation_enabled", + "data.streams.enabled": "data_streams_enabled", + "dynamic.instrumentation.enabled": "dynamic_instrumentation_enabled", + "dynamic_instrumentation.enabled": "dynamic_instrumentation_enabled", + "HOSTNAME": "agent_hostname", + "dd_agent_host": "agent_host", + "instrumentation.telemetry.enabled": "instrumentation_telemetry_enabled", + "integrations.enabled": "trace_integrations_enabled", + "logs.injection": "logs_injection_enabled", + "logs.mdc.tags.injection": "logs_mdc_tags_injection_enabled", + "os.name": "os_name", + "openai_service": "open_ai_service", + "openai_logs_enabled": "open_ai_logs_enabled", + "openAiLogsEnabled": "open_ai_logs_enabled", + "openai_span_char_limit": "open_ai_span_char_limit", + "openaiSpanCharLimit": "open_ai_span_char_limit", + "openai_span_prompt_completion_sample_rate": "open_ai_span_prompt_completion_sample_rate", + "openai_log_prompt_completion_sample_rate": "open_ai_log_prompt_completion_sample_rate", + "openai_metrics_enabled": "open_ai_metrics_enabled", + "priority.sampling": "trace_priority_sample_enabled", + "profiling.allocation.enabled": "profiling_allocation_enabled", + "profiling.enabled": "profiling_enabled", + "profiling.start-force-first": "profiling_start_force_first", + "remote_config.enabled": "remote_config_enabled", + "remoteConfig.enabled": "remote_config_enabled", + "remoteConfig.pollInterval": "remote_config_poll_interval", + "trace.agent.port": "trace_agent_port", + "trace.agent.v0.5.enabled": "trace_agent_v0.5_enabled", + "trace.analytics.enabled": "trace_analytics_enabled", + "trace.enabled": "trace_enabled", + "trace.client-ip.enabled": "trace_client_ip_enabled", + "trace.jms.propagation.enabled": "trace_jms_propagation_enabled", + "trace.x-datadog-tags.max.length": "trace_x_datadog_tags_max_length", + "trace.kafka.client.propagation.enabled": "trace_kafka_client_propagation_enabled", + "trace.laravel_queue_distributed_tracing": "trace_laravel_queue_distributed_tracing", + "trace.symfony_messenger_distributed_tracing": "trace_symfony_messenger_distributed_tracing", + "trace.symfony_messenger_middlewares": "trace_symfony_messenger_middlewares", + "trace.sources_path": "trace_sources_path", + "trace.log_file": "trace_log_file", + "trace.log_level": "trace_log_level", + "kafka.client.base64.decoding.enabled": "trace_kafka_client_base64_decoding_enabled", + "trace.aws-sdk.propagation.enabled": "trace_aws_sdk_propagation_enabled", + "trace.aws-sdk.legacy.tracing.enabled": "trace_aws_sdk_legacy_tracing_enabled", + "trace.servlet.principal.enabled": "trace_servlet_principal_enabled", + "trace.servlet.async-timeout.error": "trace_servlet_async_timeout_error_enabled", + "trace.rabbit.propagation.enabled": "trace_rabbit_propagation_enabled", + "trace.partial.flush.min.spans": "trace_partial_flush_min_spans", + "trace.sample.rate": "trace_sample_rate", + "trace.sqs.propagation.enabled": "trace_sqs_propagation_enabled", + "trace.peerservicetaginterceptor.enabled": "trace_peer_service_tag_interceptor_enabled", + "dd_trace_sample_rate": "trace_sample_rate", + "trace_methods": "trace_methods", + "tracer_instance_count": "trace_instance_count", + "trace.db.client.split-by-instance": "trace_db_client_split_by_instance", + "trace.db.client.split-by-instance.type.suffix": "trace_db_client_split_by_instance_type_suffix", + "trace.http.client.split-by-domain" : "trace_http_client_split_by_domain", + "trace.agent.timeout": "trace_agent_timeout", + "trace.header.tags.legacy.parsing.enabled": "trace_header_tags_legacy_parsing_enabled", + "trace.client-ip.resolver.enabled": "trace_client_ip_resolver_enabled", + "trace.play.report-http-status": "trace_play_report_http_status", + "trace.jmxfetch.tomcat.enabled": "trace_jmxfetch_tomcat_enabled", + "trace.jmxfetch.kafka.enabled": "trace_jmxfetch_kafka_enabled", + "trace.scope.depth.limit": "trace_scope_depth_limit", + "inferredProxyServicesEnabled": "inferred_proxy_services_enabled", + "resolver.use.loadclass": "resolver_use_loadclass", + "resolver.outline.pool.enabled": "resolver_outline_pool_enabled", + "appsec.apiSecurity.enabled": "api_security_enabled", + "appsec.apiSecurity.requestSampling": "api_security_request_sample_rate", + "appsec.enabled": "appsec_enabled", + "appsec.eventTracking": "appsec_auto_user_events_tracking", + "appsec.eventTracking.mode": "appsec_auto_user_events_tracking", + "appsec.testing": "appsec_testing", + "appsec.trace.rate.limit": "appsec_trace_rate_limit", + "appsec.obfuscatorKeyRegex": "appsec_obfuscation_parameter_key_regexp", + "appsec.obfuscatorValueRegex": "appsec_obfuscation_parameter_value_regexp", + "appsec.rasp.enabled": "appsec_rasp_enabled", + "appsec.rateLimit": "appsec_rate_limit", + "appsec.rules": "appsec_rules", + "appsec.sca_enabled": "appsec_sca_enabled", + "appsec.wafTimeout": "appsec_waf_timeout", + "appsec.sca.enabled": "appsec_sca_enabled", + "clientIpHeader": "trace_client_ip_header", + "clientIpEnabled": "trace_client_ip_enabled", + "clientIpHeaderDisabled": "client_ip_header_disabled", + "debug": "trace_debug_enabled", + "dd.trace.debug": "trace_debug_enabled", + "dogstatsd.hostname": "dogstatsd_hostname", + "dogstatsd.port": "dogstatsd_port", + "dogstatsd.start-delay": "dogstatsd_start_delay", + "env": "env", + "experimental.b3": "experimental_b3", + "experimental.enableGetRumData": "experimental_enable_get_rum_data", + "experimental.exporter": "experimental_exporter", + "experimental.runtimeId": "experimental_runtime_id", + "experimental.sampler.rateLimit": "experimental_sampler_rate_limit", + "experimental.sampler.sampleRate": "experimental_sampler_sample_rate", + "experimental.traceparent": "experimental_traceparent", + "flushInterval": "flush_interval", + "flushMinSpans": "flush_min_spans", + "hostname": "agent_hostname", + "iast.enabled": "iast_enabled", + "iast.cookieFilterPattern": "iast_cookie_filter_pattern", + "iast.deduplication.enabled": "iast_deduplication_enabled", + "iast.maxConcurrentRequests": "iast_max_concurrent_requests", + "iast.max-concurrent-requests": "iast_max_concurrent_requests", + "iast.maxContextOperations": "iast_max_context_operations", + "iast.requestSampling": "iast_request_sampling", + "iast.request-sampling": "iast_request_sampling", + "iast.debug.enabled": "iast_debug_enabled", + "iast.vulnerabilities-per-request": "iast_vulnerability_per_request", + "iast.deduplicationEnabled": "iast_deduplication_enabled", + "iast.redactionEnabled": "iast_redaction_enabled", + "iast.redactionNamePattern": "iast_redaction_name_pattern", + "iast.redactionValuePattern": "iast_redaction_value_pattern", + "iast.telemetryVerbosity": "iast_telemetry_verbosity", + "isAzureFunction": "azure_function", + "isGitUploadEnabled": "git_upload_enabled", + "isIntelligentTestRunnerEnabled": "intelligent_test_runner_enabled", + "logger": "logger", + "logInjection": "logs_injection_enabled", + "logLevel": "trace_log_level", + "memcachedCommandEnabled": "memchached_command_enabled", + "lookup": "lookup", + "plugins": "plugins", + "port": "trace_agent_port", + "profiling.exporters": "profiling_exporters", + "profiling.sourceMap": "profiling_source_map_enabled", + "protocolVersion": "trace_agent_protocol_version", + "querystringObfuscation": "trace_obfuscation_query_string_regexp", + "reportHostname": "trace_report_hostname", + "trace.report-hostname": "trace_report_hostname", + "runtimeMetrics": "runtime_metrics_enabled", + "sampler.rateLimit": "trace_rate_limit", + "trace.rate.limit": "trace_rate_limit", + "sampler.sampleRate": "trace_sample_rate", + "sampleRate": "trace_sample_rate", + "scope": "scope", + "service": "service", + "serviceMapping": "dd_service_mapping", + "site": "site", + "startupLogs": "trace_startup_logs_enabled", + "stats.enabled": "stats_enabled", + "DD_TRACE_HEADER_TAGS": "trace_header_tags", + "tagsHeaderMaxLength": "trace_header_tags_max_length", + "telemetryEnabled": "instrumentation_telemetry_enabled", + "otel_enabled": "trace_otel_enabled", + "trace.otel.enabled": "trace_otel_enabled", + "trace.otel_enabled": "trace_otel_enabled", + "tracing": "trace_enabled", + "url": "trace_agent_url", + "version": "application_version", + "trace.tracer.metrics.enabled": "trace_metrics_enabled", + "trace.perf.metrics.enabled": "trace_perf_metrics_enabled", + "trace.health.metrics.enabled": "trace_health_metrics_enabled", + "trace.health.metrics.statsd.port": "trace_health_metrics_statsd_port", + "trace.grpc.server.trim-package-resource": "trace_grpc_server_trim_package_resource_enabled", + "DD_TRACE_DEBUG": "trace_debug_enabled", + "profiling.start-delay": "profiling_start_delay", + "profiling.upload.period": "profiling_upload_period", + "profiling.async.enabled": "profiling_async_enabled", + "profiling.async.alloc.enabled": "profiling_async_alloc_enabled", + "profiling.directallocation.enabled": "profiling_direct_allocation_enabled", + "profiling.hotspots.enabled": "profiling_hotspots_enabled", + "profiling.async.cpu.enabled": "profiling_async_cpu_enabled", + "profiling.async.memleak.enabled": "profiling_async_memleak_enabled", + "profiling.async.wall.enabled": "profiling_async_wall_enabled", + "profiling.ddprof.enabled": "profiling_ddprof_enabled", + "profiling.heap.enabled": "profiling_heap_enabled", + "profiling.legacy.tracing.integration": "profiling_legacy_tracing_integration_enabled", + "queryStringObfuscation": "trace_obfuscation_query_string_regexp", + "dbmPropagationMode": "dbm_propagation_mode", + "rcPollingInterval": "rc_polling_interval", + "jmxfetch.initial-refresh-beans-period": "jmxfetch_initial_refresh_beans_period", + "jmxfetch.refresh-beans-period": "jmxfetch_initial_refresh_beans_period", + "jmxfetch.multiple-runtime-services.enabled": "jmxfetch_multiple_runtime_services_enabled", + "jmxfetch.enabled": "jmxfetch_enabled", + "jmxfetch.statsd.port": "jmxfetch_statsd_port", + "jmxfetch.check-period": "jmxfetch_check_period", + "appsec.blockedTemplateGraphql": "appsec_blocked_template_graphql", + "appsec.blockedTemplateHtml": "appsec_blocked_template_html", + "appsec.blockedTemplateJson": "appsec_blocked_template_json", + "appsec.waf.timeout": "appsec_waf_timeout", + "civisibility.enabled": "ci_visibility_enabled", + "civisibility.agentless.enabled": "ci_visibility_agentless_enabled", + "isCiVisibility": "ci_visibility_enabled", + "cws.enabled": "cws_enabled", + "AWS_LAMBDA_INITIALIZATION_TYPE": "aws_lambda_initialization_type", + "http.server.tag.query-string": "trace_http_server_tag_query_string", + "http.server.route-based-naming": "trace_http_server_route_based_naming_enabled", + "http.client.tag.query-string": "trace_http_client_tag_query_string", + "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING": "trace_http_client_tag_query_string", + "hystrix.tags.enabled": "hystrix_tags_enabled", + "hystrix.measured.enabled": "hystrix_measured_enabled", + "ignite.cache.include_keys": "ignite_cache_include_keys_enabled", + "dynamic.instrumentation.classfile.dump.enabled": "dynamic_instrumentation_classfile_dump_enabled", + "dynamic.instrumentation.metrics.enabled": "dynamic_instrumentation_metrics_enabled", + "message.broker.split-by-destination": "message_broker_split_by_destination", + "agent_feature_drop_p0s": "agent_feature_drop_p0s", + "appsec.rules.metadata.rules_version": "appsec_rules_metadata_rules_version", + "appsec.rules.version": "appsec_rules_version", + "appsec.customRulesProvided": "appsec_rules_custom_provided", + "dogstatsd_addr": "dogstatsd_url", + "lambda_mode": "lambda_mode", + "profiling.ddprof.alloc.enabled": "profiling_ddprof_alloc_enabled", + "profiling.ddprof.cpu.enabled": "profiling_ddprof_cpu_enabled", + "profiling.ddprof.memleak.enabled": "profiling_ddprof_memleak_enabled", + "profiling.ddprof.wall.enabled": "profiling_ddprof_wall_enabled", + "profiling_endpoints_enabled": "profiling_endpoints_enabled", + "send_retries": "trace_send_retries", + "telemetry.enabled": "instrumentation_telemetry_enabled", + "telemetry.debug": "instrumentation_telemetry_debug_enabled", + "telemetry.logCollection": "instrumentation_telemetry_log_collection_enabled", + "telemetry.metrics": "instrumentation_telemetry_metrics_enabled", + "telemetry.metricsInterval": "instrumentation_telemetry_metrics_interval", + "telemetry.heartbeat.interval": "instrumentation_telemetry_heartbeat_interval", + "telemetry_heartbeat_interval": "instrumentation_telemetry_heartbeat_interval", + "universal_version": "universal_version_enabled", + "global_tag_version": "version", + "traceId128BitGenerationEnabled": "trace_128_bits_id_enabled", + "traceId128BitLoggingEnabled": "trace_128_bits_id_logging_enabled", + "trace.status404decorator.enabled": "trace_status_404_decorator_enabled", + "trace.status404rule.enabled": "trace_status_404_rule_enabled", + "discovery": "agent_discovery_enabled", + "repositoryurl": "repository_url", + "gitmetadataenabled": "git_metadata_enabled", + "commitsha": "commit_sha", + "isgcpfunction": "is_gcp_function", + "isGCPFunction": "is_gcp_function", + "legacy.installer.enabled": "legacy_installer_enabled", + "trace.request_init_hook": "trace_request_init_hook", + "dogstatsd_url": "dogstatsd_url", + "distributed_tracing": "trace_distributed_trace_enabled", + "autofinish_spans": "trace_auto_finish_spans_enabled", + "trace.url_as_resource_names_enabled": "trace_url_as_resource_names_enabled", + "integrations_disabled": "trace_disabled_integrations", + "priority_sampling": "trace_priority_sampling_enabled", + "trace.auto_flush_enabled": "trace_auto_flush_enabled", + "trace.measure_compile_time": "trace_measure_compile_time_enabled", + "trace.measure_peak_memory_usage": "trace_measure_peak_memory_usage_enabled", + "trace.health_metrics_heartbeat_sample_rate": "trace_health_metrics_heartbeat_sample_rate", + "trace.redis_client_split_by_host": "trace_redis_client_split_by_host_enabled", + "trace.memory_limit": "trace_memory_limit", + "trace.flush_collect_cycles": "trace_flush_collect_cycles_enabled", + "trace.resource_uri_fragment_regex": "trace_resource_uri_fragment_regex", + "trace.resource_uri_mapping_incoming": "trace_resource_uri_mapping_incoming", + "trace.resource_uri_mapping_outgoing": "trace_resource_uri_mapping_outgoing", + "trace.resource_uri_query_param_allowed": "trace_resource_uri_query_param_allowed", + "trace.http_url_query_param_allowed": "trace_http_url_query_param_allowed", + "trace.http_post_data_param_allowed": "trace_http_post_data_param_allowed", + "trace.sampling_rules": "trace_sample_rules", + "span_sampling_rules_file": "span_sample_rules_file", + "trace.propagation_style_extract": "trace_propagation_style_extract", + "trace.propagation_style_inject": "trace_propagation_style_inject", + "trace.propagation_style": "trace_propagation_style", + "trace.propagation_extract_first": "trace_propagation_extract_first", + "tracePropagationExtractFirst": "trace_propagation_extract_first", + "tracePropagationStyle.extract": "trace_propagation_style_extract", + "tracePropagationStyle.inject": "trace_propagation_style_inject", + "tracePropagationStyle,otelPropagators": "trace_propagation_style_otel_propagators", + "tracing.distributed_tracing.propagation_extract_style": "trace_propagation_style_extract", + "tracing.distributed_tracing.propagation_inject_style": "trace_propagation_style_inject", + "trace.traced_internal_functions": "trace_traced_internal_functions", + "trace.agent_connect_timeout": "trace_agent_connect_timeout", + "trace.debug_prng_seed": "trace_debug_prng_seed", + "log_backtrace": "trace_log_backtrace_enabled", + "trace.generate_root_span": "trace_generate_root_span_enabled", + "trace.spans_limit": "trace_spans_limit", + "trace.128_bit_traceid_generation_enabled": "trace_128_bits_id_enabled", + "trace.agent_max_consecutive_failures": "trace_send_retries", + "trace.agent_attempt_retry_time_msec": "trace_agent_attempt_retry_time_msec", + "trace.bgs_connect_timeout": "trace_bgs_connect_timeout", + "trace.bgs_timeout": "trace_bgs_timeout", + "trace.agent_flush_interval": "trace_agent_flush_interval", + "trace.agent_flush_after_n_requests": "trace_agent_flush_after_n_requests", + "trace.shutdown_timeout": "trace_shutdown_timeout", + "trace.agent_debug_verbose_curl": "trace_agent_debug_verbose_curl_enabled", + "trace.debug_curl_output": "trace_debug_curl_output_enabled", + "trace.beta_high_memory_pressure_percent": "trace_beta_high_memory_pressure_percent", + "trace.warn_legacy_dd_trace": "trace_warn_legacy_dd_trace_enabled", + "trace.retain_thread_capabilities": "trace_retain_thread_capabilities_enabled", + "trace.client_ip_header": "client_ip_header", + "trace.forked_process": "trace_forked_process_enabled", + "trace.hook_limit": "trace_hook_limit", + "trace.agent_max_payload_size": "trace_agent_max_payload_size", + "trace.agent_stack_initial_size": "trace_agent_stack_initial_size", + "trace.agent_stack_backlog": "trace_agent_stack_backlog", + "trace.agent_retries": "trace_send_retries", + "trace.agent_test_session_token": "trace_agent_test_session_token", + "trace.propagate_user_id_default": "trace_propagate_user_id_default_enabled", + "dbm_propagation_mode": "dbm_propagation_mode", + "trace.remove_root_span_laravel_queue": "trace_remove_root_span_laravel_queue_enabled", + "trace.remove_root_span_symfony_messenger": "trace_remove_root_span_symfony_messenger_enabled", + "trace.remove_autoinstrumentation_orphans": "trace_remove_auto_instrumentation_orphans_enabled", + "trace.memcached_obfuscation": "trace_memcached_obfuscation_enabled", + "DD_TRACE_CONFIG_FILE": "trace_config_file", + "DD_DOTNET_TRACER_CONFIG_FILE": "trace_config_file", + "DD_ENV": "env", + "DD_SERVICE": "service", + "DD_SERVICE_NAME": "service", + "DD_VERSION": "application_version", + "DD_GIT_REPOSITORY_URL": "repository_url", + "git_repository_url": "repository_url", + "DD_GIT_COMMIT_SHA": "commit_sha", + "DD_TRACE_GIT_METADATA_ENABLED": "git_metadata_enabled", + "trace.git_metadata_enabled": "git_metadata_enabled", + "git_commit_sha": "commit_sha", + "DD_TRACE_ENABLED": "trace_enabled", + "DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED": "experimental_appsec_standalone_enabled", + "DD_INTERNAL_WAIT_FOR_DEBUGGER_ATTACH": "internal_wait_for_debugger_attach_enabled", + "DD_INTERNAL_WAIT_FOR_NATIVE_DEBUGGER_ATTACH": "internal_wait_for_native_debugger_attach_enabled", + "DD_DISABLED_INTEGRATIONS": "trace_disabled_integrations", + "DD_TRACE_ANALYTICS_ENABLED": "trace_analytics_enabled", + "DD_TRACE_BUFFER_SIZE": "trace_serialization_buffer_size", + "trace.buffer_size": "trace_serialization_buffer_size", + "DD_TRACE_BATCH_INTERVAL": "trace_serialization_batch_interval", + "DD_LOG_INJECTION": "logs_injection_enabled", + "DD_LOGS_INJECTION": "logs_injection_enabled", + "DD_TRACE_RATE_LIMIT": "trace_rate_limit", + "DD_MAX_TRACES_PER_SECOND": "trace_rate_limit", + "DD_TRACE_SAMPLING_RULES": "trace_sample_rules", + "DD_SPAN_SAMPLING_RULES": "span_sample_rules", + "DD_TRACE_SAMPLE_RATE": "trace_sample_rate", + "DD_APM_ENABLE_RARE_SAMPLER": "trace_rare_sampler_enabled", + "DD_TRACE_METRICS_ENABLED": "trace_metrics_enabled", + "DD_RUNTIME_METRICS_ENABLED": "runtime_metrics_enabled", + "DD_TRACE_AGENT_PATH": "agent_trace_agent_excecutable_path", + "DD_TRACE_AGENT_ARGS": "agent_trace_agent_excecutable_args", + "DD_DOGSTATSD_PATH": "agent_dogstatsd_executable_path", + "DD_DOGSTATSD_ARGS": "agent_dogstatsd_executable_args", + "DD_DIAGNOSTIC_SOURCE_ENABLED": "trace_diagnostic_source_enabled", + "DD_SITE": "site", + "DD_TRACE_HTTP_CLIENT_EXCLUDED_URL_SUBSTRINGS": "trace_http_client_excluded_urls", + "DD_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", + "DD_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", + "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": "trace_http_server_error_statuses", + "DD_TRACE_HTTP_CLIENT_ERROR_STATUSES": "trace_http_client_error_statuses", + "DD_TRACE_CLIENT_IP_HEADER": "trace_client_ip_header", + "DD_TRACE_CLIENT_IP_ENABLED": "trace_client_ip_enabled", + "DD_TRACE_KAFKA_CREATE_CONSUMER_SCOPE_ENABLED": "trace_kafka_create_consumer_scope_enabled", + "DD_TRACE_EXPAND_ROUTE_TEMPLATES_ENABLED": "trace_route_template_expansion_enabled", + "DD_TRACE_STATS_COMPUTATION_ENABLED": "trace_stats_computation_enabled", + "_DD_TRACE_STATS_COMPUTATION_INTERVAL": "trace_stats_computation_interval", + "DD_TRACE_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", + "DD_PROPAGATION_STYLE_INJECT": "trace_propagation_style_inject", + "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", + "DD_PROPAGATION_STYLE_EXTRACT": "trace_propagation_style_extract", + "DD_TRACE_PROPAGATION_STYLE": "trace_propagation_style", + "DD_TRACE_PROPAGATION_EXTRACT_FIRST": "trace_propagation_extract_first", + "DD_TRACE_METHODS": "trace_methods", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP": "trace_obfuscation_query_string_regexp", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP_TIMEOUT": "trace_obfuscation_query_string_regexp_timeout", + "DD_HTTP_SERVER_TAG_QUERY_STRING_SIZE": "trace_http_server_tag_query_string_size", + "DD_HTTP_SERVER_TAG_QUERY_STRING": "trace_http_server_tag_query_string_enabled", + "DD_DBM_PROPAGATION_MODE": "dbm_propagation_mode", + "DD_TRACE_SPAN_ATTRIBUTE_SCHEMA": "trace_span_attribute_schema", + "DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED": "trace_peer_service_defaults_enabled", + "DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED": "trace_remove_integration_service_names_enabled", + "DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH": "trace_x_datadog_tags_max_length", + "DD_DATA_STREAMS_ENABLED": "data_streams_enabled", + "DD_DATA_STREAMS_LEGACY_HEADERS": "data_streams_legacy_headers", + "DD_CIVISIBILITY_ENABLED": "ci_visibility_enabled", + "DD_CIVISIBILITY_AGENTLESS_ENABLED": "ci_visibility_agentless_enabled", + "DD_CIVISIBILITY_AGENTLESS_URL": "ci_visibility_agentless_url", + "DD_CIVISIBILITY_LOGS_ENABLED": "ci_visibility_logs_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_ENABLED": "ci_visibility_code_coverage_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_MODE": "ci_visibility_code_coverage_mode", + "DD_CIVISIBILITY_CODE_COVERAGE_SNK_FILEPATH": "ci_visibility_code_coverage_snk_path", + "DD_CIVISIBILITY_CODE_COVERAGE_ENABLE_JIT_OPTIMIZATIONS": "ci_visibility_code_coverage_jit_optimisations_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_PATH": "ci_visibility_code_coverage_path", + "DD_CIVISIBILITY_GIT_UPLOAD_ENABLED": "ci_visibility_git_upload_enabled", + "DD_CIVISIBILITY_TESTSSKIPPING_ENABLED": "ci_visibility_test_skipping_enabled", + "DD_CIVISIBILITY_ITR_ENABLED": "ci_visibility_intelligent_test_runner_enabled", + "DD_CIVISIBILITY_FORCE_AGENT_EVP_PROXY": "ci_visibility_force_agent_evp_proxy_enabled", + "DD_CIVISIBILITY_EXTERNAL_CODE_COVERAGE_PATH": "ci_visibility_code_coverage_external_path", + "DD_CIVISIBILITY_GAC_INSTALL_ENABLED": "ci_visibility_gac_install_enabled", + "DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED": "ci_visibility_early_flake_detection_enabled", + "DD_CIVISIBILITY_CODE_COVERAGE_COLLECTORPATH": "ci_visibility_code_coverage_collectorpath", + "DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS": "ci_visibility_rum_flush_wait_millis", + "DD_CIVISIBILITY_FLAKY_RETRY_ENABLED": "ci_visibility_flaky_retry_enabled", + "DD_CIVISIBILITY_FLAKY_RETRY_COUNT": "ci_visibility_flaky_retry_count", + "DD_CIVISIBILITY_TOTAL_FLAKY_RETRY_COUNT": "ci_visibility_total_flaky_retry_count", + "DD_TEST_SESSION_NAME": "test_session_name", + "DD_PROXY_HTTPS": "proxy_https", + "DD_PROXY_NO_PROXY": "proxy_no_proxy", + "DD_TRACE_DEBUG_LOOKUP_MDTOKEN": "trace_lookup_mdtoken_enabled", + "DD_TRACE_DEBUG_LOOKUP_FALLBACK": "trace_lookup_fallback_enabled", + "DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED": "trace_route_template_resource_names_enabled", + "DD_TRACE_DELAY_WCF_INSTRUMENTATION_ENABLED": "trace_delay_wcf_instrumentation_enabled", + "DD_TRACE_WCF_WEB_HTTP_RESOURCE_NAMES_ENABLED": "trace_wcf_web_http_resource_names_enabled", + "DD_TRACE_WCF_RESOURCE_OBFUSCATION_ENABLED": "trace_wcf_obfuscation_enabled", + "DD_TRACE_HEADER_TAG_NORMALIZATION_FIX_ENABLED": "trace_header_tag_normalization_fix_enabled", + "DD_TRACE_OTEL_ENABLED": "trace_otel_enabled", + "DD_TRACE_OTEL_LEGACY_OPERATION_NAME_ENABLED": "trace_otel_legacy_operation_name_enabled", + "DD_TRACE_ACTIVITY_LISTENER_ENABLED": "trace_activity_listener_enabled", + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED": "trace_128_bits_id_enabled", + "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED": "trace_128_bits_id_logging_enabled", + "DD_TRACE_HEALTH_METRICS_ENABLED": "dd_trace_health_metrics_enabled", + "DD_LIB_INJECTION_ATTEMPTED": "dd_lib_injection_attempted", + "DD_LIB_INJECTED": "dd_lib_injected", + "DD_INJECT_FORCED": "dd_lib_injection_forced", + "DD_SPAN_SAMPLING_RULES_FILE": "dd_span_sampling_rules_file", + "DD_TRACE_COMPUTE_STATS": "dd_trace_compute_stats", + "DD_EXCEPTION_DEBUGGING_ENABLED": "dd_exception_debugging_enabled", + "DD_EXCEPTION_DEBUGGING_MAX_FRAMES_TO_CAPTURE": "dd_exception_debugging_max_frames_to_capture", + "DD_EXCEPTION_DEBUGGING_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_debugging_capture_full_callstack_enabled", + "DD_EXCEPTION_DEBUGGING_RATE_LIMIT_SECONDS": "dd_exception_debugging_rate_limit_seconds", + "DD_EXCEPTION_DEBUGGING_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_debugging_max_exception_analysis_limit", + "DD_EXCEPTION_REPLAY_ENABLED": "dd_exception_replay_enabled", + "DD_EXCEPTION_REPLAY_MAX_FRAMES_TO_CAPTURE": "dd_exception_replay_max_frames_to_capture", + "DD_EXCEPTION_REPLAY_CAPTURE_FULL_CALLSTACK_ENABLED": "dd_exception_replay_capture_full_callstack_enabled", + "DD_EXCEPTION_REPLAY_RATE_LIMIT_SECONDS": "dd_exception_replay_rate_limit_seconds", + "DD_EXCEPTION_REPLAY_MAX_EXCEPTION_ANALYSIS_LIMIT": "dd_exception_replay_max_exception_analysis_limit", + "exception_replay_capture_interval_seconds": "dd_exception_replay_capture_interval_seconds", + "exception_replay_capture_max_frames": "dd_exception_replay_capture_max_frames", + "exception_replay_enabled": "dd_exception_replay_enabled", + "DD_TRACE_OBFUSCATION_QUERY_STRING_PATTERN": "dd_trace_obfuscation_query_string_pattern", + "DD_CALL_BASIC_CONFIG": "dd_call_basic_config", + "DD_SERVICE_MAPPING": "dd_service_mapping", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED": "instrumentation_telemetry_enabled", + "DD_INSTRUMENTATION_TELEMETRY_AGENTLESS_ENABLED": "instrumentation_telemetry_agentless_enabled", + "DD_INSTRUMENTATION_TELEMETRY_AGENT_PROXY_ENABLED": "instrumentation_telemetry_agent_proxy_enabled", + "DD_INSTRUMENTATION_TELEMETRY_URL": "instrumentation_telemetry_agentless_url", + "DD_TELEMETRY_HEARTBEAT_INTERVAL": "instrumentation_telemetry_heartbeat_interval", + "DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED": "instrumentation_telemetry_dependency_collection_enabled", + "DD_TELEMETRY_LOG_COLLECTION_ENABLED": "instrumentation_telemetry_log_collection_enabled", + "DD_TELEMETRY_METRICS_ENABLED": "instrumentation_telemetry_metrics_enabled", + "DD_INTERNAL_TELEMETRY_V2_ENABLED": "instrumentation_telemetry_v2_enabled", + "DD_INTERNAL_TELEMETRY_DEBUG_ENABLED": "instrumentation_telemetry_debug_enabled", + "DD_APPSEC_ENABLED": "appsec_enabled", + "DD_APPSEC_RULES": "appsec_rules", + "DD_APPSEC_IPHEADER": "appsec_ip_header", + "DD_APPSEC_EXTRA_HEADERS": "appsec_extra_headers", + "DD_APPSEC_KEEP_TRACES": "appsec_force_keep_traces_enabled", + "DD_APPSEC_TRACE_RATE_LIMIT": "appsec_trace_rate_limit", + "DD_APPSEC_WAF_TIMEOUT": "appsec_waf_timeout", + "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP": "appsec_obfuscation_parameter_key_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP": "appsec_obfuscation_parameter_value_regexp", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML": "appsec_blocked_template_html", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON": "appsec_blocked_template_json", + "DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING": "appsec_auto_user_events_tracking", + "DD_APPSEC_RASP_ENABLED": "appsec_rasp_enabled", + "DD_APPSEC_STACK_TRACE_ENABLED": "appsec_stack_trace_enabled", + "DD_APPSEC_MAX_STACK_TRACES": "appsec_max_stack_traces", + "DD_APPSEC_MAX_STACK_TRACE_DEPTH": "appsec_max_stack_trace_depth", + "DD_APPSEC_MAX_STACK_TRACE_DEPTH_TOP_PERCENT": "appsec_max_stack_trace_depth_top_percent", + "DD_APPSEC_SCA_ENABLED": "appsec_sca_enabled", + "DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": "appsec_auto_user_instrumentation_mode", + "DD_EXPERIMENTAL_APPSEC_USE_UNSAFE_ENCODER": "appsec_use_unsafe_encoder", + "DD_API_SECURITY_REQUEST_SAMPLE_RATE":"api_security_request_sample_rate", + "DD_API_SECURITY_MAX_CONCURRENT_REQUESTS":"api_security_max_concurrent_requests", + "DD_API_SECURITY_SAMPLE_DELAY":"api_security_sample_delay", + "DD_API_SECURITY_ENABLED":"api_security_enabled", + "DD_EXPERIMENTAL_API_SECURITY_ENABLED":"experimental_api_security_enabled", + "DD_APPSEC_WAF_DEBUG": "appsec_waf_debug_enabled", + "DD_AZURE_APP_SERVICES": "aas_enabled", + "DD_AAS_DOTNET_EXTENSION_VERSION": "aas_site_extensions_version", + "WEBSITE_OWNER_NAME": "aas_website_owner_name", + "WEBSITE_RESOURCE_GROUP": "aas_website_resource_group", + "WEBSITE_SITE_NAME": "aas_website_site_name", + "FUNCTIONS_EXTENSION_VERSION": "aas_functions_runtime_version", + "FUNCTIONS_WORKER_RUNTIME": "aas_functions_worker_runtime", + "COMPUTERNAME": "aas_instance_name", + "WEBSITE_INSTANCE_ID": "aas_website_instance_id", + "WEBSITE_OS": "aas_website_os", + "WEBSITE_SKU": "aas_website_sku", + "FUNCTION_NAME": "gcp_deprecated_function_name", + "GCP_PROJECT": "gcp_deprecated_project", + "K_SERVICE": "gcp_function_name", + "FUNCTION_TARGET": "gcp_function_target", + "DD_AAS_ENABLE_CUSTOM_TRACING": "aas_custom_tracing_enabled", + "DD_AAS_ENABLE_CUSTOM_METRICS": "aas_custom_metrics_enabled", + "DD_DYNAMIC_INSTRUMENTATION_ENABLED": "dynamic_instrumentation_enabled", + "DD_DEBUGGER_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", + "DD_DEBUGGER_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", + "DD_DEBUGGER_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", + "DD_DEBUGGER_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", + "DD_DEBUGGER_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", + "DD_DYNAMIC_INSTRUMENTATION_MAX_DEPTH_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_depth", + "DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE": "dynamic_instrumentation_serialization_max_duration", + "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_BATCH_SIZE": "dynamic_instrumentation_upload_batch_size", + "DD_DYNAMIC_INSTRUMENTATION_DIAGNOSTICS_INTERVAL": "dynamic_instrumentation_diagnostics_interval", + "DD_DYNAMIC_INSTRUMENTATION_UPLOAD_FLUSH_INTERVAL": "dynamic_instrumentation_upload_interval", + "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS": "dynamic_instrumentation_redacted_identifiers", + "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES": "dynamic_instrumentation_redacted_types", + "dynamic_instrumentation.redacted_types": "dynamic_instrumentation_redacted_types", + "dynamic_instrumentation.redacted_identifiers": "dynamic_instrumentation_redacted_identifiers", + "DD_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "symbol_database_batch_size_bytes", + "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_BATCH_SIZE_BYTES": "dynamic_instrumentation_symbol_database_batch_size_bytes", + "DD_SYMBOL_DATABASE_UPLOAD_ENABLED": "symbol_database_upload_enabled", + "DD_DYNAMIC_INSTRUMENTATION_SYMBOL_DATABASE_UPLOAD_ENABLED": "dynamic_instrumentation_symbol_database_upload_enabled", + "DD_INTERAL_FORCE_SYMBOL_DATABASE_UPLOAD": "internal_force_symbol_database_upload", + "DD_THIRD_PARTY_DETECTION_INCLUDES": "third_party_detection_includes", + "DD_THIRD_PARTY_DETECTION_EXCLUDES": "third_party_detection_excludes", + "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_INCLUDES": "symbol_database_third_party_detection_includes", + "DD_SYMBOL_DATABASE_THIRD_PARTY_DETECTION_EXCLUDES": "symbol_database_third_party_detection_excludes", + "DD_CODE_ORIGIN_FOR_SPANS_ENABLED": "code_origin_for_spans_enabled", + "DD_CODE_ORIGIN_FOR_SPANS_MAX_USER_FRAMES": "code_origin_for_spans_max_user_frames", + "DD_LOGS_DIRECT_SUBMISSION_INTEGRATIONS": "logs_direct_submission_integrations", + "DD_LOGS_DIRECT_SUBMISSION_HOST": "logs_direct_submission_host", + "DD_LOGS_DIRECT_SUBMISSION_SOURCE": "logs_direct_submission_source", + "DD_LOGS_DIRECT_SUBMISSION_TAGS": "logs_direct_submission_tags", + "DD_LOGS_DIRECT_SUBMISSION_URL": "logs_direct_submission_url", + "DD_LOGS_DIRECT_SUBMISSION_MINIMUM_LEVEL": "logs_direct_submission_minimum_level", + "DD_LOGS_DIRECT_SUBMISSION_MAX_BATCH_SIZE": "logs_direct_submission_max_batch_size", + "DD_LOGS_DIRECT_SUBMISSION_MAX_QUEUE_SIZE": "logs_direct_submission_max_queue_size", + "DD_LOGS_DIRECT_SUBMISSION_BATCH_PERIOD_SECONDS": "logs_direct_submission_batch_period_seconds", + "DD_AGENT_HOST": "agent_host", + "DATADOG_TRACE_AGENT_HOSTNAME": "agent_host", + "DD_TRACE_AGENT_HOSTNAME": "agent_host", + "DD_TRACE_AGENT_PORT": "trace_agent_port", + "DATADOG_TRACE_AGENT_PORT": "trace_agent_port", + "DD_TRACE_PIPE_NAME": "trace_agent_named_pipe", + "DD_TRACE_PIPE_TIMEOUT_MS": "trace_agent_named_pipe_timeout_ms", + "DD_DOGSTATSD_PIPE_NAME": "dogstatsd_named_pipe", + "DD_APM_RECEIVER_PORT": "trace_agent_port", + "DD_TRACE_AGENT_URL": "trace_agent_url", + "DD_DOGSTATSD_PORT": "dogstatsd_port", + "DD_TRACE_PARTIAL_FLUSH_ENABLED": "trace_partial_flush_enabled", + "DD_TRACE_PARTIAL_FLUSH_MIN_SPANS": "trace_partial_flush_min_spans", + "DD_APM_RECEIVER_SOCKET": "trace_agent_socket", + "DD_DOGSTATSD_SOCKET": "dogstatsd_socket", + "DD_DOGSTATSD_URL": "dogstatsd_url", + "DD_IAST_ENABLED": "iast_enabled", + "DD_IAST_WEAK_HASH_ALGORITHMS": "iast_weak_hash_algorithms", + "DD_IAST_WEAK_CIPHER_ALGORITHMS": "iast_weak_cipher_algorithms", + "DD_IAST_DEDUPLICATION_ENABLED": "iast_deduplication_enabled", + "DD_IAST_REQUEST_SAMPLING": "iast_request_sampling_percentage", + "DD_IAST_MAX_CONCURRENT_REQUESTS": "iast_max_concurrent_requests", + "DD_IAST_MAX_RANGE_COUNT": "iast_max_range_count", + "DD_IAST_VULNERABILITIES_PER_REQUEST": "iast_vulnerability_per_request", + "DD_IAST_REDACTION_ENABLED": "iast_redaction_enabled", + "DD_IAST_REDACTION_KEYS_REGEXP": "iast_redaction_keys_regexp", + "DD_IAST_REDACTION_VALUES_REGEXP": "iast_redaction_values_regexp", + "DD_IAST_REDACTION_NAME_PATTERN": "iast_redaction_name_pattern", + "DD_IAST_REDACTION_VALUE_PATTERN": "iast_redaction_value_pattern", + "DD_IAST_REDACTION_REGEXP_TIMEOUT": "iast_redaction_regexp_timeout", + "DD_IAST_REGEXP_TIMEOUT": "iast_regexp_timeout", + "DD_IAST_TELEMETRY_VERBOSITY": "iast_telemetry_verbosity", + "DD_IAST_TRUNCATION_MAX_VALUE_LENGTH": "iast_truncation_max_value_length", + "DD_IAST_DB_ROWS_TO_TAINT": "iast_db_rows_to_taint", + "DD_IAST_COOKIE_FILTER_PATTERN": "iast_cookie_filter_pattern", + "DD_TRACE_STARTUP_LOGS": "trace_startup_logs_enabled", + "DD_TRACE_DISABLED_ADONET_COMMAND_TYPES": "trace_disabled_adonet_command_types", + "DD_MAX_LOGFILE_SIZE": "trace_log_file_max_size", + "DD_TRACE_LOGGING_RATE": "trace_log_rate", + "DD_TRACE_LOG_PATH": "trace_log_path", + "DD_TRACE_LOG_DIRECTORY": "trace_log_directory", + "DD_TRACE_LOGFILE_RETENTION_DAYS": "trace_log_file_retention_days", + "DD_TRACE_LOG_SINKS": "trace_log_sinks", + "DD_TRACE_COMMANDS_COLLECTION_ENABLED": "trace_commands_collection_enabled", + "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS": "remote_config_poll_interval", + "remote_config_poll_interval_seconds": "remote_config_poll_interval", + "DD_INTERNAL_RCM_POLL_INTERVAL": "remote_config_poll_interval", + "trace.128_bit_traceid_logging_enabled": "trace_128_bits_id_logging_enabled", + "DD_PROFILING_ENABLED": "profiling_enabled", + "DD_PROFILING_CODEHOTSPOTS_ENABLED": "profiling_codehotspots_enabled", + "DD_PROFILING_ENDPOINT_COLLECTION_ENABLED": "profiling_endpoint_collection_enabled", + "DD_LOG_LEVEL": "agent_log_level", + "DD_TAGS": "agent_tags", + "DD_TRACE_GLOBAL_TAGS": "trace_tags", + "trace.agent_url": "trace_agent_url", + "trace.append_trace_ids_to_logs": "trace_append_trace_ids_to_logs", + "trace.client_ip_enabled": "trace_client_ip_enabled", + "trace.analytics_enabled": "trace_analytics_enabled", + "trace.rate_limit": "trace_rate_limit", + "trace.report_hostname": "trace_report_hostname", + "trace.http_client_split_by_domain": "trace_http_client_split_by_domain", + "trace.debug": "trace_debug_enabled", + "trace.agent_timeout": "trace_agent_timeout", + "trace.agent_port": "trace_agent_port", + "trace.x_datadog_tags_max_length": "trace_x_datadog_tags_max_length", + "trace.obfuscation_query_string_regexp": "trace_obfuscation_query_string_regexp", + "trace.peer_service_defaults_enabled": "trace_peer_service_defaults_enabled", + "trace.propagate_service": "trace_propagate_service", + "trace.remove_integration_service_names_enabled": "trace_remove_integration_service_names_enabled", + "trace.sample_rate": "trace_sample_rate", + "trace.health_metrics_enabled": "trace_health_metrics_enabled", + "trace.telemetry_enabled": "instrumentation_telemetry_enabled", + "trace.cli_enabled": "trace_cli_enabled", + "trace.db_client_split_by_instance": "trace_db_client_split_by_instance", + "trace.startup_logs": "trace_startup_logs", + "http_server_route_based_naming": "http_server_route_based_naming", + "DD_TRACE_PEER_SERVICE_MAPPING": "trace_peer_service_mapping", + "peerServiceMapping": "trace_peer_service_mapping", + "trace.peer.service.mapping": "trace_peer_service_mapping", + "trace.peer_service_mapping": "trace_peer_service_mapping", + "spanComputePeerService": "trace_peer_service_defaults_enabled", + "spanLeakDebug": "span_leak_debug", + "trace.peer.service.defaults.enabled": "trace_peer_service_defaults_enabled", + "spanAttributeSchema": "trace_span_attribute_schema", + "trace.span.attribute.schema": "trace_span_attribute_schema", + "spanRemoveIntegrationFromService": "trace_remove_integration_service_names_enabled", + "trace.remove.integration-service-names.enabled": "trace_remove_integration_service_names_enabled", + "ddtrace_auto_used": "ddtrace_auto_used", + "ddtrace_bootstrapped": "ddtrace_bootstrapped", + "orchestrion_enabled": "orchestrion_enabled", + "orchestrion_version": "orchestrion_version", + "trace.once_logs": "trace_once_logs", + "trace.wordpress_callbacks": "trace_wordpress_callbacks", + "trace.wordpress_enhanced_integration": "trace_wordpress_enhanced_integration", + "trace.wordpress_additional_actions": "trace_wordpress_additional_actions", + "trace.sidecar_trace_sender": "trace_sidecar_trace_sender", + "trace.sampling_rules_format": "trace_sampling_rules_format", + "DD_TRACE_SAMPLING_RULES_FORMAT": "trace_sampling_rules_format", + "trace.agentless": "trace_agentless", + "dd_agent_port": "trace_agent_port", + "dd_priority_sampling": "trace_priority_sampling_enabled", + "dd_profiling_capture_pct": "profiling_capture_pct", + "dd_profiling_export_libdd_enabled": "profiling_export_libdd_enabled", + "dd_profiling_heap_enabled": "profiling_heap_enabled", + "dd_profiling_lock_enabled": "profiling_lock_enabled", + "dd_profiling_max_frames": "profiling_max_frames", + "dd_profiling_memory_enabled": "profiling_memory_enabled", + "dd_profiling_stack_enabled": "profiling_stack_enabled", + "dd_profiling_upload_interval": "profiling_upload_interval", + "dd_remote_configuration_enabled": "remote_config_enabled", + "dd_trace_agent_timeout_seconds": "trace_agent_timeout", + "dd_trace_api_version": "trace_api_version", + "dd_trace_writer_buffer_size_bytes": "trace_serialization_buffer_size", + "dd_trace_writer_interval_seconds": "trace_agent_flush_interval", + "dd_trace_writer_max_payload_size_bytes": "trace_agent_max_payload_size", + "dd_trace_writer_reuse_connections": "trace_agent_reuse_connections", + "tracing_enabled": "trace_enabled", + "ssi_injection_enabled": "ssi_injection_enabled", + "DD_INJECTION_ENABLED": "ssi_injection_enabled", + "ssi_forced_injection_enabled": "ssi_forced_injection_enabled", + "DD_INJECT_FORCE": "ssi_forced_injection_enabled", + "inject_force": "ssi_forced_injection_enabled", + "OTEL_LOGS_EXPORTER": "otel_logs_exporter", + "OTEL_LOG_LEVEL": "otel_log_level", + "OTEL_METRICS_EXPORTER": "otel_metrics_exporter", + "integration_metrics_enabled": "integration_metrics_enabled", + "OTEL_SDK_DISABLED": "otel_sdk_disabled", + "OTEL_SERVICE_NAME": "otel_service_name", + "OTEL_PROPAGATORS": "otel_propagators", + "OTEL_RESOURCE_ATTRIBUTES": "otel_resource_attributes", + "OTEL_TRACES_EXPORTER": "otel_traces_exporter", + "OTEL_TRACES_SAMPLER": "otel_traces_sampler", + "OTEL_TRACES_SAMPLER_ARG": "otel_traces_sampler_arg", + "crashtracking_enabled": "crashtracking_enabled", + "crashtracking_available": "crashtracking_available", + "crashtracking_started": "crashtracking_started", + "crashtracking_stdout_filename": "crashtracking_stdout_filename", + "crashtracking_stderr_filename": "crashtracking_stderr_filename", + "crashtracking_alt_stack": "crashtracking_alt_stack", + "crashtracking_stacktrace_resolver": "crashtracking_stacktrace_resolver", + "crashtracking_debug_url": "crashtracking_debug_url", + "debug_stack_enabled": "debug_stack_enabled", + "DD_TRACE_BAGGAGE_MAX_ITEMS": "trace_baggage_max_items", + "DD_TRACE_BAGGAGE_MAX_BYTES": "trace_baggage_max_bytes", + "appsec.apiSecurity.sampleDelay": "api_security_sample_delay", + "appsec.stackTrace.enabled": "appsec_stack_trace_enabled", + "appsec.stackTrace.maxDepth": "appsec_max_stack_trace_depth", + "appsec.stackTrace.maxStackTraces": "appsec_max_stack_traces", + "appsec.standalone.enabled": "experimental_appsec_standalone_enabled", + "baggageMaxBytes": "trace_baggage_max_bytes", + "baggageMaxItems": "trace_baggage_max_items", + "ciVisAgentlessLogSubmissionEnabled": "ci_visibility_agentless_enabled", + "ciVisibilityTestSessionName": "test_session_name", + "cloudPayloadTagging.maxDepth": "cloud_payload_tagging_max_depth", + "cloudPayloadTagging.requestsEnabled": "cloud_payload_tagging_requests_enabled", + "cloudPayloadTagging.responsesEnabled": "cloud_payload_tagging_responses_enabled", + "cloudPayloadTagging.rules.aws.eventbridge.expand": "cloud_payload_tagging_rules_aws_eventbridge_expand", + "cloudPayloadTagging.rules.aws.eventbridge.request": "cloud_payload_tagging_rules_aws_eventbridge_request", + "cloudPayloadTagging.rules.aws.eventbridge.response": "cloud_payload_tagging_rules_aws_eventbridge_response", + "cloudPayloadTagging.rules.aws.kinesis.expand": "cloud_payload_tagging_rules_aws_kinesis_expand", + "cloudPayloadTagging.rules.aws.kinesis.request": "cloud_payload_tagging_rules_aws_kinesis_request", + "cloudPayloadTagging.rules.aws.kinesis.response": "cloud_payload_tagging_rules_aws_kinesis_response", + "cloudPayloadTagging.rules.aws.s3.expand": "cloud_payload_tagging_rules_aws_s3_expand", + "cloudPayloadTagging.rules.aws.s3.request": "cloud_payload_tagging_rules_aws_s3_request", + "cloudPayloadTagging.rules.aws.s3.response": "cloud_payload_tagging_rules_aws_s3_response", + "cloudPayloadTagging.rules.aws.sns.expand": "cloud_payload_tagging_rules_aws_sns_expand", + "cloudPayloadTagging.rules.aws.sns.request": "cloud_payload_tagging_rules_aws_sns_request", + "cloudPayloadTagging.rules.aws.sns.response": "cloud_payload_tagging_rules_aws_sns_response", + "cloudPayloadTagging.rules.aws.sqs.expand": "cloud_payload_tagging_rules_aws_sqs_expand", + "cloudPayloadTagging.rules.aws.sqs.request": "cloud_payload_tagging_rules_aws_sqs_request", + "cloudPayloadTagging.rules.aws.sqs.response": "cloud_payload_tagging_rules_aws_sqs_response", + "codeOriginForSpans.enabled": "code_origin_for_spans_enabled", + "commitSHA": "commit_sha", + "crashtracking.enabled": "crashtracking_enabled", + "dynamicInstrumentationEnabled": "dynamic_instrumentation_enabled", + "flakyTestRetriesCount": "ci_visibility_flaky_retry_count", + "gitMetadataEnabled": "git_metadata_enabled", + "grpc.client.error.statuses": "trace_grpc_client_error_statuses", + "grpc.server.error.statuses": "trace_grpc_server_error_statuses", + "headerTags": "trace_header_tags", + "injectionEnabled": "ssi_injection_enabled", + "instrumentation_config_id": "instrumentation_config_id", + "isEarlyFlakeDetectionEnabled": "ci_visibility_early_flake_detection_enabled", + "isFlakyTestRetriesEnabled": "ci_visibility_flaky_retry_enabled", + "isManualApiEnabled": "ci_visibility_manual_api_enabled", + "isTestDynamicInstrumentationEnabled": "ci_visibility_test_dynamic_instrumentation_enabled", + "langchain.spanCharLimit": "langchain_span_char_limit", + "langchain.spanPromptCompletionSampleRate": "langchain_span_prompt_completion_sample_rate", + "legacyBaggageEnabled": "trace_legacy_baggage_enabled", + "llmobs.agentlessEnabled": "llmobs_agentless_enabled", + "llmobs.enabled": "llmobs_enabled", + "llmobs.mlApp": "llmobs_ml_app", + "profiling.longLivedThreshold": "profiling_long_lived_threshold", + "repositoryUrl": "repository_url", + "sampler.rules": "trace_sample_rules", + "sampler.spanSamplingRules": "span_sample_rules", + "telemetry.dependencyCollection": "instrumentation_telemetry_dependency_collection_enabled", + "telemetry.heartbeatInterval": "instrumentation_telemetry_heartbeat_interval", + "traceEnabled": "trace_enabled", + "tracePropagationStyle.otelPropagators": "trace_propagation_style_otel_propagators" +} diff --git a/packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json b/packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json new file mode 100644 index 00000000000..fc5188f2c2b --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/config_prefix_block_list.json @@ -0,0 +1,243 @@ +[ + "apiKey", + "appsec.eventTracking.enabled", + "trace.integration.", + "global_tag_runtime-id", + "tracePropagationStyle.inject.", + "DD_PROFILING_API_KEY", + "dd_profiling_apikey", + "N/A", + "DD_API_KEY", + "DD_APPLICATION_KEY", + "DD_TRACE_HttpMessageHandler_", + "DD_HttpMessageHandler_", + "DD_TRACE_HttpSocketsHandler_", + "DD_HttpSocketsHandler_", + "DD_TRACE_WinHttpHandler_", + "DD_WinHttpHandler_", + "DD_TRACE_CurlHandler_", + "DD_CurlHandler_", + "DD_TRACE_AspNetCore_", + "DD_AspNetCore_", + "DD_TRACE_AdoNet_", + "DD_AdoNet_", + "DD_TRACE_AspNet_", + "DD_AspNet_", + "DD_TRACE_AspNetMvc_", + "DD_AspNetMvc_", + "DD_TRACE_AspNetWebApi2_", + "DD_AspNetWebApi2_", + "DD_TRACE_GraphQL_", + "DD_GraphQL_", + "DD_TRACE_HotChocolate_", + "DD_HotChocolate_", + "DD_TRACE_MongoDb_", + "DD_MongoDb_", + "DD_TRACE_XUnit_", + "DD_XUnit_", + "DD_TRACE_NUnit_", + "DD_NUnit_", + "DD_TRACE_MsTestV2_", + "DD_MsTestV2_", + "DD_TRACE_Wcf_", + "DD_Wcf_", + "DD_TRACE_WebRequest_", + "DD_WebRequest_", + "DD_TRACE_ElasticsearchNet_", + "DD_ElasticsearchNet_", + "DD_TRACE_ServiceStackRedis_", + "DD_ServiceStackRedis_", + "DD_TRACE_StackExchangeRedis_", + "DD_StackExchangeRedis_", + "DD_TRACE_ServiceRemoting_", + "DD_ServiceRemoting_", + "DD_TRACE_RabbitMQ_", + "DD_RabbitMQ_", + "DD_TRACE_Msmq_", + "DD_Msmq_", + "DD_TRACE_Kafka_", + "DD_Kafka_", + "DD_TRACE_CosmosDb_", + "DD_CosmosDb_", + "DD_TRACE_AwsLambda_", + "DD_AwsLambda_", + "DD_TRACE_AwsSdk_", + "DD_AwsSdk_", + "DD_TRACE_AwsSqs_", + "DD_AwsSqs_", + "DD_TRACE_AwsSns_", + "DD_AwsSns_", + "DD_TRACE_ILogger_", + "DD_ILogger_", + "DD_TRACE_Aerospike_", + "DD_Aerospike_", + "DD_TRACE_AzureFunctions_", + "DD_AzureFunctions_", + "DD_TRACE_Couchbase_", + "DD_Couchbase_", + "DD_TRACE_MySql_", + "DD_MySql_", + "DD_TRACE_Npgsql_", + "DD_Npgsql_", + "DD_TRACE_Oracle_", + "DD_Oracle_", + "DD_TRACE_SqlClient_", + "DD_SqlClient_", + "DD_TRACE_Sqlite_", + "DD_Sqlite_", + "DD_TRACE_Serilog_", + "DD_Serilog_", + "DD_TRACE_Log4Net_", + "DD_Log4Net_", + "DD_TRACE_NLog_", + "DD_NLog_", + "DD_TRACE_TraceAnnotations_", + "DD_TraceAnnotations_", + "DD_TRACE_Grpc_", + "DD_Grpc_", + "DD_TRACE_Process_", + "DD_Process_", + "DD_TRACE_HashAlgorithm_", + "DD_HashAlgorithm_", + "DD_TRACE_SymmetricAlgorithm_", + "DD_SymmetricAlgorithm_", + "DD_TRACE_OpenTelemetry_", + "DD_OpenTelemetry_", + "DD_TRACE_PathTraversal_", + "DD_PathTraversal_", + "DD_TRACE_Ssrf_", + "DD_Ssrf_", + "DD_TRACE_Ldap_", + "DD_Ldap_", + "DD_TRACE_AwsKinesis_", + "DD_AwsKinesis_", + "DD_TRACE_AzureServiceBus_", + "DD_AzureServiceBus_", + "DD_TRACE_SystemRandom_", + "DD_SystemRandom_", + "DD_TRACE_AwsDynamoDb_", + "DD_AwsDynamoDb_", + "DD_TRACE_HardcodedSecret_", + "DD_HarcodedSecret_", + "DD_TRACE_IbmMq_", + "DD_IbmMq_", + "DD_TRACE_Remoting_", + "DD_Remoting_", + "trace.amqp_enabled", + "trace.amqp_analytics_enabled", + "trace.amqp_analytics_sample_rate", + "trace.cakephp_enabled", + "trace.cakephp_analytics_enabled", + "trace.cakephp_analytics_sample_rate", + "trace.codeigniter_enabled", + "trace.codeigniter_analytics_enabled", + "trace.codeigniter_analytics_sample_rate", + "trace.curl_enabled", + "trace.curl_analytics_enabled", + "trace.curl_analytics_sample_rate", + "trace.elasticsearch_enabled", + "trace.elasticsearch_analytics_enabled", + "trace.elasticsearch_analytics_sample_rate", + "trace.eloquent_enabled", + "trace.eloquent_analytics_enabled", + "trace.eloquent_analytics_sample_rate", + "trace.frankenphp_enabled", + "trace.frankenphp_analytics_enabled", + "trace.frankenphp_analytics_sample_rate", + "trace.googlespanner_enabled", + "trace.googlespanner_analytics_enabled", + "trace.googlespanner_analytics_sample_rate", + "trace.guzzle_enabled", + "trace.guzzle_analytics_enabled", + "trace.guzzle_analytics_sample_rate", + "trace.laminas_enabled", + "trace.laminas_analytics_enabled", + "trace.laminas_analytics_sample_rate", + "trace.laravel_enabled", + "trace.laravel_analytics_enabled", + "trace.laravel_analytics_sample_rate", + "trace.laravelqueue_enabled", + "trace.laravelqueue_analytics_enabled", + "trace.laravelqueue_analytics_sample_rate", + "trace.logs_enabled", + "trace.logs_analytics_enabled", + "trace.logs_analytics_sample_rate", + "trace.lumen_enabled", + "trace.lumen_analytics_enabled", + "trace.lumen_analytics_sample_rate", + "trace.memcache_enabled", + "trace.memcache_analytics_enabled", + "trace.memcache_analytics_sample_rate", + "trace.memcached_enabled", + "trace.memcached_analytics_enabled", + "trace.memcached_analytics_sample_rate", + "trace.mongo_enabled", + "trace.mongo_analytics_enabled", + "trace.mongo_analytics_sample_rate", + "trace.mongodb_enabled", + "trace.mongodb_analytics_enabled", + "trace.mongodb_analytics_sample_rate", + "trace.mysqli_enabled", + "trace.mysqli_analytics_enabled", + "trace.mysqli_analytics_sample_rate", + "trace.nette_enabled", + "trace.nette_analytics_enabled", + "trace.nette_analytics_sample_rate", + "trace.openai_enabled", + "trace.openai_analytics_enabled", + "trace.openai_analytics_sample_rate", + "trace.pcntl_enabled", + "trace.pcntl_analytics_enabled", + "trace.pcntl_analytics_sample_rate", + "trace.pdo_enabled", + "trace.pdo_analytics_enabled", + "trace.pdo_analytics_sample_rate", + "trace.phpredis_enabled", + "trace.phpredis_analytics_enabled", + "trace.phpredis_analytics_sample_rate", + "trace.predis_enabled", + "trace.predis_analytics_enabled", + "trace.predis_analytics_sample_rate", + "trace.psr18_enabled", + "trace.psr18_analytics_enabled", + "trace.psr18_analytics_sample_rate", + "trace.roadrunner_enabled", + "trace.roadrunner_analytics_enabled", + "trace.roadrunner_analytics_sample_rate", + "trace.sqlsrv_enabled", + "trace.sqlsrv_analytics_enabled", + "trace.sqlsrv_analytics_sample_rate", + "trace.slim_enabled", + "trace.slim_analytics_enabled", + "trace.slim_analytics_sample_rate", + "trace.swoole_enabled", + "trace.swoole_analytics_enabled", + "trace.swoole_analytics_sample_rate", + "trace.symfonymessenger_enabled", + "trace.symfonymessenger_analytics_enabled", + "trace.symfonymessenger_analytics_sample_rate", + "trace.symfony_enabled", + "trace.symfony_analytics_enabled", + "trace.symfony_analytics_sample_rate", + "trace.web_enabled", + "trace.web_analytics_enabled", + "trace.web_analytics_sample_rate", + "trace.wordpress_enabled", + "trace.wordpress_analytics_enabled", + "trace.wordpress_analytics_sample_rate", + "trace.yii_enabled", + "trace.yii_analytics_enabled", + "trace.yii_analytics_sample_rate", + "trace.zendframework_enabled", + "trace.zendframework_analytics_enabled", + "trace.zendframework_analytics_sample_rate", + "trace.drupal_enabled", + "trace.drupal_analytics_enabled", + "trace.drupal_analytics_sample_rate", + "trace.magento_enabled", + "trace.magento_analytics_enabled", + "trace.magento_analytics_sample_rate", + "trace.exec_enabled", + "trace.exec_analytics_enabled", + "trace.exec_analytics_sample_rate" +] diff --git a/packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json b/packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json new file mode 100644 index 00000000000..b96a6ab5d15 --- /dev/null +++ b/packages/dd-trace/test/fixtures/telemetry/nodejs_config_rules.json @@ -0,0 +1,175 @@ +{ + "normalization_rules" : + { + "HOSTNAME" : "agent_hostname", + "hostname" : "agent_hostname", + "appsec.blockedTemplateHtml" : "appsec_blocked_template_html", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML" : "appsec_blocked_template_html", + "appsec.blockedTemplateJson" : "appsec_blocked_template_json", + "DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON" : "appsec_blocked_template_json", + "security_enabled" : "appsec_enabled", + "appsec.enabled" : "appsec_enabled", + "DD_APPSEC_ENABLED" : "appsec_enabled", + "appsec.obfuscatorKeyRegex" : "appsec_obfuscation_parameter_key_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP" : "appsec_obfuscation_parameter_key_regexp", + "appsec.obfuscatorValueRegex" : "appsec_obfuscation_parameter_value_regexp", + "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP" : "appsec_obfuscation_parameter_value_regexp", + "appsec.rateLimit" : "appsec_rate_limit", + "appsec.rules" : "appsec_rules", + "DD_APPSEC_RULES" : "appsec_rules", + "appsec.customRulesProvided" : "appsec_rules_custom_provided", + "appsec.rules.metadata.rules_version" : "appsec_rules_metadata_rules_version", + "appsec.rules.version" : "appsec_rules_version", + "appsec.wafTimeout" : "appsec_waf_timeout", + "appsec.waf.timeout" : "appsec_waf_timeout", + "DD_APPSEC_WAF_TIMEOUT" : "appsec_waf_timeout", + "civisibility.enabled" : "ci_visibility_enabled", + "isCiVisibility" : "ci_visibility_enabled", + "DD_CIVISIBILITY_ENABLED" : "ci_visibility_enabled", + "clientIpHeaderDisabled" : "client_ip_header_disabled", + "dbmPropagationMode" : "dbm_propagation_mode", + "dbm_propagation_mode" : "dbm_propagation_mode", + "DD_DBM_PROPAGATION_MODE" : "dbm_propagation_mode", + "dogstatsd.hostname" : "dogstatsd_hostname", + "dogstatsd.port" : "dogstatsd_port", + "DD_DOGSTATSD_PORT" : "dogstatsd_port", + "env" : "env", + "DD_ENV" : "env", + "experimental.b3" : "experimental_b3", + "experimental.enableGetRumData" : "experimental_enable_get_rum_data", + "experimental.exporter" : "experimental_exporter", + "experimental.runtimeId" : "experimental_runtime_id", + "experimental.sampler.rateLimit" : "experimental_sampler_rate_limit", + "experimental.sampler.sampleRate" : "experimental_sampler_sample_rate", + "experimental.traceparent" : "experimental_traceparent", + "flushInterval" : "flush_interval", + "flushMinSpans" : "flush_min_spans", + "isGitUploadEnabled" : "git_upload_enabled", + "iast.deduplication.enabled" : "iast_deduplication_enabled", + "iast.deduplicationEnabled" : "iast_deduplication_enabled", + "DD_IAST_DEDUPLICATION_ENABLED" : "iast_deduplication_enabled", + "iast.enabled" : "iast_enabled", + "DD_IAST_ENABLED" : "iast_enabled", + "iast.maxConcurrentRequests" : "iast_max_concurrent_requests", + "iast.max-concurrent-requests" : "iast_max_concurrent_requests", + "DD_IAST_MAX_CONCURRENT_REQUESTS" : "iast_max_concurrent_requests", + "iast.maxContextOperations" : "iast_max_context_operations", + "iast.requestSampling" : "iast_request_sampling", + "iast.request-sampling" : "iast_request_sampling", + "telemetry.debug" : "instrumentation_telemetry_debug_enabled", + "DD_INTERNAL_TELEMETRY_DEBUG_ENABLED" : "instrumentation_telemetry_debug_enabled", + "instrumentation.telemetry.enabled" : "instrumentation_telemetry_enabled", + "telemetryEnabled" : "instrumentation_telemetry_enabled", + "telemetry.enabled" : "instrumentation_telemetry_enabled", + "DD_INSTRUMENTATION_TELEMETRY_ENABLED" : "instrumentation_telemetry_enabled", + "trace.telemetry_enabled" : "instrumentation_telemetry_enabled", + "telemetry.logCollection" : "instrumentation_telemetry_log_collection_enabled", + "telemetry.metrics" : "instrumentation_telemetry_metrics_enabled", + "DD_TELEMETRY_METRICS_ENABLED" : "instrumentation_telemetry_metrics_enabled", + "isIntelligentTestRunnerEnabled" : "intelligent_test_runner_enabled", + "logger" : "logger", + "logInjection_enabled" : "logs_injection_enabled", + "logs.injection" : "logs_injection_enabled", + "logInjection" : "logs_injection_enabled", + "DD_LOGS_INJECTION" : "logs_injection_enabled", + "lookup" : "lookup", + "plugins" : "plugins", + "profiling.enabled" : "profiling_enabled", + "DD_PROFILING_ENABLED" : "profiling_enabled", + "profiling.exporters" : "profiling_exporters", + "profiling.sourceMap" : "profiling_source_map_enabled", + "remote_config.enabled" : "remote_config_enabled", + "remoteConfig.enabled" : "remote_config_enabled", + "remoteConfig.pollInterval" : "remote_config_poll_interval", + "DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS" : "remote_config_poll_interval", + "DD_INTERNAL_RCM_POLL_INTERVAL" : "remote_config_poll_interval", + "runtimemetrics_enabled" : "runtime_metrics_enabled", + "runtime.metrics.enabled" : "runtime_metrics_enabled", + "runtimeMetrics" : "runtime_metrics_enabled", + "DD_RUNTIME_METRICS_ENABLED" : "runtime_metrics_enabled", + "scope" : "scope", + "service" : "service", + "DD_SERVICE" : "service", + "DD_SERVICE_NAME" : "service", + "site" : "site", + "DD_SITE" : "site", + "stats.enabled" : "stats_enabled", + "traceId128BitGenerationEnabled" : "trace_128_bits_id_enabled", + "trace.128_bit_traceid_generation_enabled" : "trace_128_bits_id_enabled", + "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED" : "trace_128_bits_id_enabled", + "traceId128BitLoggingEnabled" : "trace_128_bits_id_logging_enabled", + "DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED" : "trace_128_bits_id_logging_enabled", + "trace.128_bit_traceid_logging_enabled" : "trace_128_bits_id_logging_enabled", + "trace.agent.port" : "trace_agent_port", + "port" : "trace_agent_port", + "DD_TRACE_AGENT_PORT" : "trace_agent_port", + "DATADOG_TRACE_AGENT_PORT" : "trace_agent_port", + "DD_APM_RECEIVER_PORT" : "trace_agent_port", + "trace.agent_port" : "trace_agent_port", + "protocolVersion" : "trace_agent_protocol_version", + "agent_url" : "trace_agent_url", + "url" : "trace_agent_url", + "DD_TRACE_AGENT_URL" : "trace_agent_url", + "trace.agent_url" : "trace_agent_url", + "trace.client-ip.enabled" : "trace_client_ip_enabled", + "clientIpEnabled" : "trace_client_ip_enabled", + "DD_TRACE_CLIENT_IP_ENABLED" : "trace_client_ip_enabled", + "trace.client_ip_enabled" : "trace_client_ip_enabled", + "clientIpHeader" : "trace_client_ip_header", + "DD_TRACE_CLIENT_IP_HEADER" : "trace_client_ip_header", + "debug" : "trace_debug_enabled", + "dd.trace.debug" : "trace_debug_enabled", + "DD_TRACE_DEBUG" : "trace_debug_enabled", + "trace.debug" : "trace_debug_enabled", + "enabled" : "trace_enabled", + "trace.enabled" : "trace_enabled", + "tracing" : "trace_enabled", + "DD_TRACE_ENABLED" : "trace_enabled", + "tagsHeaderMaxLength" : "trace_header_tags_max_length", + "logLevel" : "trace_log_level", + "querystringObfuscation" : "trace_obfuscation_query_string_regexp", + "queryStringObfuscation" : "trace_obfuscation_query_string_regexp", + "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP" : "trace_obfuscation_query_string_regexp", + "trace.obfuscation_query_string_regexp" : "trace_obfuscation_query_string_regexp", + "DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED" : "trace_peer_service_defaults_enabled", + "trace.peer_service_defaults_enabled" : "trace_peer_service_defaults_enabled", + "spanComputePeerService" : "trace_peer_service_defaults_enabled", + "trace.peer.service.defaults.enabled" : "trace_peer_service_defaults_enabled", + "DD_TRACE_PEER_SERVICE_MAPPING" : "trace_peer_service_mapping", + "peerServiceMapping" : "trace_peer_service_mapping", + "trace.peer.service.mapping" : "trace_peer_service_mapping", + "trace.peer_service_mapping" : "trace_peer_service_mapping", + "sampler.rateLimit" : "trace_rate_limit", + "trace.rate.limit" : "trace_rate_limit", + "DD_TRACE_RATE_LIMIT" : "trace_rate_limit", + "DD_MAX_TRACES_PER_SECOND" : "trace_rate_limit", + "trace.rate_limit" : "trace_rate_limit", + "DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED" : "trace_remove_integration_service_names_enabled", + "trace.remove_integration_service_names_enabled" : "trace_remove_integration_service_names_enabled", + "spanRemoveIntegrationFromService" : "trace_remove_integration_service_names_enabled", + "trace.remove.integration-service-names.enabled" : "trace_remove_integration_service_names_enabled", + "reportHostname" : "trace_report_hostname", + "trace.report-hostname" : "trace_report_hostname", + "trace.report_hostname" : "trace_report_hostname", + "sample_rate" : "trace_sample_rate", + "trace.sample.rate" : "trace_sample_rate", + "dd_trace_sample_rate" : "trace_sample_rate", + "sampler.sampleRate" : "trace_sample_rate", + "sampleRate" : "trace_sample_rate", + "DD_TRACE_SAMPLE_RATE" : "trace_sample_rate", + "trace.sample_rate" : "trace_sample_rate", + "spanattributeschema" : "trace_span_attribute_schema", + "DD_TRACE_SPAN_ATTRIBUTE_SCHEMA" : "trace_span_attribute_schema", + "spanAttributeSchema" : "trace_span_attribute_schema", + "trace.span.attribute.schema" : "trace_span_attribute_schema", + "startupLogs" : "trace_startup_logs_enabled", + "DD_TRACE_STARTUP_LOGS" : "trace_startup_logs_enabled", + "global_tag_version" : "version" + }, + "prefix_block_list" : [ + ], + "redaction_list" :[ + ], + "reduce_rules" : { + } +} From 844d62377fc5e20b55bca916c72f1e3c6acc9690 Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Mon, 2 Dec 2024 17:27:50 -0500 Subject: [PATCH 13/61] fix mysql2 3.11.5 support (#4962) --- .../datadog-instrumentations/src/mysql2.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/datadog-instrumentations/src/mysql2.js b/packages/datadog-instrumentations/src/mysql2.js index 096eec0e80e..bd5c48daf56 100644 --- a/packages/datadog-instrumentations/src/mysql2.js +++ b/packages/datadog-instrumentations/src/mysql2.js @@ -8,7 +8,7 @@ const { const shimmer = require('../../datadog-shimmer') const semver = require('semver') -addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['>=1'] }, (Connection, version) => { +function wrapConnection (Connection, version) { const startCh = channel('apm:mysql2:query:start') const finishCh = channel('apm:mysql2:query:finish') const errorCh = channel('apm:mysql2:query:error') @@ -151,9 +151,8 @@ addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['>=1'] }, (Conne } }, cmd)) } -}) - -addHook({ name: 'mysql2', file: 'lib/pool.js', versions: ['>=1'] }, (Pool, version) => { +} +function wrapPool (Pool, version) { const startOuterQueryCh = channel('datadog:mysql2:outerquery:start') const shouldEmitEndAfterQueryAbort = semver.intersects(version, '>=1.3.3') @@ -221,10 +220,9 @@ addHook({ name: 'mysql2', file: 'lib/pool.js', versions: ['>=1'] }, (Pool, versi }) return Pool -}) +} -// PoolNamespace.prototype.query does not exist in mysql2<2.3.0 -addHook({ name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['>=2.3.0'] }, PoolCluster => { +function wrapPoolCluster (PoolCluster) { const startOuterQueryCh = channel('datadog:mysql2:outerquery:start') const wrappedPoolNamespaces = new WeakSet() @@ -297,4 +295,11 @@ addHook({ name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['>=2.3.0'] }, }) return PoolCluster -}) +} + +addHook({ name: 'mysql2', file: 'lib/base/connection.js', versions: ['>=3.11.5'] }, wrapConnection) +addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['1 - 3.11.4'] }, wrapConnection) +addHook({ name: 'mysql2', file: 'lib/pool.js', versions: ['1 - 3.11.4'] }, wrapPool) + +// PoolNamespace.prototype.query does not exist in mysql2<2.3.0 +addHook({ name: 'mysql2', file: 'lib/pool_cluster.js', versions: ['2.3.0 - 3.11.4'] }, wrapPoolCluster) From 3296eb8e18908846c6cf53e1ac23f98ee808fa9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Tue, 3 Dec 2024 10:26:01 +0100 Subject: [PATCH 14/61] [test optimization] Add dynamic instrumentation support for cucumber (#4956) --- integration-tests/ci-visibility-intake.js | 7 +- .../features-di/support/steps.js | 24 +++ .../ci-visibility/features-di/support/sum.js | 10 + .../features-di/test-hit-breakpoint.feature | 6 + .../test-not-hit-breakpoint.feature | 6 + integration-tests/cucumber/cucumber.spec.js | 194 +++++++++++++++++- .../datadog-instrumentations/src/cucumber.js | 32 ++- packages/datadog-plugin-cucumber/src/index.js | 43 +++- 8 files changed, 311 insertions(+), 11 deletions(-) create mode 100644 integration-tests/ci-visibility/features-di/support/steps.js create mode 100644 integration-tests/ci-visibility/features-di/support/sum.js create mode 100644 integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature create mode 100644 integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature diff --git a/integration-tests/ci-visibility-intake.js b/integration-tests/ci-visibility-intake.js index c133a7a31fe..f08f1a24ecd 100644 --- a/integration-tests/ci-visibility-intake.js +++ b/integration-tests/ci-visibility-intake.js @@ -25,7 +25,7 @@ const DEFAULT_SUITES_TO_SKIP = [] const DEFAULT_GIT_UPLOAD_STATUS = 200 const DEFAULT_KNOWN_TESTS_UPLOAD_STATUS = 200 const DEFAULT_INFO_RESPONSE = { - endpoints: ['/evp_proxy/v2'] + endpoints: ['/evp_proxy/v2', '/debugger/v1/input'] } const DEFAULT_CORRELATION_ID = '1234' const DEFAULT_KNOWN_TESTS = ['test-suite1.js.test-name1', 'test-suite2.js.test-name2'] @@ -208,7 +208,10 @@ class FakeCiVisIntake extends FakeAgent { }) }) - app.post('/api/v2/logs', express.json(), (req, res) => { + app.post([ + '/api/v2/logs', + '/debugger/v1/input' + ], express.json(), (req, res) => { res.status(200).send('OK') this.emit('message', { headers: req.headers, diff --git a/integration-tests/ci-visibility/features-di/support/steps.js b/integration-tests/ci-visibility/features-di/support/steps.js new file mode 100644 index 00000000000..00880f83467 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/support/steps.js @@ -0,0 +1,24 @@ +const assert = require('assert') +const { When, Then } = require('@cucumber/cucumber') +const sum = require('./sum') + +let count = 0 + +When('the greeter says hello', function () { + this.whatIHeard = 'hello' +}) + +Then('I should have heard {string}', function (expectedResponse) { + sum(11, 3) + assert.equal(this.whatIHeard, expectedResponse) +}) + +Then('I should have flakily heard {string}', function (expectedResponse) { + const shouldFail = count++ < 1 + if (shouldFail) { + sum(11, 3) + } else { + sum(1, 3) // does not hit the breakpoint the second time + } + assert.equal(this.whatIHeard, expectedResponse) +}) diff --git a/integration-tests/ci-visibility/features-di/support/sum.js b/integration-tests/ci-visibility/features-di/support/sum.js new file mode 100644 index 00000000000..cb1d7adb951 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/support/sum.js @@ -0,0 +1,10 @@ +function funSum (a, b) { + const localVariable = 2 + if (a > 10) { + throw new Error('the number is too big') + } + + return a + b + localVariable +} + +module.exports = funSum diff --git a/integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature b/integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature new file mode 100644 index 00000000000..06ef560af61 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/test-hit-breakpoint.feature @@ -0,0 +1,6 @@ + +Feature: Greeting + + Scenario: Say hello + When the greeter says hello + Then I should have heard "hello" diff --git a/integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature b/integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature new file mode 100644 index 00000000000..ca5562b55c0 --- /dev/null +++ b/integration-tests/ci-visibility/features-di/test-not-hit-breakpoint.feature @@ -0,0 +1,6 @@ + +Feature: Greeting + + Scenario: Say hello + When the greeter says hello + Then I should have flakily heard "hello" diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index d7fd132caf7..8f21b3a688f 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -37,7 +37,11 @@ const { TEST_SUITE, TEST_CODE_OWNERS, TEST_SESSION_NAME, - TEST_LEVEL_EVENT_TYPES + TEST_LEVEL_EVENT_TYPES, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_LINE } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -86,10 +90,11 @@ versions.forEach(version => { reportMethods.forEach((reportMethod) => { context(`reporting via ${reportMethod}`, () => { - let envVars, isAgentless + let envVars, isAgentless, logsEndpoint beforeEach(() => { isAgentless = reportMethod === 'agentless' envVars = isAgentless ? getCiVisAgentlessConfig(receiver.port) : getCiVisEvpProxyConfig(receiver.port) + logsEndpoint = isAgentless ? '/api/v2/logs' : '/debugger/v1/input' }) const runModes = ['serial'] @@ -1536,6 +1541,191 @@ versions.forEach(version => { }) }) }) + // Dynamic instrumentation only supported from >=8.0.0 + context('dynamic instrumentation', () => { + it('does not activate if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === logsEndpoint, (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', + { + cwd, + env: envVars, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + early_flake_detection: { + enabled: false + }, + flaky_test_retries_enabled: false + }) + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), payloads => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/features-di/support/sum.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url === logsEndpoint, (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '3' + }, + localVariable: { + type: 'number', + value: '2' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-hit-breakpoint.feature --retry 1', + { + cwd, + env: { + ...envVars, + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + early_flake_detection: { + enabled: false + }, + flaky_test_retries_enabled: false + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/features-di/support/sum.js' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/cucumber-js ci-visibility/features-di/test-not-hit-breakpoint.feature --retry 1', + { + cwd, + env: { + ...envVars, + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: 'true' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', (exitCode) => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(exitCode, 0) + done() + }).catch(done) + }) + }) + }) } }) }) diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 0f84d717381..7b9a2db5a02 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -126,6 +126,20 @@ function getTestStatusFromRetries (testStatuses) { return 'pass' } +function getErrorFromCucumberResult (cucumberResult) { + if (!cucumberResult.message) { + return + } + + const [message] = cucumberResult.message.split('\n') + const error = new Error(message) + if (cucumberResult.exception) { + error.type = cucumberResult.exception.type + } + error.stack = cucumberResult.message + return error +} + function getChannelPromise (channelToPublishTo) { return new Promise(resolve => { sessionAsyncResource.runInAsyncScope(() => { @@ -230,9 +244,19 @@ function wrapRun (pl, isLatestVersion) { if (testCase?.testCaseFinished) { const { testCaseFinished: { willBeRetried } } = testCase if (willBeRetried) { // test case failed and will be retried + let error + try { + const cucumberResult = this.getWorstStepResult() + error = getErrorFromCucumberResult(cucumberResult) + } catch (e) { + // ignore error + } + const failedAttemptAsyncResource = numAttemptToAsyncResource.get(numAttempt) + const isRetry = numAttempt++ > 0 failedAttemptAsyncResource.runInAsyncScope(() => { - testRetryCh.publish(numAttempt++ > 0) // the current span will be finished and a new one will be created + // the current span will be finished and a new one will be created + testRetryCh.publish({ isRetry, error }) }) const newAsyncResource = new AsyncResource('bound-anonymous-fn') @@ -251,7 +275,7 @@ function wrapRun (pl, isLatestVersion) { }) promise.finally(() => { const result = this.getWorstStepResult() - const { status, skipReason, errorMessage } = isLatestVersion + const { status, skipReason } = isLatestVersion ? getStatusFromResultLatest(result) : getStatusFromResult(result) @@ -270,8 +294,10 @@ function wrapRun (pl, isLatestVersion) { } const attemptAsyncResource = numAttemptToAsyncResource.get(numAttempt) + const error = getErrorFromCucumberResult(result) + attemptAsyncResource.runInAsyncScope(() => { - testFinishCh.publish({ status, skipReason, errorMessage, isNew, isEfdRetry, isFlakyRetry: numAttempt > 0 }) + testFinishCh.publish({ status, skipReason, error, isNew, isEfdRetry, isFlakyRetry: numAttempt > 0 }) }) }) return promise diff --git a/packages/datadog-plugin-cucumber/src/index.js b/packages/datadog-plugin-cucumber/src/index.js index d24f97c33e6..e674131d639 100644 --- a/packages/datadog-plugin-cucumber/src/index.js +++ b/packages/datadog-plugin-cucumber/src/index.js @@ -26,7 +26,12 @@ const { TEST_MODULE, TEST_MODULE_ID, TEST_SUITE, - CUCUMBER_IS_PARALLEL + CUCUMBER_IS_PARALLEL, + TEST_NAME, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } = require('../../dd-trace/src/plugins/util/test') const { RESOURCE_NAME } = require('../../../ext/tags') const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants') @@ -46,6 +51,7 @@ const { const id = require('../../dd-trace/src/id') const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID +const debuggerParameterPerTest = new Map() function getTestSuiteTags (testSuiteSpan) { const suiteTags = { @@ -220,14 +226,40 @@ class CucumberPlugin extends CiPlugin { const testSpan = this.startTestSpan(testName, testSuite, extraTags) this.enter(testSpan, store) + + const debuggerParameters = debuggerParameterPerTest.get(testName) + + if (debuggerParameters) { + const spanContext = testSpan.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + testSpan.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + testSpan.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + testSpan.setTag(DI_DEBUG_ERROR_FILE, file) + testSpan.setTag(DI_DEBUG_ERROR_LINE, line) + } }) - this.addSub('ci:cucumber:test:retry', (isFlakyRetry) => { + this.addSub('ci:cucumber:test:retry', ({ isRetry, error }) => { const store = storage.getStore() const span = store.span - if (isFlakyRetry) { + if (isRetry) { span.setTag(TEST_IS_RETRY, 'true') } + span.setTag('error', error) + if (this.di && error) { + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(error) + debuggerParameterPerTest.set(testName, debuggerParameters) + } span.setTag(TEST_STATUS, 'fail') span.finish() finishAllTraceSpans(span) @@ -281,6 +313,7 @@ class CucumberPlugin extends CiPlugin { isStep, status, skipReason, + error, errorMessage, isNew, isEfdRetry, @@ -302,7 +335,9 @@ class CucumberPlugin extends CiPlugin { span.setTag(TEST_SKIP_REASON, skipReason) } - if (errorMessage) { + if (error) { + span.setTag('error', error) + } else if (errorMessage) { // we can't get a full error in cucumber steps span.setTag(ERROR_MESSAGE, errorMessage) } From b771888058d4482b8fc5d817a3d5ce4d3c9cc96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Tue, 3 Dec 2024 11:06:33 +0100 Subject: [PATCH 15/61] [test optimization] Add Dynamic Instrumentation support for Vitest (#4959) --- .../ci-visibility/vitest-tests/bad-sum.mjs | 7 + .../vitest-tests/breakpoint-not-hit.mjs | 18 ++ .../vitest-tests/dynamic-instrumentation.mjs | 11 + integration-tests/vitest/vitest.spec.js | 205 +++++++++++++++++- .../datadog-instrumentations/src/vitest.js | 7 +- packages/datadog-plugin-vitest/src/index.js | 36 ++- .../dynamic-instrumentation/worker/index.js | 14 +- 7 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 integration-tests/ci-visibility/vitest-tests/bad-sum.mjs create mode 100644 integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs create mode 100644 integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs diff --git a/integration-tests/ci-visibility/vitest-tests/bad-sum.mjs b/integration-tests/ci-visibility/vitest-tests/bad-sum.mjs new file mode 100644 index 00000000000..809a131c8d3 --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/bad-sum.mjs @@ -0,0 +1,7 @@ +export function sum (a, b) { + const localVar = 10 + if (a > 10) { + throw new Error('a is too large') + } + return a + b + localVar - localVar +} diff --git a/integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs b/integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs new file mode 100644 index 00000000000..33c9bca09c5 --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/breakpoint-not-hit.mjs @@ -0,0 +1,18 @@ +import { describe, test, expect } from 'vitest' +import { sum } from './bad-sum' + +let numAttempt = 0 + +describe('dynamic instrumentation', () => { + test('can sum', () => { + const shouldFail = numAttempt++ === 0 + if (shouldFail) { + expect(sum(11, 2)).to.equal(13) + } else { + expect(sum(1, 2)).to.equal(3) + } + }) + test('is not retried', () => { + expect(sum(1, 2)).to.equal(3) + }) +}) diff --git a/integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs b/integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs new file mode 100644 index 00000000000..1e2bb73352d --- /dev/null +++ b/integration-tests/ci-visibility/vitest-tests/dynamic-instrumentation.mjs @@ -0,0 +1,11 @@ +import { describe, test, expect } from 'vitest' +import { sum } from './bad-sum' + +describe('dynamic instrumentation', () => { + test('can sum', () => { + expect(sum(11, 2)).to.equal(13) + }) + test('is not retried', () => { + expect(sum(1, 2)).to.equal(3) + }) +}) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index de38feee9da..0489db04b44 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -24,7 +24,11 @@ const { TEST_NAME, TEST_EARLY_FLAKE_ENABLED, TEST_EARLY_FLAKE_ABORT_REASON, - TEST_SUITE + TEST_SUITE, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE, + DI_DEBUG_ERROR_SNAPSHOT_ID } = require('../../packages/dd-trace/src/plugins/util/test') const { DD_HOST_CPU_COUNT } = require('../../packages/dd-trace/src/plugins/util/env') @@ -896,5 +900,204 @@ versions.forEach((version) => { }) }) }) + + // dynamic instrumentation only supported from >=2.0.0 + if (version === 'latest') { + context('dynamic instrumentation', () => { + it('does not activate it if DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED is not set', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false + }) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.notProperty(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_FILE) + assert.notProperty(retriedTest.metrics, DI_DEBUG_ERROR_LINE) + assert.notProperty(retriedTest.meta, DI_DEBUG_ERROR_SNAPSHOT_ID) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + + it('runs retries with dynamic instrumentation', (done) => { + receiver.setSettings({ + itr_enabled: false, + code_coverage: false, + tests_skipping: false, + flaky_test_retries_enabled: false, + early_flake_detection: { + enabled: false + } + // di_enabled: true // TODO + }) + + let snapshotIdByTest, snapshotIdByLog + let spanIdByTest, spanIdByLog, traceIdByTest, traceIdByLog + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/vitest-tests/bad-sum.mjs' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + + snapshotIdByTest = retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID] + spanIdByTest = retriedTest.span_id.toString() + traceIdByTest = retriedTest.trace_id.toString() + + const notRetriedTest = tests.find(test => test.meta[TEST_NAME].includes('is not retried')) + + assert.notProperty(notRetriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + const [{ logMessage: [diLog] }] = payloads + assert.deepInclude(diLog, { + ddsource: 'dd_debugger', + level: 'error' + }) + assert.equal(diLog.debugger.snapshot.language, 'javascript') + assert.deepInclude(diLog.debugger.snapshot.captures.lines['4'].locals, { + a: { + type: 'number', + value: '11' + }, + b: { + type: 'number', + value: '2' + }, + localVar: { + type: 'number', + value: '10' + } + }) + spanIdByLog = diLog.dd.span_id + traceIdByLog = diLog.dd.trace_id + snapshotIdByLog = diLog.debugger.snapshot.id + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/dynamic-instrumentation*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + assert.equal(snapshotIdByTest, snapshotIdByLog) + assert.equal(spanIdByTest, spanIdByLog) + assert.equal(traceIdByTest, traceIdByLog) + done() + }).catch(done) + }) + }) + + it('does not crash if the retry does not hit the breakpoint', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const tests = events.filter(event => event.type === 'test').map(event => event.content) + const retriedTests = tests.filter(test => test.meta[TEST_IS_RETRY] === 'true') + + assert.equal(retriedTests.length, 1) + const [retriedTest] = retriedTests + + assert.propertyVal(retriedTest.meta, DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + assert.propertyVal( + retriedTest.meta, + DI_DEBUG_ERROR_FILE, + 'ci-visibility/vitest-tests/bad-sum.mjs' + ) + assert.equal(retriedTest.metrics[DI_DEBUG_ERROR_LINE], 4) + assert.exists(retriedTest.meta[DI_DEBUG_ERROR_SNAPSHOT_ID]) + }) + + const logsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/logs'), (payloads) => { + if (payloads.length > 0) { + throw new Error('Unexpected logs') + } + }, 5000) + + childProcess = exec( + './node_modules/.bin/vitest run --retry=1', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + TEST_DIR: 'ci-visibility/vitest-tests/breakpoint-not-hit*', + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', + DD_TEST_DYNAMIC_INSTRUMENTATION_ENABLED: '1' + }, + stdio: 'pipe' + } + ) + + childProcess.on('exit', () => { + Promise.all([eventsPromise, logsPromise]).then(() => { + done() + }).catch(done) + }) + }) + }) + } }) }) diff --git a/packages/datadog-instrumentations/src/vitest.js b/packages/datadog-instrumentations/src/vitest.js index f0117e0e8c0..6e2d1d6e048 100644 --- a/packages/datadog-instrumentations/src/vitest.js +++ b/packages/datadog-instrumentations/src/vitest.js @@ -316,12 +316,17 @@ addHook({ // We finish the previous test here because we know it has failed already if (numAttempt > 0) { + const probe = {} const asyncResource = taskToAsync.get(task) const testError = task.result?.errors?.[0] if (asyncResource) { asyncResource.runInAsyncScope(() => { - testErrorCh.publish({ error: testError }) + testErrorCh.publish({ error: testError, willBeRetried: true, probe }) }) + // We wait for the probe to be set + if (probe.setProbePromise) { + await probe.setProbePromise + } } } diff --git a/packages/datadog-plugin-vitest/src/index.js b/packages/datadog-plugin-vitest/src/index.js index 34617bdb1ac..d0a2984ac74 100644 --- a/packages/datadog-plugin-vitest/src/index.js +++ b/packages/datadog-plugin-vitest/src/index.js @@ -17,7 +17,12 @@ const { TEST_SOURCE_START, TEST_IS_NEW, TEST_EARLY_FLAKE_ENABLED, - TEST_EARLY_FLAKE_ABORT_REASON + TEST_EARLY_FLAKE_ABORT_REASON, + TEST_NAME, + DI_ERROR_DEBUG_INFO_CAPTURED, + DI_DEBUG_ERROR_SNAPSHOT_ID, + DI_DEBUG_ERROR_FILE, + DI_DEBUG_ERROR_LINE } = require('../../dd-trace/src/plugins/util/test') const { COMPONENT } = require('../../dd-trace/src/constants') const { @@ -31,6 +36,8 @@ const { // This is because there's some loss of resolution. const MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION = 5 +const debuggerParameterPerTest = new Map() + class VitestPlugin extends CiPlugin { static get id () { return 'vitest' @@ -81,6 +88,26 @@ class VitestPlugin extends CiPlugin { extraTags ) + const debuggerParameters = debuggerParameterPerTest.get(testName) + + if (debuggerParameters) { + const spanContext = span.context() + + // TODO: handle race conditions with this.retriedTestIds + this.retriedTestIds = { + spanId: spanContext.toSpanId(), + traceId: spanContext.toTraceId() + } + const { snapshotId, file, line } = debuggerParameters + + // TODO: should these be added on test:end if and only if the probe is hit? + // Sync issues: `hitProbePromise` might be resolved after the test ends + span.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true') + span.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId) + span.setTag(DI_DEBUG_ERROR_FILE, file) + span.setTag(DI_DEBUG_ERROR_LINE, line) + } + this.enter(span, store) }) @@ -110,11 +137,16 @@ class VitestPlugin extends CiPlugin { } }) - this.addSub('ci:vitest:test:error', ({ duration, error }) => { + this.addSub('ci:vitest:test:error', ({ duration, error, willBeRetried, probe }) => { const store = storage.getStore() const span = store?.span if (span) { + if (willBeRetried && this.di) { + const testName = span.context()._tags[TEST_NAME] + const debuggerParameters = this.addDiProbe(error, probe) + debuggerParameterPerTest.set(testName, debuggerParameters) + } this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', { hasCodeowners: !!span.context()._tags[TEST_CODE_OWNERS] }) diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js index 0ba8d01f53c..952ba1a7cf7 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js @@ -75,18 +75,20 @@ async function addBreakpoint (snapshotId, probe) { log.debug(`Adding breakpoint at ${path}:${line}`) - let generatedPosition = { line } - let hasSourceMap = false + let lineNumber = line if (sourceMapURL && sourceMapURL.startsWith('data:')) { - hasSourceMap = true - generatedPosition = await processScriptWithInlineSourceMap({ file, line, sourceMapURL }) + try { + lineNumber = await processScriptWithInlineSourceMap({ file, line, sourceMapURL }) + } catch (err) { + log.error(err) + } } const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { scriptId, - lineNumber: hasSourceMap ? generatedPosition.line : generatedPosition.line - 1 + lineNumber: lineNumber - 1 } }) @@ -120,5 +122,5 @@ async function processScriptWithInlineSourceMap (params) { consumer.destroy() - return generatedPosition + return generatedPosition.line } From b1cbf8f8220229d4472331c1c8d6328dd4951e56 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 3 Dec 2024 14:18:12 +0100 Subject: [PATCH 16/61] [DI] Adhere to diagnostics JSON schema (version -> probeVersion) (#4964) --- integration-tests/debugger/basic.spec.js | 18 +++++++++--------- .../src/debugger/devtools_client/status.js | 4 ++-- .../debugger/devtools_client/status.spec.js | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 3330a6c32d3..8782bc90449 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -24,15 +24,15 @@ describe('Dynamic Instrumentation', function () { const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'EMITTING' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'EMITTING' } } }] t.agent.on('remote-config-ack-update', (id, version, state, error) => { @@ -75,19 +75,19 @@ describe('Dynamic Instrumentation', function () { const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 1, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 1, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 1, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 1, status: 'INSTALLED' } } }] const triggers = [ () => { @@ -128,11 +128,11 @@ describe('Dynamic Instrumentation', function () { const expectedPayloads = [{ ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'RECEIVED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'RECEIVED' } } }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: { probeId, version: 0, status: 'INSTALLED' } } + debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }] t.agent.on('remote-config-ack-update', (id, version, state, error) => { diff --git a/packages/dd-trace/src/debugger/devtools_client/status.js b/packages/dd-trace/src/debugger/devtools_client/status.js index e4ba10d8c55..a18480d4037 100644 --- a/packages/dd-trace/src/debugger/devtools_client/status.js +++ b/packages/dd-trace/src/debugger/devtools_client/status.js @@ -91,12 +91,12 @@ function send (payload) { }) } -function statusPayload (probeId, version, status) { +function statusPayload (probeId, probeVersion, status) { return { ddsource, service, debugger: { - diagnostics: { probeId, runtimeId, version, status } + diagnostics: { probeId, runtimeId, probeVersion, status } } } } diff --git a/packages/dd-trace/test/debugger/devtools_client/status.spec.js b/packages/dd-trace/test/debugger/devtools_client/status.spec.js index 41433f453c5..365d86d6e96 100644 --- a/packages/dd-trace/test/debugger/devtools_client/status.spec.js +++ b/packages/dd-trace/test/debugger/devtools_client/status.spec.js @@ -79,7 +79,7 @@ describe('diagnostic message http request caching', function () { function assertRequestData (request, { probeId, version, status, exception }) { const payload = getFormPayload(request) - const diagnostics = { probeId, runtimeId, version, status } + const diagnostics = { probeId, runtimeId, probeVersion: version, status } // Error requests will also contain an `exception` property if (exception) diagnostics.exception = exception From 048736ef14c4aa8d626927a6942a58284b3792b3 Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Tue, 3 Dec 2024 16:16:46 +0100 Subject: [PATCH 17/61] Use sampling on timeline events (#4861) --- integration-tests/profiler/profiler.spec.js | 9 +-- packages/dd-trace/src/profiling/config.js | 3 + .../profilers/event_plugins/event.js | 20 ++++--- .../src/profiling/profilers/events.js | 55 ++++++++++++++++--- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/integration-tests/profiler/profiler.spec.js b/integration-tests/profiler/profiler.spec.js index 172c186f1eb..5a5a68be392 100644 --- a/integration-tests/profiler/profiler.spec.js +++ b/integration-tests/profiler/profiler.spec.js @@ -105,10 +105,9 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args const proc = fork(path.join(cwd, scriptFilePath), args, { cwd, env: { - DD_PROFILING_PROFILERS: 'wall', DD_PROFILING_EXPORTERS: 'file', DD_PROFILING_ENABLED: 1, - DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED: 1 + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED: 0 // capture all events } }) @@ -205,12 +204,8 @@ describe('profiler', () => { const proc = fork(path.join(cwd, 'profiler/codehotspots.js'), { cwd, env: { - DD_PROFILING_PROFILERS: 'wall', DD_PROFILING_EXPORTERS: 'file', - DD_PROFILING_ENABLED: 1, - DD_PROFILING_CODEHOTSPOTS_ENABLED: 1, - DD_PROFILING_ENDPOINT_COLLECTION_ENABLED: 1, - DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED: 1 + DD_PROFILING_ENABLED: 1 } }) diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 3c360d65f7a..4e7863dce3a 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -21,6 +21,7 @@ class Config { const { DD_AGENT_HOST, DD_ENV, + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, // used for testing DD_PROFILING_CODEHOTSPOTS_ENABLED, DD_PROFILING_CPU_ENABLED, DD_PROFILING_DEBUG_SOURCE_MAPS, @@ -175,6 +176,8 @@ class Config { DD_PROFILING_EXPERIMENTAL_TIMELINE_ENABLED, samplingContextsAvailable)) logExperimentalVarDeprecation('TIMELINE_ENABLED') checkOptionWithSamplingContextAllowed(this.timelineEnabled, 'Timeline view') + this.timelineSamplingEnabled = isTrue(coalesce(options.timelineSamplingEnabled, + DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED, true)) this.codeHotspotsEnabled = isTrue(coalesce(options.codeHotspotsEnabled, DD_PROFILING_CODEHOTSPOTS_ENABLED, diff --git a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js index 73d3214e231..5d81e1d8a3f 100644 --- a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js @@ -5,9 +5,10 @@ const { performance } = require('perf_hooks') // We are leveraging the TracingPlugin class for its functionality to bind // start/error/finish methods to the appropriate diagnostic channels. class EventPlugin extends TracingPlugin { - constructor (eventHandler) { + constructor (eventHandler, eventFilter) { super() this.eventHandler = eventHandler + this.eventFilter = eventFilter this.store = storage('profiling') this.entryType = this.constructor.entryType } @@ -30,17 +31,20 @@ class EventPlugin extends TracingPlugin { } const duration = performance.now() - startTime - const context = this.activeSpan?.context() - const _ddSpanId = context?.toSpanId() - const _ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || _ddSpanId - const event = { entryType: this.entryType, startTime, - duration, - _ddSpanId, - _ddRootSpanId + duration + } + + if (!this.eventFilter(event)) { + return } + + const context = this.activeSpan?.context() + event._ddSpanId = context?.toSpanId() + event._ddRootSpanId = context?._trace.started[0]?.context().toSpanId() || event._ddSpanId + this.eventHandler(this.extendEvent(event, startEvent)) } } diff --git a/packages/dd-trace/src/profiling/profilers/events.js b/packages/dd-trace/src/profiling/profilers/events.js index f8f43b06a9a..2200eaadd2e 100644 --- a/packages/dd-trace/src/profiling/profilers/events.js +++ b/packages/dd-trace/src/profiling/profilers/events.js @@ -254,10 +254,10 @@ class NodeApiEventSource { } class DatadogInstrumentationEventSource { - constructor (eventHandler) { + constructor (eventHandler, eventFilter) { this.plugins = ['dns_lookup', 'dns_lookupservice', 'dns_resolve', 'dns_reverse', 'net'].map(m => { const Plugin = require(`./event_plugins/${m}`) - return new Plugin(eventHandler) + return new Plugin(eventHandler, eventFilter) }) this.started = false @@ -292,29 +292,68 @@ class CompositeEventSource { } } +function createPossionProcessSamplingFilter (samplingIntervalMillis) { + let nextSamplingInstant = performance.now() + let currentSamplingInstant = 0 + setNextSamplingInstant() + + return event => { + const endTime = event.startTime + event.duration + while (endTime >= nextSamplingInstant) { + setNextSamplingInstant() + } + // An event is sampled if it started before, and ended on or after a sampling instant. The above + // while loop will ensure that the ending invariant is always true for the current sampling + // instant so we don't have to test for it below. Across calls, the invariant also holds as long + // as the events arrive in endTime order. This is true for events coming from + // DatadogInstrumentationEventSource; they will be ordered by endTime by virtue of this method + // being invoked synchronously with the plugins' finish() handler which evaluates + // performance.now(). OTOH, events coming from NodeAPIEventSource (GC in typical setup) might be + // somewhat delayed as they are queued by Node, so they can arrive out of order with regard to + // events coming from the non-queued source. By omitting the endTime check, we will pass through + // some short events that started and ended before the current sampling instant. OTOH, if we + // were to check for this.currentSamplingInstant <= endTime, we would discard some long events + // that also ended before the current sampling instant. We'd rather err on the side of including + // some short events than excluding some long events. + return event.startTime < currentSamplingInstant + } + + function setNextSamplingInstant () { + currentSamplingInstant = nextSamplingInstant + nextSamplingInstant -= Math.log(1 - Math.random()) * samplingIntervalMillis + } +} + /** * This class generates pprof files with timeline events. It combines an event - * source with an event serializer. + * source with a sampling event filter and an event serializer. */ class EventsProfiler { constructor (options = {}) { this.type = 'events' this.eventSerializer = new EventSerializer() - const eventHandler = event => { - this.eventSerializer.addEvent(event) + const eventHandler = event => this.eventSerializer.addEvent(event) + const eventFilter = options.timelineSamplingEnabled + // options.samplingInterval comes in microseconds, we need millis + ? createPossionProcessSamplingFilter((options.samplingInterval ?? 1e6 / 99) / 1000) + : _ => true + const filteringEventHandler = event => { + if (eventFilter(event)) { + eventHandler(event) + } } if (options.codeHotspotsEnabled) { // Use Datadog instrumentation to collect events with span IDs. Still use // Node API for GC events. this.eventSource = new CompositeEventSource([ - new DatadogInstrumentationEventSource(eventHandler), - new NodeApiEventSource(eventHandler, ['gc']) + new DatadogInstrumentationEventSource(eventHandler, eventFilter), + new NodeApiEventSource(filteringEventHandler, ['gc']) ]) } else { // Use Node API instrumentation to collect events without span IDs - this.eventSource = new NodeApiEventSource(eventHandler) + this.eventSource = new NodeApiEventSource(filteringEventHandler) } } From d6fd88c10766e08201edce8e5cf65effe6e5a643 Mon Sep 17 00:00:00 2001 From: ishabi Date: Tue, 3 Dec 2024 16:36:15 +0100 Subject: [PATCH 18/61] remove try catch from iast plugin (#4804) * remove try catch from iast plugin * fix linter --- .../dd-trace/src/appsec/iast/iast-plugin.js | 22 +----- .../analyzers/vulnerability-analyzer.spec.js | 21 ------ .../test/appsec/iast/iast-plugin.spec.js | 75 +++++++++++++------ 3 files changed, 55 insertions(+), 63 deletions(-) diff --git a/packages/dd-trace/src/appsec/iast/iast-plugin.js b/packages/dd-trace/src/appsec/iast/iast-plugin.js index 9c728a189b0..10dcde340c3 100644 --- a/packages/dd-trace/src/appsec/iast/iast-plugin.js +++ b/packages/dd-trace/src/appsec/iast/iast-plugin.js @@ -60,24 +60,10 @@ class IastPlugin extends Plugin { this.pluginSubs = [] } - _wrapHandler (handler) { - return (message, name) => { - try { - handler(message, name) - } catch (e) { - log.error('[ASM] Error executing IAST plugin handler', e) - } - } - } - _getTelemetryHandler (iastSub) { return () => { - try { - const iastContext = getIastContext(storage.getStore()) - iastSub.increaseExecuted(iastContext) - } catch (e) { - log.error('[ASM] Error increasing handler executed metrics', e) - } + const iastContext = getIastContext(storage.getStore()) + iastSub.increaseExecuted(iastContext) } } @@ -99,11 +85,11 @@ class IastPlugin extends Plugin { addSub (iastSub, handler) { if (typeof iastSub === 'string') { - super.addSub(iastSub, this._wrapHandler(handler)) + super.addSub(iastSub, handler) } else { iastSub = this._getAndRegisterSubscription(iastSub) if (iastSub) { - super.addSub(iastSub.channelName, this._wrapHandler(handler)) + super.addSub(iastSub.channelName, handler) if (iastTelemetry.isEnabled()) { super.addSub(iastSub.channelName, this._getTelemetryHandler(iastSub)) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js index 332e0c29e35..b47fb95b81b 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js @@ -133,27 +133,6 @@ describe('vulnerability-analyzer', () => { ) }) - it('should wrap subscription handler and catch thrown Errors', () => { - const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE) - const handler = sinon.spy(() => { - throw new Error('handler Error') - }) - const wrapped = vulnerabilityAnalyzer._wrapHandler(handler) - - const iastContext = { - name: 'test' - } - iastContextHandler.getIastContext.returns(iastContext) - - expect(typeof wrapped).to.be.equal('function') - const message = {} - const name = 'test' - expect(() => wrapped(message, name)).to.not.throw() - const args = handler.firstCall.args - expect(args[0]).to.be.equal(message) - expect(args[1]).to.be.equal(name) - }) - it('should catch thrown Errors inside subscription handlers', () => { const vulnerabilityAnalyzer = new VulnerabilityAnalyzer(ANALYZER_TYPE) vulnerabilityAnalyzer.addSub({ channelName: 'dd-trace:test:error:sub' }, () => { diff --git a/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js b/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js index caa4e91bf8b..21696d3b70f 100644 --- a/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/iast-plugin.spec.js @@ -4,6 +4,7 @@ const { expect } = require('chai') const { channel } = require('dc-polyfill') const proxyquire = require('proxyquire') const { getExecutedMetric, getInstrumentedMetric, TagKey } = require('../../../src/appsec/iast/telemetry/iast-metric') +const { IastPlugin } = require('../../../src/appsec/iast/iast-plugin') const VULNERABILITY_TYPE = TagKey.VULNERABILITY_TYPE const SOURCE_TYPE = TagKey.SOURCE_TYPE @@ -71,33 +72,23 @@ describe('IAST Plugin', () => { }) describe('addSub', () => { - it('should call Plugin.addSub with channelName and wrapped handler', () => { + it('should call Plugin.addSub with channelName and handler', () => { iastPlugin.addSub('test', handler) expect(addSubMock).to.be.calledOnce const args = addSubMock.getCall(0).args expect(args[0]).equal('test') - - const wrapped = args[1] - expect(wrapped).to.be.a('function') - expect(wrapped).to.not.be.equal(handler) - expect(wrapped()).to.not.throw - expect(logError).to.be.calledOnce + expect(args[1]).to.equal(handler) }) - it('should call Plugin.addSub with channelName and wrapped handler after registering iastPluginSub', () => { + it('should call Plugin.addSub with channelName and handler after registering iastPluginSub', () => { const iastPluginSub = { channelName: 'test' } iastPlugin.addSub(iastPluginSub, handler) expect(addSubMock).to.be.calledOnce const args = addSubMock.getCall(0).args expect(args[0]).equal('test') - - const wrapped = args[1] - expect(wrapped).to.be.a('function') - expect(wrapped).to.not.be.equal(handler) - expect(wrapped()).to.not.throw - expect(logError).to.be.calledOnce + expect(args[1]).to.equal(handler) }) it('should infer moduleName from channelName after registering iastPluginSub', () => { @@ -117,20 +108,15 @@ describe('IAST Plugin', () => { }) it('should not call _getTelemetryHandler', () => { - const wrapHandler = sinon.stub() - iastPlugin._wrapHandler = wrapHandler const getTelemetryHandler = sinon.stub() iastPlugin._getTelemetryHandler = getTelemetryHandler iastPlugin.addSub({ channelName, tagKey: VULNERABILITY_TYPE }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.not.called - wrapHandler.reset() getTelemetryHandler.reset() iastPlugin.addSub({ channelName, tagKey: SOURCE_TYPE, tag: 'test-tag' }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.not.called }) }) @@ -235,20 +221,15 @@ describe('IAST Plugin', () => { describe('addSub', () => { it('should call _getTelemetryHandler with correct metrics', () => { - const wrapHandler = sinon.stub() - iastPlugin._wrapHandler = wrapHandler const getTelemetryHandler = sinon.stub() iastPlugin._getTelemetryHandler = getTelemetryHandler iastPlugin.addSub({ channelName, tagKey: VULNERABILITY_TYPE }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.calledOnceWith(iastPlugin.pluginSubs[0]) - wrapHandler.reset() getTelemetryHandler.reset() iastPlugin.addSub({ channelName, tagKey: SOURCE_TYPE, tag: 'test-tag' }, handler) - expect(wrapHandler).to.be.calledOnceWith(handler) expect(getTelemetryHandler).to.be.calledOnceWith(iastPlugin.pluginSubs[1]) }) @@ -399,4 +380,50 @@ describe('IAST Plugin', () => { }) }) }) + + describe('Add sub to iast plugin', () => { + class BadPlugin extends IastPlugin { + static get id () { return 'badPlugin' } + + constructor () { + super() + this.addSub('appsec:badPlugin:start', this.start) + } + + start () { + throw new Error('this is one bad plugin') + } + } + class GoodPlugin extends IastPlugin { + static get id () { return 'goodPlugin' } + + constructor () { + super() + this.addSub('appsec:goodPlugin:start', this.start) + } + + start () {} + } + + const badPlugin = new BadPlugin() + const goodPlugin = new GoodPlugin() + + it('should disable bad plugin', () => { + badPlugin.configure({ enabled: true }) + expect(badPlugin._enabled).to.be.true + + channel('appsec:badPlugin:start').publish({ foo: 'bar' }) + + expect(badPlugin._enabled).to.be.false + }) + + it('should not disable good plugin', () => { + goodPlugin.configure({ enabled: true }) + expect(goodPlugin._enabled).to.be.true + + channel('appsec:goodPlugin:start').publish({ foo: 'bar' }) + + expect(goodPlugin._enabled).to.be.true + }) + }) }) From 66ac25add84a3260263e1f1d7cf94577851f8e13 Mon Sep 17 00:00:00 2001 From: ishabi Date: Thu, 5 Dec 2024 12:10:40 +0100 Subject: [PATCH 19/61] Explain why keeping query in http end translator (#4967) * remove query from http end translator * add nextjs comment * fix typo --- packages/dd-trace/src/appsec/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index be5273f815f..4748148a2de 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -163,6 +163,7 @@ function incomingHttpEndTranslator ({ req, res }) { persistent[addresses.HTTP_INCOMING_COOKIES] = req.cookies } + // we need to keep this to support nextjs if (req.query !== null && typeof req.query === 'object') { persistent[addresses.HTTP_INCOMING_QUERY] = req.query } From 823cfd44e0e472a3a2dd3853842f0280d259da3e Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Thu, 5 Dec 2024 16:09:38 -0500 Subject: [PATCH 20/61] fix next esm tests installing wrong version of react (#4973) * fix next esm tests installing wrong version of react * ignore prereleases when installing test peer dependencies --- .../test/integration-test/client.spec.js | 2 +- scripts/install_plugin_modules.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/datadog-plugin-next/test/integration-test/client.spec.js b/packages/datadog-plugin-next/test/integration-test/client.spec.js index 054e2fc6357..5bd4825ce93 100644 --- a/packages/datadog-plugin-next/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-next/test/integration-test/client.spec.js @@ -30,7 +30,7 @@ describe('esm', () => { before(async function () { // next builds slower in the CI, match timeout with unit tests this.timeout(120 * 1000) - sandbox = await createSandbox([`'next@${version}'`, 'react', 'react-dom'], + sandbox = await createSandbox([`'next@${version}'`, 'react@^18.2.0', 'react-dom@^18.2.0'], false, ['./packages/datadog-plugin-next/test/integration-test/*'], BUILD_COMMAND) }) diff --git a/scripts/install_plugin_modules.js b/scripts/install_plugin_modules.js index 682e2d3c5ad..c82ed03057b 100644 --- a/scripts/install_plugin_modules.js +++ b/scripts/install_plugin_modules.js @@ -151,7 +151,15 @@ async function addDependencies (dependencies, name, versionRange) { for (const dep of deps[name]) { for (const section of ['devDependencies', 'peerDependencies']) { if (pkgJson[section] && dep in pkgJson[section]) { - dependencies[dep] = pkgJson[section][dep] + if (pkgJson[section][dep].includes('||')) { + dependencies[dep] = pkgJson[section][dep].split('||') + .map(v => v.trim()) + .filter(v => !/[a-z]/.test(v)) // Ignore prereleases. + .join(' || ') + } else { + // Only one version available so use that even if it is a prerelease. + dependencies[dep] = pkgJson[section][dep] + } break } } From de5b2c81129fdcc2e239f4632049cd340c092bb4 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 5 Dec 2024 16:32:06 -0500 Subject: [PATCH 21/61] modernize eslint config (#4759) * modernize eslint config * Switch from the old eslintrc format to the newer format via: `npx @eslint/migrate-config .eslintrc.json` * ECMAScript version is now set at 2022, in line with code supported in Node.js 16. This is needed for a bunch of ESM syntax like top-level await. * Fixes: * ESM files are now covered. * Test globals and other test-specific config are now isolated to tests. * text_map.js has an invalid switch case. Fixed that in what I thought was the most reasonable way. * replace max-len with @stylistic/js/max-len * switch to stylistic for other rules * update LICENSE-3rdparty.csv * review feedback applied --- .eslintignore | 12 -- .eslintrc.json | 46 ------- LICENSE-3rdparty.csv | 4 + eslint.config.mjs | 119 ++++++++++++++++++ .../features-esm/support/steps.mjs | 3 + integration-tests/debugger/snapshot.spec.js | 4 +- .../debugger/target-app/snapshot.js | 2 +- .../esbuild/build-and-test-typescript.mjs | 4 +- integration-tests/esbuild/complex-app.mjs | 5 +- loader-hook.mjs | 4 + package.json | 4 + packages/datadog-instrumentations/src/pg.js | 2 +- .../test/eventbridge.spec.js | 2 +- .../test/kinesis.spec.js | 2 +- .../datadog-plugin-aws-sdk/test/sns.spec.js | 2 +- .../test/stepfunctions.spec.js | 2 +- .../test/integration-test/server.mjs | 2 +- .../src/scrub-cmd-params.js | 2 +- .../test/integration-test/server.mjs | 2 +- .../test/integration-test/server.mjs | 2 +- .../test/integration-test/server.mjs | 6 +- .../test/integration-test/server.mjs | 2 +- .../test/integration-test/server.mjs | 1 - .../test/integration-test/server2.mjs | 2 +- .../test/integration-test/server.mjs | 2 +- .../test/integration-test/server.mjs | 2 +- .../test/integration-test/server.mjs | 2 +- .../test/integration-test/server.mjs | 6 +- .../test/integration-test/server.mjs | 2 +- .../dd-trace/src/appsec/blocked_templates.js | 2 +- .../analyzers/hardcoded-password-rules.js | 2 +- .../iast/analyzers/hardcoded-secret-rules.js | 2 +- .../iast/analyzers/hardcoded-secrets-rules.js | 2 +- .../evidence-redaction/sensitive-regex.js | 4 +- .../iast/vulnerabilities-formatter/utils.js | 2 +- packages/dd-trace/src/azure_metadata.js | 8 +- packages/dd-trace/src/config.js | 8 +- .../debugger/devtools_client/remote_config.js | 4 +- .../src/opentracing/propagation/text_map.js | 11 +- .../hardcoded-password-analyzer.spec.js | 2 +- packages/dd-trace/test/config.spec.js | 6 +- .../test/exporters/common/docker.spec.js | 2 +- .../test/fixtures/esm/esm-hook-test.mjs | 1 + .../opentracing/propagation/text_map.spec.js | 2 +- .../dd-trace/test/telemetry/index.spec.js | 2 +- scripts/release/helpers/requirements.js | 2 +- yarn.lock | 54 +++++++- 47 files changed, 248 insertions(+), 118 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.json create mode 100644 eslint.config.mjs diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index fd409251590..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,12 +0,0 @@ -coverage -dist -docs -out -node_modules -versions -acmeair-nodejs -vendor -integration-tests/esbuild/out.js -integration-tests/esbuild/aws-sdk-out.js -packages/dd-trace/src/appsec/blocked_templates.js -packages/dd-trace/src/payload-tagging/jsonpath-plus.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 13031ec7db1..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 2021 - }, - "extends": [ - "eslint:recommended", - "standard", - "plugin:mocha/recommended" - ], - "plugins": [ - "mocha", - "n" - ], - "env": { - "node": true, - "es2021": true - }, - "settings": { - "node": { - "version": ">=16.0.0" - } - }, - "rules": { - "max-len": [2, 120, 2], - "no-var": 2, - "no-console": 2, - "prefer-const": 2, - "object-curly-spacing": [2, "always"], - "import/no-extraneous-dependencies": 2, - "standard/no-callback-literal": 0, - "no-prototype-builtins": 0, - "mocha/no-mocha-arrows": 0, - "mocha/no-setup-in-describe": 0, - "mocha/no-sibling-hooks": 0, - "mocha/no-top-level-hooks": 0, - "mocha/max-top-level-suites": 0, - "mocha/no-identical-title": 0, - "mocha/no-global-tests": 0, - "mocha/no-exports": 0, - "mocha/no-skipped-tests": 0, - "n/no-restricted-require": [2, ["diagnostics_channel"]], - "n/no-callback-literal": 0, - "object-curly-newline": ["error", {"multiline": true, "consistent": true }], - "import/no-absolute-path": 0 - } -} diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index f8147f23e35..a4f6f0536fa 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -34,6 +34,9 @@ require,shell-quote,mit,Copyright (c) 2013 James Halliday require,source-map,BSD-3-Clause,Copyright (c) 2009-2011, Mozilla Foundation and contributors dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.) dev,@types/node,MIT,Copyright Authors +dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, +dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, +dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, dev,autocannon,MIT,Copyright 2016 Matteo Collina dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. dev,axios,MIT,Copyright 2014-present Matt Zabriskie @@ -54,6 +57,7 @@ dev,eslint-plugin-promise,ISC,jden and other contributors dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson dev,get-port,MIT,Copyright Sindre Sorhus dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors +dev,globals,MIT,Copyright (c) Sindre Sorhus (https://sindresorhus.com) dev,graphql,MIT,Copyright 2015 Facebook Inc. dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors dev,knex,MIT,Copyright (c) 2013-present Tim Griesser diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..8b83488c08e --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,119 @@ +import mocha from 'eslint-plugin-mocha' +import n from 'eslint-plugin-n' +import stylistic from '@stylistic/eslint-plugin-js' +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}) + +export default [ + { + ignores: [ + '**/coverage', // Just coverage reports. + '**/dist', // Generated + '**/docs', // Any JS here is for presentation only. + '**/out', // Generated + '**/node_modules', // We don't own these. + '**/versions', // This is effectively a node_modules tree. + '**/acmeair-nodejs', // We don't own this. + '**/vendor', // Generally, we didn't author this code. + 'integration-tests/esbuild/out.js', // Generated + 'integration-tests/esbuild/aws-sdk-out.js', // Generated + 'packages/dd-trace/src/appsec/blocked_templates.js', // TODO Why is this ignored? + 'packages/dd-trace/src/payload-tagging/jsonpath-plus.js' // Vendored + ] + }, ...compat.extends('eslint:recommended', 'standard', 'plugin:mocha/recommended'), { + plugins: { + mocha, + n, + '@stylistic/js': stylistic + }, + + languageOptions: { + globals: { + ...globals.node + }, + + ecmaVersion: 2022 + }, + + settings: { + node: { + version: '>=16.0.0' + } + }, + + rules: { + '@stylistic/js/max-len': ['error', { code: 120, tabWidth: 2 }], + '@stylistic/js/object-curly-newline': ['error', { + multiline: true, + consistent: true + }], + '@stylistic/js/object-curly-spacing': ['error', 'always'], + 'import/no-absolute-path': 'off', + 'import/no-extraneous-dependencies': 'error', + 'n/no-callback-literal': 'off', + 'n/no-restricted-require': ['error', ['diagnostics_channel']], + 'no-console': 'error', + 'no-prototype-builtins': 'off', + 'no-unused-expressions': 'off', + 'no-var': 'error', + 'prefer-const': 'error', + 'standard/no-callback-literal': 'off' + } + }, + { + files: [ + 'packages/*/test/**/*.js', + 'packages/*/test/**/*.mjs', + 'integration-tests/**/*.js', + 'integration-tests/**/*.mjs', + '**/*.spec.js' + ], + languageOptions: { + globals: { + ...globals.mocha, + sinon: false, + expect: false, + proxyquire: false, + withVersions: false, + withPeerService: false, + withNamingSchema: false, + withExports: false + } + }, + rules: { + 'mocha/max-top-level-suites': 'off', + 'mocha/no-exports': 'off', + 'mocha/no-global-tests': 'off', + 'mocha/no-identical-title': 'off', + 'mocha/no-mocha-arrows': 'off', + 'mocha/no-setup-in-describe': 'off', + 'mocha/no-sibling-hooks': 'off', + 'mocha/no-skipped-tests': 'off', + 'mocha/no-top-level-hooks': 'off', + 'n/handle-callback-err': 'off', + 'no-loss-of-precision': 'off' + } + }, + { + files: [ + 'integration-tests/**/*.js', + 'integration-tests/**/*.mjs', + 'packages/*/test/integration-test/**/*.js', + 'packages/*/test/integration-test/**/*.mjs' + ], + rules: { + 'import/no-extraneous-dependencies': 'off' + } + } +] diff --git a/integration-tests/ci-visibility/features-esm/support/steps.mjs b/integration-tests/ci-visibility/features-esm/support/steps.mjs index 64194a68684..66d05584383 100644 --- a/integration-tests/ci-visibility/features-esm/support/steps.mjs +++ b/integration-tests/ci-visibility/features-esm/support/steps.mjs @@ -5,12 +5,15 @@ class Greeter { sayFarewell () { return 'farewell' } + sayGreetings () { return 'greetings' } + sayYo () { return 'yo' } + sayYeah () { return 'yeah whatever' } diff --git a/integration-tests/debugger/snapshot.spec.js b/integration-tests/debugger/snapshot.spec.js index 94ef323f6a7..e3d17b225c4 100644 --- a/integration-tests/debugger/snapshot.spec.js +++ b/integration-tests/debugger/snapshot.spec.js @@ -31,7 +31,7 @@ describe('Dynamic Instrumentation', function () { str: { type: 'string', value: 'foo' }, lstr: { type: 'string', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor i', truncated: true, size: 445 @@ -129,7 +129,7 @@ describe('Dynamic Instrumentation', function () { str: { type: 'string', value: 'foo' }, lstr: { type: 'string', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor i', truncated: true, size: 445 diff --git a/integration-tests/debugger/target-app/snapshot.js b/integration-tests/debugger/target-app/snapshot.js index a7b1810c10b..bae83a2176e 100644 --- a/integration-tests/debugger/target-app/snapshot.js +++ b/integration-tests/debugger/target-app/snapshot.js @@ -30,7 +30,7 @@ function getSomeData () { num: 42, bigint: 42n, str: 'foo', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len lstr: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', sym: Symbol('foo'), regex: /bar/i, diff --git a/integration-tests/esbuild/build-and-test-typescript.mjs b/integration-tests/esbuild/build-and-test-typescript.mjs index bba9500cdd3..2fd2966384d 100755 --- a/integration-tests/esbuild/build-and-test-typescript.mjs +++ b/integration-tests/esbuild/build-and-test-typescript.mjs @@ -18,8 +18,8 @@ await esbuild.build({ external: [ 'graphql/language/visitor', 'graphql/language/printer', - 'graphql/utilities', - ], + 'graphql/utilities' + ] }) console.log('ok') // eslint-disable-line no-console diff --git a/integration-tests/esbuild/complex-app.mjs b/integration-tests/esbuild/complex-app.mjs index 5f097655eeb..5936a2c3983 100755 --- a/integration-tests/esbuild/complex-app.mjs +++ b/integration-tests/esbuild/complex-app.mjs @@ -4,10 +4,11 @@ import 'dd-trace/init.js' import assert from 'assert' import express from 'express' import redis from 'redis' -const app = express() -const PORT = 3000 import pg from 'pg' import PGP from 'pg-promise' // transient dep of 'pg' + +const app = express() +const PORT = 3000 const pgp = PGP() assert.equal(redis.Graph.name, 'Graph') diff --git a/loader-hook.mjs b/loader-hook.mjs index 40bbdbade81..fc2a250e3a1 100644 --- a/loader-hook.mjs +++ b/loader-hook.mjs @@ -1 +1,5 @@ +// TODO(bengl): Not sure why `import/export` fails on this line, but it's just +// a passthrough to another module so it should be fine. Disabling for now. + +// eslint-disable-next-line import/export export * from 'import-in-the-middle/hook.mjs' diff --git a/package.json b/package.json index f39bcd5a68a..3f7189f5d98 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,9 @@ }, "devDependencies": { "@apollo/server": "^4.11.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.11.1", + "@stylistic/eslint-plugin-js": "^2.8.0", "@types/node": "^16.18.103", "autocannon": "^4.5.2", "aws-sdk": "^2.1446.0", @@ -139,6 +142,7 @@ "express": "^4.18.2", "get-port": "^3.2.0", "glob": "^7.1.6", + "globals": "^15.10.0", "graphql": "0.13.2", "jszip": "^3.5.0", "knex": "^2.4.2", diff --git a/packages/datadog-instrumentations/src/pg.js b/packages/datadog-instrumentations/src/pg.js index 55642d82e96..6c3d621ad00 100644 --- a/packages/datadog-instrumentations/src/pg.js +++ b/packages/datadog-instrumentations/src/pg.js @@ -72,7 +72,7 @@ function wrapQuery (query) { if (abortController.signal.aborted) { const error = abortController.signal.reason || new Error('Aborted') - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len // Based on: https://github.com/brianc/node-postgres/blob/54eb0fa216aaccd727765641e7d1cf5da2bc483d/packages/pg/lib/client.js#L510 const reusingQuery = typeof pgQuery.submit === 'function' const callback = arguments[arguments.length - 1] diff --git a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js index 3f65acdab0b..342af3ea723 100644 --- a/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/eventbridge.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const EventBridge = require('../src/services/eventbridge') diff --git a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js index 04c3ba796ee..2e3bf356f3e 100644 --- a/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/kinesis.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const sinon = require('sinon') diff --git a/packages/datadog-plugin-aws-sdk/test/sns.spec.js b/packages/datadog-plugin-aws-sdk/test/sns.spec.js index 7b62156f06c..b205c652669 100644 --- a/packages/datadog-plugin-aws-sdk/test/sns.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/sns.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const sinon = require('sinon') diff --git a/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js b/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js index ed77ecd51b2..44677b4efed 100644 --- a/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js +++ b/packages/datadog-plugin-aws-sdk/test/stepfunctions.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const semver = require('semver') diff --git a/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs b/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs index 91ff60029fb..c65ebffe78d 100644 --- a/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs +++ b/packages/datadog-plugin-cassandra-driver/test/integration-test/server.mjs @@ -9,4 +9,4 @@ const client = new Client({ await client.connect() await client.execute('SELECT now() FROM local;') -await client.shutdown() \ No newline at end of file +await client.shutdown() diff --git a/packages/datadog-plugin-child_process/src/scrub-cmd-params.js b/packages/datadog-plugin-child_process/src/scrub-cmd-params.js index b5fb59bb781..595d8f5746a 100644 --- a/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +++ b/packages/datadog-plugin-child_process/src/scrub-cmd-params.js @@ -6,7 +6,7 @@ const ALLOWED_ENV_VARIABLES = ['LD_PRELOAD', 'LD_LIBRARY_PATH', 'PATH'] const PROCESS_DENYLIST = ['md5'] const VARNAMES_REGEX = /\$([\w\d_]*)(?:[^\w\d_]|$)/gmi -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const PARAM_PATTERN = '^-{0,2}(?:p(?:ass(?:w(?:or)?d)?)?|address|api[-_]?key|e?mail|secret(?:[-_]?key)?|a(?:ccess|uth)[-_]?token|mysql_pwd|credentials|(?:stripe)?token)$' const regexParam = new RegExp(PARAM_PATTERN, 'i') const ENV_PATTERN = '^(\\w+=\\w+;)*\\w+=\\w+;?$' diff --git a/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs b/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs index a54efd22e4d..f3f2cc1d9a7 100644 --- a/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs +++ b/packages/datadog-plugin-elasticsearch/test/integration-test/server.mjs @@ -3,4 +3,4 @@ import { Client } from '@elastic/elasticsearch' const client = new Client({ node: 'http://localhost:9200' }) -await client.ping() \ No newline at end of file +await client.ping() diff --git a/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs b/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs index f315996ba58..fc3ab176f24 100644 --- a/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs +++ b/packages/datadog-plugin-google-cloud-pubsub/test/integration-test/server.mjs @@ -8,4 +8,4 @@ const [subscription] = await topic.createSubscription('foo') await topic.publishMessage({ data: Buffer.from('Test message!') }) await subscription.close() -await pubsub.close() \ No newline at end of file +await pubsub.close() diff --git a/packages/datadog-plugin-graphql/test/integration-test/server.mjs b/packages/datadog-plugin-graphql/test/integration-test/server.mjs index 822155d1710..d7aab2d1b3b 100644 --- a/packages/datadog-plugin-graphql/test/integration-test/server.mjs +++ b/packages/datadog-plugin-graphql/test/integration-test/server.mjs @@ -15,8 +15,8 @@ const schema = new graphql.GraphQLSchema({ }) }) -await graphql.graphql({ - schema, - source: `query MyQuery { hello(name: "world") }`, +await graphql.graphql({ + schema, + source: 'query MyQuery { hello(name: "world") }', variableValues: { who: 'world' } }) diff --git a/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs b/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs index bf174c489da..ce72c80e82d 100644 --- a/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs +++ b/packages/datadog-plugin-microgateway-core/test/integration-test/server.mjs @@ -6,7 +6,7 @@ import getPort from 'get-port' const port = await getPort() const gateway = Gateway({ edgemicro: { - port: port, + port, logging: { level: 'info', dir: os.tmpdir() } }, proxies: [] diff --git a/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs b/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs index 11fa3ac576b..0c643c53a7b 100644 --- a/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs +++ b/packages/datadog-plugin-mongodb-core/test/integration-test/server.mjs @@ -7,4 +7,3 @@ const db = client.db('test_db') const collection = db.collection('test_collection') collection.insertOne({ a: 1 }, {}, () => {}) setTimeout(() => { client.close() }, 1500) - diff --git a/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs b/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs index 39127aaab23..c11c934993d 100644 --- a/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs +++ b/packages/datadog-plugin-mongodb-core/test/integration-test/server2.mjs @@ -15,7 +15,7 @@ const connectPromise = new Promise((resolve, reject) => { await server.connect() await connectPromise -server.insert(`test.your_collection_name`, [{ a: 1 }], {}, (err) => { +server.insert('test.your_collection_name', [{ a: 1 }], {}, (err) => { if (err) { return } diff --git a/packages/datadog-plugin-net/test/integration-test/server.mjs b/packages/datadog-plugin-net/test/integration-test/server.mjs index 4575498e13a..fc7ec19a696 100644 --- a/packages/datadog-plugin-net/test/integration-test/server.mjs +++ b/packages/datadog-plugin-net/test/integration-test/server.mjs @@ -14,4 +14,4 @@ client.on('end', () => { client.on('error', (err) => { client.end() -}) \ No newline at end of file +}) diff --git a/packages/datadog-plugin-openai/test/integration-test/server.mjs b/packages/datadog-plugin-openai/test/integration-test/server.mjs index 56a046d56c0..62d812baea8 100644 --- a/packages/datadog-plugin-openai/test/integration-test/server.mjs +++ b/packages/datadog-plugin-openai/test/integration-test/server.mjs @@ -23,7 +23,7 @@ nock('https://api.openai.com:443') ]) const openaiApp = new openai.OpenAIApi(new openai.Configuration({ - apiKey: 'sk-DATADOG-ACCEPTANCE-TESTS', + apiKey: 'sk-DATADOG-ACCEPTANCE-TESTS' })) await openaiApp.createCompletion({ diff --git a/packages/datadog-plugin-opensearch/test/integration-test/server.mjs b/packages/datadog-plugin-opensearch/test/integration-test/server.mjs index 0b45b5eefb2..21be1cead43 100644 --- a/packages/datadog-plugin-opensearch/test/integration-test/server.mjs +++ b/packages/datadog-plugin-opensearch/test/integration-test/server.mjs @@ -1,5 +1,5 @@ import 'dd-trace/init.js' import opensearch from '@opensearch-project/opensearch' -const client = new opensearch.Client({ node: `http://localhost:9201` }) +const client = new opensearch.Client({ node: 'http://localhost:9201' }) await client.ping() diff --git a/packages/datadog-plugin-oracledb/test/integration-test/server.mjs b/packages/datadog-plugin-oracledb/test/integration-test/server.mjs index b50a7b36d13..739877fbcd7 100644 --- a/packages/datadog-plugin-oracledb/test/integration-test/server.mjs +++ b/packages/datadog-plugin-oracledb/test/integration-test/server.mjs @@ -7,13 +7,11 @@ const config = { user: 'test', password: 'Oracle18', connectString: `${hostname}:1521/xepdb1` -}; +} const dbQuery = 'select current_timestamp from dual' -let connection; - -connection = await oracledb.getConnection(config) +const connection = await oracledb.getConnection(config) await connection.execute(dbQuery) if (connection) { diff --git a/packages/datadog-plugin-sharedb/test/integration-test/server.mjs b/packages/datadog-plugin-sharedb/test/integration-test/server.mjs index c0b93fbcab2..8b593029fc9 100644 --- a/packages/datadog-plugin-sharedb/test/integration-test/server.mjs +++ b/packages/datadog-plugin-sharedb/test/integration-test/server.mjs @@ -4,4 +4,4 @@ import ShareDB from 'sharedb' const backend = new ShareDB({ presence: true }) const connection = backend.connect() await connection.get('some-collection', 'some-id').fetch() -connection.close() \ No newline at end of file +connection.close() diff --git a/packages/dd-trace/src/appsec/blocked_templates.js b/packages/dd-trace/src/appsec/blocked_templates.js index 1eb62e22df0..3017d4de9db 100644 --- a/packages/dd-trace/src/appsec/blocked_templates.js +++ b/packages/dd-trace/src/appsec/blocked_templates.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const html = `You've been blocked

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

` diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js index 2e204b72830..04e243c8b5a 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const { NameAndValue } = require('./hardcoded-rule-type') diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js index 88ec3d54254..1d61c5fcc91 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const { ValueOnly, NameAndValue } = require('./hardcoded-rule-type') diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js index 88ec3d54254..1d61c5fcc91 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const { ValueOnly, NameAndValue } = require('./hardcoded-rule-type') diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js index fe9d22f9c49..e0054b8546f 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js @@ -1,6 +1,6 @@ -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const DEFAULT_IAST_REDACTION_NAME_PATTERN = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|(?:sur|last)name|user(?:name)?|address|e?mail)' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const DEFAULT_IAST_REDACTION_VALUE_PATTERN = '(?:bearer\\s+[a-z0-9\\._\\-]+|glpat-[\\w\\-]{20}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=\\-]+\\.ey[I-L][\\w=\\-]+(?:\\.[\\w.+/=\\-]+)?|(?:[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY[\\-]{5}|ssh-rsa\\s*[a-z0-9/\\.+]{100,})|[\\w\\.-]+@[a-zA-Z\\d\\.-]+\\.[a-zA-Z]{2,})' module.exports = { diff --git a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js index 959df790afd..256b47f5532 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js @@ -7,7 +7,7 @@ const STRINGIFY_RANGE_KEY = 'DD_' + crypto.randomBytes(20).toString('hex') const STRINGIFY_SENSITIVE_KEY = STRINGIFY_RANGE_KEY + 'SENSITIVE' const STRINGIFY_SENSITIVE_NOT_STRING_KEY = STRINGIFY_SENSITIVE_KEY + 'NOTSTRING' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const KEYS_REGEX_WITH_SENSITIVE_RANGES = new RegExp(`(?:"(${STRINGIFY_RANGE_KEY}_\\d+_))|(?:"(${STRINGIFY_SENSITIVE_KEY}_\\d+_(\\d+)_))|("${STRINGIFY_SENSITIVE_NOT_STRING_KEY}_\\d+_([\\s0-9.a-zA-Z]*)")`, 'gm') const KEYS_REGEX_WITHOUT_SENSITIVE_RANGES = new RegExp(`"(${STRINGIFY_RANGE_KEY}_\\d+_)`, 'gm') diff --git a/packages/dd-trace/src/azure_metadata.js b/packages/dd-trace/src/azure_metadata.js index 94c29c9dd16..6895f28b479 100644 --- a/packages/dd-trace/src/azure_metadata.js +++ b/packages/dd-trace/src/azure_metadata.js @@ -1,6 +1,6 @@ 'use strict' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len // Modeled after https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/ddcommon/src/azure_app_services.rs const os = require('os') @@ -79,7 +79,7 @@ function buildMetadata () { function getAzureAppMetadata () { // DD_AZURE_APP_SERVICES is an environment variable introduced by the .NET APM team and is set automatically for // anyone using the Datadog APM Extensions (.NET, Java, or Node) for Windows Azure App Services - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len // See: https://github.com/DataDog/datadog-aas-extension/blob/01f94b5c28b7fa7a9ab264ca28bd4e03be603900/node/src/applicationHost.xdt#L20-L21 return process.env.DD_AZURE_APP_SERVICES !== undefined ? buildMetadata() : undefined } @@ -88,9 +88,9 @@ function getAzureFunctionMetadata () { return getIsAzureFunction() ? buildMetadata() : undefined } -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len // Modeled after https://github.com/DataDog/libdatadog/blob/92272e90a7919f07178f3246ef8f82295513cfed/profiling/src/exporter/mod.rs#L187 -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len // and https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/trace-utils/src/trace_utils.rs#L533 function getAzureTagsFromMetadata (metadata) { if (metadata === undefined) { diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 73cac449546..588dd5e8b9e 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -132,11 +132,11 @@ function checkIfBothOtelAndDdEnvVarSet () { const fromEntries = Object.fromEntries || (entries => entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {})) -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const qsRegex = '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const defaultWafObfuscatorKeyRegex = '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\\.net[_-]sessionid|sid|jwt' -// eslint-disable-next-line max-len +// eslint-disable-next-line @stylistic/js/max-len const defaultWafObfuscatorValueRegex = '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\\.net(?:[_-]|-)sessionid|sid|jwt)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}' const runtimeId = uuid() @@ -1288,7 +1288,7 @@ class Config { // TODO: Deeply merge configurations. // TODO: Move change tracking to telemetry. // for telemetry reporting, `name`s in `containers` need to be keys from: - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-payload/static/config_norm_rules.json _merge () { const containers = [this._remote, this._options, this._env, this._calculated, this._defaults] diff --git a/packages/dd-trace/src/debugger/devtools_client/remote_config.js b/packages/dd-trace/src/debugger/devtools_client/remote_config.js index 8a7d7386e33..b0cffee3732 100644 --- a/packages/dd-trace/src/debugger/devtools_client/remote_config.js +++ b/packages/dd-trace/src/debugger/devtools_client/remote_config.js @@ -66,7 +66,7 @@ async function processMsg (action, probe) { } if (!probe.where.sourceFile && !probe.where.lines) { throw new Error( - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len `Unsupported probe insertion point! Only line-based probes are supported (id: ${probe.id}, version: ${probe.version})` ) } @@ -98,7 +98,7 @@ async function processMsg (action, probe) { break default: throw new Error( - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len `Cannot process probe ${probe.id} (version: ${probe.version}) - unknown remote configuration action: ${action}` ) } diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index b117ae0ae5e..dcf8fb3fcc6 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -300,14 +300,17 @@ class TextMapPropagator { case 'tracecontext': extractedContext = this._extractTraceparentContext(carrier) break - case 'b3' && this - ._config - .tracePropagationStyle - .otelPropagators: // TODO: should match "b3 single header" in next major case 'b3 single header': // TODO: delete in major after singular "b3" extractedContext = this._extractB3SingleContext(carrier) break case 'b3': + if (this._config.tracePropagationStyle.otelPropagators) { + // TODO: should match "b3 single header" in next major + extractedContext = this._extractB3SingleContext(carrier) + } else { + extractedContext = this._extractB3MultiContext(carrier) + } + break case 'b3multi': extractedContext = this._extractB3MultiContext(carrier) break diff --git a/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js index fdc51ce0153..e20c83ef33d 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/hardcoded-password-analyzer.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ 'use strict' const path = require('path') diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 503c2675a95..62fe403eaa8 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -287,13 +287,13 @@ describe('Config', () => { { name: 'appsec.enabled', value: undefined, origin: 'default' }, { name: 'appsec.obfuscatorKeyRegex', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: '(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\\.net[_-]sessionid|sid|jwt', origin: 'default' }, { name: 'appsec.obfuscatorValueRegex', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: '(?i)(?:p(?:ass)?w(?:or)?d|pass(?:[_-]?phrase)?|secret(?:[_-]?key)?|(?:(?:api|private|public|access)[_-]?)key(?:[_-]?id)?|(?:(?:auth|access|id|refresh)[_-]?)?token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?|jsessionid|phpsessid|asp\\.net(?:[_-]|-)sessionid|sid|jwt)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?|[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}', origin: 'default' }, @@ -362,7 +362,7 @@ describe('Config', () => { { name: 'protocolVersion', value: '0.4', origin: 'default' }, { name: 'queryStringObfuscation', - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len value: '(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:(?:\\s|%20)*(?:=|%3D)[^&]+|(?:"|%22)(?:\\s|%20)*(?::|%3A)(?:\\s|%20)*(?:"|%22)(?:%2[^2]|%[^2]|[^"%])+(?:"|%22))|bearer(?:\\s|%20)+[a-z0-9\\._\\-]+|token(?::|%3A)[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L](?:[\\w=-]|%3D)+\\.ey[I-L](?:[\\w=-]|%3D)+(?:\\.(?:[\\w.+\\/=-]|%3D|%2F|%2B)+)?|[\\-]{5}BEGIN(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY[\\-]{5}[^\\-]+[\\-]{5}END(?:[a-z\\s]|%20)+PRIVATE(?:\\s|%20)KEY|ssh-rsa(?:\\s|%20)*(?:[a-z0-9\\/\\.+]|%2F|%5C|%2B){100,}', origin: 'default' }, diff --git a/packages/dd-trace/test/exporters/common/docker.spec.js b/packages/dd-trace/test/exporters/common/docker.spec.js index dd1610c8e60..2c2bc9275b8 100644 --- a/packages/dd-trace/test/exporters/common/docker.spec.js +++ b/packages/dd-trace/test/exporters/common/docker.spec.js @@ -53,7 +53,7 @@ describe('docker', () => { it('should support IDs with Kubernetes format', () => { const cgroup = [ - '1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2d3da189_6407_48e3_9ab6_78188d75e609.slice/docker-7b8952daecf4c0e44bbcefe1b5c5ebc7b4839d4eefeccefe694709d3809b6199.scope' // eslint-disable-line max-len + '1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2d3da189_6407_48e3_9ab6_78188d75e609.slice/docker-7b8952daecf4c0e44bbcefe1b5c5ebc7b4839d4eefeccefe694709d3809b6199.scope' // eslint-disable-line @stylistic/js/max-len ].join('\n') fs.readFileSync.withArgs('/proc/self/cgroup').returns(Buffer.from(cgroup)) diff --git a/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs b/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs index 9f9bd110f04..ea6a7ab34fe 100644 --- a/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs +++ b/packages/dd-trace/test/fixtures/esm/esm-hook-test.mjs @@ -20,6 +20,7 @@ esmHook(['express', 'os'], (exports, name, baseDir) => { const { freemem } = await import('os') const expressResult = expressDefault() const express = typeof expressResult === 'function' ? 'express()' : expressResult + // eslint-disable-next-line no-console console.log(JSON.stringify({ express, freemem: freemem() diff --git a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js index c6247330a69..3e4f6aed3e8 100644 --- a/packages/dd-trace/test/opentracing/propagation/text_map.spec.js +++ b/packages/dd-trace/test/opentracing/propagation/text_map.spec.js @@ -108,7 +108,7 @@ describe('TextMapPropagator', () => { const spanContext = createContext({ baggageItems }) propagator.inject(spanContext, carrier) - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len expect(carrier.baggage).to.be.equal('%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D%F0%9F%90%B6%C3%A9%E6%88%91=%22%2C%3B%5C%F0%9F%90%B6%C3%A9%E6%88%91') }) diff --git a/packages/dd-trace/test/telemetry/index.spec.js b/packages/dd-trace/test/telemetry/index.spec.js index 306d7a16c30..0263f395e9f 100644 --- a/packages/dd-trace/test/telemetry/index.spec.js +++ b/packages/dd-trace/test/telemetry/index.spec.js @@ -409,7 +409,7 @@ describe('Telemetry extended heartbeat', () => { { name: 'DD_TRACE_SAMPLING_RULES', value: - // eslint-disable-next-line max-len + // eslint-disable-next-line @stylistic/js/max-len '[{"service":"*","sampling_rate":1},{"service":"svc*","resource":"*abc","name":"op-??","tags":{"tag-a":"ta-v*","tag-b":"tb-v?","tag-c":"tc-v"},"sample_rate":0.5}]', origin: 'code' } diff --git a/scripts/release/helpers/requirements.js b/scripts/release/helpers/requirements.js index e8488610051..a2da9f924bb 100644 --- a/scripts/release/helpers/requirements.js +++ b/scripts/release/helpers/requirements.js @@ -1,6 +1,6 @@ 'use strict' -/* eslint-disable max-len */ +/* eslint-disable @stylistic/js/max-len */ const { capture, fatal, run } = require('./terminal') diff --git a/yarn.lock b/yarn.lock index 0efe56a17c9..5af61ae9c7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -589,11 +589,31 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" + integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + "@eslint/js@8.57.0": version "8.57.0" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@^9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.11.1.tgz#8bcb37436f9854b3d9a561440daf916acd940986" + integrity sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA== + "@graphql-tools/merge@^8.4.1": version "8.4.2" resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.4.2.tgz#95778bbe26b635e8d2f60ce9856b388f11fe8288" @@ -891,6 +911,14 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== +"@stylistic/eslint-plugin-js@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.8.0.tgz#f605202c75aa17692342662231f77d413d96d940" + integrity sha512-/e7pSzVMrwBd6yzSDsKHwax3TS96+pd/xSKzELaTkOuYqUhYfj/becWdfDbFSBGQD7BBBCiiE4L8L2cUfu5h+A== + dependencies: + eslint-visitor-keys "^4.0.0" + espree "^10.1.0" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -1012,7 +1040,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.12.0, acorn@^8.8.2, acorn@^8.9.0: version "8.12.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -2219,6 +2247,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.0.0, eslint-visitor-keys@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c" + integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg== + eslint@^8.57.0: version "8.57.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" @@ -2268,6 +2301,15 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== +espree@^10.0.1, espree@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.2.0.tgz#f4bcead9e05b0615c968e85f83816bc386a45df6" + integrity sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g== + dependencies: + acorn "^8.12.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.1.0" + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -2668,6 +2710,16 @@ globals@^13.19.0, globals@^13.24.0: dependencies: type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^15.10.0: + version "15.10.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.10.0.tgz#a7eab3886802da248ad8b6a9ccca6573ff899c9b" + integrity sha512-tqFIbz83w4Y5TCbtgjZjApohbuh7K9BxGYFm7ifwDR240tvdb7P9x+/9VvUKlmkPoiknoJtanI8UOrqxS3a7lQ== + globalthis@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" From 528c013716732b605e510cfa0bb4ccda614c12a5 Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Fri, 6 Dec 2024 08:49:32 -0500 Subject: [PATCH 22/61] fix next test using an incompatible version of react (#4977) --- scripts/install_plugin_modules.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/install_plugin_modules.js b/scripts/install_plugin_modules.js index c82ed03057b..608fe71a992 100644 --- a/scripts/install_plugin_modules.js +++ b/scripts/install_plugin_modules.js @@ -152,12 +152,10 @@ async function addDependencies (dependencies, name, versionRange) { for (const section of ['devDependencies', 'peerDependencies']) { if (pkgJson[section] && dep in pkgJson[section]) { if (pkgJson[section][dep].includes('||')) { - dependencies[dep] = pkgJson[section][dep].split('||') - .map(v => v.trim()) - .filter(v => !/[a-z]/.test(v)) // Ignore prereleases. - .join(' || ') + // Use the first version in the list (as npm does by default) + dependencies[dep] = pkgJson[section][dep].split('||')[0].trim() } else { - // Only one version available so use that even if it is a prerelease. + // Only one version available so use that. dependencies[dep] = pkgJson[section][dep] } break From e8e074e0dcaca38849b2313bc0b184a9af9187ad Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 6 Dec 2024 14:59:29 +0100 Subject: [PATCH 23/61] Bump path-to-regexp from v0.1.10 to v0.1.12 (#4979) --- package.json | 4 ++-- yarn.lock | 34 +++++++++++++++++----------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 3f7189f5d98..7a5149e2533 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "module-details-from-path": "^1.0.3", "msgpack-lite": "^0.1.26", "opentracing": ">=0.12.1", - "path-to-regexp": "^0.1.10", + "path-to-regexp": "^0.1.12", "pprof-format": "^2.1.0", "protobufjs": "^7.2.5", "retry": "^0.13.1", @@ -139,7 +139,7 @@ "eslint-plugin-mocha": "^10.4.3", "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.4.0", - "express": "^4.18.2", + "express": "^4.21.2", "get-port": "^3.2.0", "glob": "^7.1.6", "globals": "^15.10.0", diff --git a/yarn.lock b/yarn.lock index 5af61ae9c7c..2eaf99af6fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -871,6 +871,14 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== +"@stylistic/eslint-plugin-js@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.8.0.tgz#f605202c75aa17692342662231f77d413d96d940" + integrity sha512-/e7pSzVMrwBd6yzSDsKHwax3TS96+pd/xSKzELaTkOuYqUhYfj/becWdfDbFSBGQD7BBBCiiE4L8L2cUfu5h+A== + dependencies: + eslint-visitor-keys "^4.0.0" + espree "^10.1.0" + "@types/body-parser@*": version "1.19.5" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" @@ -911,14 +919,6 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== -"@stylistic/eslint-plugin-js@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.8.0.tgz#f605202c75aa17692342662231f77d413d96d940" - integrity sha512-/e7pSzVMrwBd6yzSDsKHwax3TS96+pd/xSKzELaTkOuYqUhYfj/becWdfDbFSBGQD7BBBCiiE4L8L2cUfu5h+A== - dependencies: - eslint-visitor-keys "^4.0.0" - espree "^10.1.0" - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -2368,10 +2368,10 @@ events@1.1.1: resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz" integrity "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" -express@^4.17.1, express@^4.18.2: - version "4.21.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" - integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== +express@^4.17.1, express@^4.21.2: + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" @@ -2392,7 +2392,7 @@ express@^4.17.1, express@^4.18.2: methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.10" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" qs "6.13.0" range-parser "~1.2.1" @@ -4012,10 +4012,10 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.10, path-to-regexp@^0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== +path-to-regexp@0.1.12, path-to-regexp@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-to-regexp@^6.2.1: version "6.3.0" From c131b4cb38807985f99c81a59092c4f8af802d0e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 6 Dec 2024 15:15:44 +0100 Subject: [PATCH 24/61] Delete unused benchmark for profiler (#4978) The folder `benchmark/profiler` contained benchmark code for the Profiler. However, it hasn't been used in a while and is currently broken. Deleting to avoid confusion. --- benchmark/profiler/index.js | 230 ----------------------------------- benchmark/profiler/server.js | 26 ---- package.json | 1 - 3 files changed, 257 deletions(-) delete mode 100644 benchmark/profiler/index.js delete mode 100644 benchmark/profiler/server.js diff --git a/benchmark/profiler/index.js b/benchmark/profiler/index.js deleted file mode 100644 index 20f1455d05d..00000000000 --- a/benchmark/profiler/index.js +++ /dev/null @@ -1,230 +0,0 @@ -'use strict' - -/* eslint-disable no-console */ - -const autocannon = require('autocannon') -const axios = require('axios') -const chalk = require('chalk') -const getPort = require('get-port') -const Table = require('cli-table3') -const URL = require('url').URL -const { spawn } = require('child_process') - -main() - -async function main () { - try { - const disabled = await run(false) - const enabled = await run(true) - - compare(disabled, enabled) - } catch (e) { - console.error(e) - process.exit(1) - } -} - -async function run (profilerEnabled) { - const port = await getPort() - const url = new URL(`http://localhost:${port}/hello`) - const server = await createServer(profilerEnabled, url) - - title(`Benchmark (enabled=${profilerEnabled})`) - - await getUsage(url) - - const net = await benchmark(url.href, 15000) - const cpu = await getUsage(url) - - server.kill('SIGINT') - - return { cpu, net } -} - -function benchmark (url, maxConnectionRequests) { - return new Promise((resolve, reject) => { - const duration = maxConnectionRequests * 2 / 1000 - const instance = autocannon({ duration, maxConnectionRequests, url }, (err, result) => { - err ? reject(err) : resolve(result) - }) - - process.once('SIGINT', () => { - instance.stop() - }) - - autocannon.track(instance, { - renderResultsTable: true, - renderProgressBar: false - }) - }) -} - -function compare (result1, result2) { - title('Comparison (disabled VS enabled)') - - compareNet(result1.net, result2.net) - compareCpu(result1.cpu, result2.cpu) -} - -function compareNet (result1, result2) { - const shortLatency = new Table({ - head: asColor(chalk.cyan, ['Stat', '2.5%', '50%', '97.5%', '99%', 'Avg', 'Max']) - }) - - shortLatency.push(asLowRow(chalk.bold('Latency'), asDiff(result1.latency, result2.latency))) - - console.log(shortLatency.toString()) - - const requests = new Table({ - head: asColor(chalk.cyan, ['Stat', '1%', '2.5%', '50%', '97.5%', 'Avg', 'Min']) - }) - - requests.push(asHighRow(chalk.bold('Req/Sec'), asDiff(result1.requests, result2.requests, true))) - requests.push(asHighRow(chalk.bold('Bytes/Sec'), asDiff(result1.throughput, result2.throughput, true))) - - console.log(requests.toString()) -} - -function compareCpu (result1, result2) { - const cpuTime = new Table({ - head: asColor(chalk.cyan, ['Stat', 'User', 'System', 'Process']) - }) - - cpuTime.push(asTimeRow(chalk.bold('CPU Time'), asDiff(result1, result2))) - - console.log(cpuTime.toString()) -} - -function waitOn ({ interval = 250, timeout, resources }) { - return Promise.all(resources.map(resource => { - return new Promise((resolve, reject) => { - let intervalTimer - const timeoutTimer = timeout && setTimeout(() => { - reject(new Error('Timeout.')) - clearTimeout(timeoutTimer) - clearTimeout(intervalTimer) - }, timeout) - - function waitOnResource () { - if (timeout && !timeoutTimer) return - - axios.get(resource) - .then(() => { - resolve() - clearTimeout(timeoutTimer) - clearTimeout(intervalTimer) - }) - .catch(() => { - intervalTimer = setTimeout(waitOnResource, interval) - }) - } - - waitOnResource() - }) - })) -} - -async function createServer (profilerEnabled, url) { - const server = spawn(process.execPath, ['server'], { - cwd: __dirname, - env: { - DD_PROFILING_ENABLED: String(profilerEnabled), - PORT: url.port - } - }) - - process.once('SIGINT', () => { - server.kill('SIGINT') - }) - - await waitOn({ - timeout: 5000, - resources: [url.href] - }) - - return server -} - -async function getUsage (url) { - const response = await axios.get(`${url.origin}/usage`) - const usage = response.data - - usage.process = usage.user + usage.system - - return usage -} - -function asColor (colorise, row) { - return row.map((entry) => colorise(entry)) -} - -function asDiff (stat1, stat2, reverse = false) { - const result = Object.create(null) - - Object.keys(stat1).forEach((k) => { - if (stat2[k] === stat1[k]) return (result[k] = '0%') - if (stat1[k] === 0) return (result[k] = '+∞%') - if (stat2[k] === 0) return (result[k] = '-∞%') - - const fraction = stat2[k] / stat1[k] - const percent = Math.round(fraction * 100) - 100 - const value = `${withSign(percent)}%` - - if (percent > 0) { - result[k] = reverse ? chalk.green(value) : chalk.red(value) - } else if (percent < 0) { - result[k] = reverse ? chalk.red(value) : chalk.green(value) - } else { - result[k] = value - } - }) - - return result -} - -function asLowRow (name, stat) { - return [ - name, - stat.p2_5, - stat.p50, - stat.p97_5, - stat.p99, - stat.average, - typeof stat.max === 'string' ? stat.max : Math.floor(stat.max * 100) / 100 - ] -} - -function asHighRow (name, stat) { - return [ - name, - stat.p1, - stat.p2_5, - stat.p50, - stat.p97_5, - stat.average, - typeof stat.min === 'string' ? stat.min : Math.floor(stat.min * 100) / 100 - ] -} - -function asTimeRow (name, stat) { - return [ - name, - stat.user, - stat.system, - stat.process - ] -} - -function withSign (value) { - return value < 0 ? `${value}` : `+${value}` -} - -function title (str) { - const line = ''.padStart(str.length, '=') - - console.log('') - console.log(line) - console.log(str) - console.log(line) - console.log('') -} diff --git a/benchmark/profiler/server.js b/benchmark/profiler/server.js deleted file mode 100644 index cf190e40eed..00000000000 --- a/benchmark/profiler/server.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict' - -require('dotenv').config() -require('../..').init({ enabled: false }) - -const express = require('express') - -const app = express() - -let usage - -app.get('/hello', (req, res) => { - res.status(200).send('Hello World!') -}) - -app.get('/usage', (req, res) => { - const diff = process.cpuUsage(usage) - - usage = process.cpuUsage() - - res.status(200).send(diff) -}) - -app.listen(process.env.PORT || 8080, '127.0.0.1', () => { - usage = process.cpuUsage() -}) diff --git a/package.json b/package.json index 7a5149e2533..94b0114f651 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "env": "bash ./plugin-env", "preinstall": "node scripts/preinstall.js", "bench": "node benchmark", - "bench:profiler": "node benchmark/profiler", "bench:e2e": "SERVICES=mongo yarn services && cd benchmark/e2e && node benchmark-run.js --duration=30", "bench:e2e:ci-visibility": "node benchmark/e2e-ci/benchmark-run.js", "type:doc": "cd docs && yarn && yarn build", From 9eb118040957ea293d9d4ffbc340a43d9ce4c57a Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Fri, 6 Dec 2024 13:35:19 -0500 Subject: [PATCH 25/61] fix guardrail on node version outside of ssi (#4974) --- .github/workflows/plugins.yml | 2 ++ .github/workflows/project.yml | 5 +-- integration-tests/helpers/index.js | 2 +- integration-tests/init.spec.js | 18 ++++++----- packages/dd-trace/src/guardrails/index.js | 37 +++++++++++------------ 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 5e1c3ac3017..d25535e2aab 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -294,6 +294,7 @@ jobs: PLUGINS: couchbase SERVICES: couchbase PACKAGE_VERSION_RANGE: ${{ matrix.range }} + DD_INJECT_FORCE: 'true' steps: - uses: actions/checkout@v4 - uses: ./.github/actions/testagent/start @@ -828,6 +829,7 @@ jobs: PLUGINS: oracledb SERVICES: oracledb DD_TEST_AGENT_URL: http://testagent:9126 + DD_INJECT_FORCE: 'true' # Needed to fix issue with `actions/checkout@v3: https://github.com/actions/checkout/issues/1590 ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true steps: diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index c58392833d2..f7839ac941e 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -49,14 +49,15 @@ jobs: matrix: version: ['0.8', '0.10', '0.12', '4', '6', '8', '10', '12.0.0'] runs-on: ubuntu-latest - env: - DD_INJECTION_ENABLED: 'true' steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.version }} - run: node ./init + - run: node ./init + env: + DD_INJECTION_ENABLED: 'true' integration-ci: strategy: diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index 09cc6c5bee4..22074c3af20 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -306,7 +306,7 @@ async function spawnPluginIntegrationTestProc (cwd, serverFile, agentPort, stdio NODE_OPTIONS: `--loader=${hookFile}`, DD_TRACE_AGENT_PORT: agentPort } - env = { ...env, ...additionalEnvArgs } + env = { ...process.env, ...env, ...additionalEnvArgs } return spawnProc(path.join(cwd, serverFile), { cwd, env diff --git a/integration-tests/init.spec.js b/integration-tests/init.spec.js index 03a17d5f4c7..fc274fb1480 100644 --- a/integration-tests/init.spec.js +++ b/integration-tests/init.spec.js @@ -34,12 +34,14 @@ function testInjectionScenarios (arg, filename, esmWorks = false) { const NODE_OPTIONS = `--no-warnings --${arg} ${path.join(__dirname, '..', filename)}` useEnv({ NODE_OPTIONS }) - context('without DD_INJECTION_ENABLED', () => { - it('should initialize the tracer', () => doTest('init/trace.js', 'true\n')) - it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n')) - it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => - doTest('init/instrument.mjs', `${esmWorks}\n`)) - }) + if (currentVersionIsSupported) { + context('without DD_INJECTION_ENABLED', () => { + it('should initialize the tracer', () => doTest('init/trace.js', 'true\n')) + it('should initialize instrumentation', () => doTest('init/instrument.js', 'true\n')) + it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation`, () => + doTest('init/instrument.mjs', `${esmWorks}\n`)) + }) + } context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) @@ -87,8 +89,8 @@ function testRuntimeVersionChecks (arg, filename) { context('when node version is less than engines field', () => { useEnv({ NODE_OPTIONS }) - it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => - doTest('true\n')) + it('should not initialize the tracer', () => + doTest('false\n')) context('with DD_INJECTION_ENABLED', () => { useEnv({ DD_INJECTION_ENABLED }) diff --git a/packages/dd-trace/src/guardrails/index.js b/packages/dd-trace/src/guardrails/index.js index 249b9343a39..179262f154e 100644 --- a/packages/dd-trace/src/guardrails/index.js +++ b/packages/dd-trace/src/guardrails/index.js @@ -11,14 +11,16 @@ var nodeVersion = require('../../../../version') var NODE_MAJOR = nodeVersion.NODE_MAJOR -// TODO: Test telemetry for Node <12. For now only bailout is tested for those. function guard (fn) { var initBailout = false var clobberBailout = false var forced = isTrue(process.env.DD_INJECT_FORCE) + var engines = require('../../../../package.json').engines + var minMajor = parseInt(engines.node.replace(/[^0-9]/g, '')) + var version = process.versions.node if (process.env.DD_INJECTION_ENABLED) { - // If we're running via single-step install, and we're not in the app's + // If we're running via single-step install, and we're in the app's // node_modules, then we should not initialize the tracer. This prevents // single-step-installed tracer from clobbering the manually-installed tracer. var resolvedInApp @@ -34,25 +36,20 @@ function guard (fn) { clobberBailout = true } } + } - // If we're running via single-step install, and the runtime doesn't match - // the engines field in package.json, then we should not initialize the tracer. - if (!clobberBailout) { - var engines = require('../../../../package.json').engines - var minMajor = parseInt(engines.node.replace(/[^0-9]/g, '')) - var version = process.versions.node - if (NODE_MAJOR < minMajor) { - initBailout = true - telemetry([ - { name: 'abort', tags: ['reason:incompatible_runtime'] }, - { name: 'abort.runtime', tags: [] } - ]) - log.info('Aborting application instrumentation due to incompatible_runtime.') - log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') - if (forced) { - log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') - } - } + // If the runtime doesn't match the engines field in package.json, then we + // should not initialize the tracer. + if (!clobberBailout && NODE_MAJOR < minMajor) { + initBailout = true + telemetry([ + { name: 'abort', tags: ['reason:incompatible_runtime'] }, + { name: 'abort.runtime', tags: [] } + ]) + log.info('Aborting application instrumentation due to incompatible_runtime.') + log.info('Found incompatible runtime nodejs ' + version + ', Supported runtimes: nodejs ' + engines.node + '.') + if (forced) { + log.info('DD_INJECT_FORCE enabled, allowing unsupported runtimes and continuing.') } } From af176d1ead77361ea1ef07ac2fe20629a3c98b28 Mon Sep 17 00:00:00 2001 From: Ida Liu <119438987+ida613@users.noreply.github.com> Date: Sun, 8 Dec 2024 13:59:31 -0500 Subject: [PATCH 26/61] make sampling rule matching case insensitive (#4972) --- packages/dd-trace/src/util.js | 2 ++ packages/dd-trace/test/sampling_rule.spec.js | 24 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/dd-trace/src/util.js b/packages/dd-trace/src/util.js index e4aa29c076c..5259a43ed60 100644 --- a/packages/dd-trace/src/util.js +++ b/packages/dd-trace/src/util.js @@ -25,6 +25,8 @@ function isError (value) { // Matches a glob pattern to a given subject string function globMatch (pattern, subject) { + if (typeof pattern === 'string') pattern = pattern.toLowerCase() + if (typeof subject === 'string') subject = subject.toLowerCase() let px = 0 // [p]attern inde[x] let sx = 0 // [s]ubject inde[x] let nextPx = 0 diff --git a/packages/dd-trace/test/sampling_rule.spec.js b/packages/dd-trace/test/sampling_rule.spec.js index 49ce1153d2e..609afe385ec 100644 --- a/packages/dd-trace/test/sampling_rule.spec.js +++ b/packages/dd-trace/test/sampling_rule.spec.js @@ -120,6 +120,30 @@ describe('sampling rule', () => { expect(rule.match(spans[10])).to.equal(false) }) + it('should match with case-insensitive strings', () => { + const lowerCaseRule = new SamplingRule({ + service: 'test', + name: 'operation' + }) + + const mixedCaseRule = new SamplingRule({ + service: 'teSt', + name: 'oPeration' + }) + + expect(lowerCaseRule.match(spans[0])).to.equal(mixedCaseRule.match(spans[0])) + expect(lowerCaseRule.match(spans[1])).to.equal(mixedCaseRule.match(spans[1])) + expect(lowerCaseRule.match(spans[2])).to.equal(mixedCaseRule.match(spans[2])) + expect(lowerCaseRule.match(spans[3])).to.equal(mixedCaseRule.match(spans[3])) + expect(lowerCaseRule.match(spans[4])).to.equal(mixedCaseRule.match(spans[4])) + expect(lowerCaseRule.match(spans[5])).to.equal(mixedCaseRule.match(spans[5])) + expect(lowerCaseRule.match(spans[6])).to.equal(mixedCaseRule.match(spans[6])) + expect(lowerCaseRule.match(spans[7])).to.equal(mixedCaseRule.match(spans[7])) + expect(lowerCaseRule.match(spans[8])).to.equal(mixedCaseRule.match(spans[8])) + expect(lowerCaseRule.match(spans[9])).to.equal(mixedCaseRule.match(spans[9])) + expect(lowerCaseRule.match(spans[10])).to.equal(mixedCaseRule.match(spans[10])) + }) + it('should match with regexp', () => { rule = new SamplingRule({ service: /test/, From 8384ba437dad7ea66d2d48f8c39927a0a6344b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Tue, 10 Dec 2024 10:52:43 +0100 Subject: [PATCH 27/61] =?UTF-8?q?[test=20optimization]=C2=A0Fix=20test=20n?= =?UTF-8?q?ame=20extraction=20in=20playwright=20(#4981)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playwright-tests/landing-page-test.js | 53 ++++++++------- .../playwright/playwright.spec.js | 65 ++++++++++--------- .../src/playwright.js | 17 ++++- 3 files changed, 79 insertions(+), 56 deletions(-) diff --git a/integration-tests/ci-visibility/playwright-tests/landing-page-test.js b/integration-tests/ci-visibility/playwright-tests/landing-page-test.js index 4e05a904176..7ee22886c7b 100644 --- a/integration-tests/ci-visibility/playwright-tests/landing-page-test.js +++ b/integration-tests/ci-visibility/playwright-tests/landing-page-test.js @@ -4,29 +4,34 @@ test.beforeEach(async ({ page }) => { await page.goto(process.env.PW_BASE_URL) }) -test.describe('playwright', () => { - test('should work with passing tests', async ({ page }) => { - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello World' - ]) - }) - test.skip('should work with skipped tests', async ({ page }) => { - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello World' - ]) - }) - test.fixme('should work with fixme', async ({ page }) => { - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello Warld' - ]) - }) - test('should work with annotated tests', async ({ page }) => { - test.info().annotations.push({ type: 'DD_TAGS[test.memory.usage]', description: 'low' }) - test.info().annotations.push({ type: 'DD_TAGS[test.memory.allocations]', description: 16 }) - // this is malformed and should be ignored - test.info().annotations.push({ type: 'DD_TAGS[test.invalid', description: 'high' }) - await expect(page.locator('.hello-world')).toHaveText([ - 'Hello World' - ]) +test.describe('highest-level-describe', () => { + test.describe(' leading and trailing spaces ', () => { + // even empty describe blocks should be allowed + test.describe(' ', () => { + test('should work with passing tests', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) + test.skip('should work with skipped tests', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) + test.fixme('should work with fixme', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello Warld' + ]) + }) + test('should work with annotated tests', async ({ page }) => { + test.info().annotations.push({ type: 'DD_TAGS[test.memory.usage]', description: 'low' }) + test.info().annotations.push({ type: 'DD_TAGS[test.memory.allocations]', description: 16 }) + // this is malformed and should be ignored + test.info().annotations.push({ type: 'DD_TAGS[test.invalid', description: 'high' }) + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) + }) }) }) diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index 440cf13d637..3f6a49e01b7 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -123,11 +123,15 @@ versions.forEach((version) => { }) assert.includeMembers(testEvents.map(test => test.content.resource), [ - 'landing-page-test.js.should work with passing tests', - 'landing-page-test.js.should work with skipped tests', - 'landing-page-test.js.should work with fixme', - 'landing-page-test.js.should work with annotated tests', - 'todo-list-page-test.js.should work with failing tests', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with passing tests', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with skipped tests', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with fixme', + 'landing-page-test.js.highest-level-describe' + + ' leading and trailing spaces should work with annotated tests', + 'todo-list-page-test.js.playwright should work with failing tests', 'todo-list-page-test.js.should work with fixme root' ]) @@ -155,7 +159,7 @@ versions.forEach((version) => { assert.property(stepEvent.content.meta, 'playwright.step') }) const annotatedTest = testEvents.find(test => - test.content.resource === 'landing-page-test.js.should work with annotated tests' + test.content.resource.endsWith('should work with annotated tests') ) assert.propertyVal(annotatedTest.content.meta, 'test.memory.usage', 'low') @@ -187,8 +191,8 @@ versions.forEach((version) => { const events = payloads.flatMap(({ payload }) => payload.events) const testEvents = events.filter(event => event.type === 'test') assert.includeMembers(testEvents.map(test => test.content.resource), [ - 'playwright-tests-ts/one-test.js.should work with passing tests', - 'playwright-tests-ts/one-test.js.should work with skipped tests' + 'playwright-tests-ts/one-test.js.playwright should work with passing tests', + 'playwright-tests-ts/one-test.js.playwright should work with skipped tests' ]) assert.include(testOutput, '1 passed') assert.include(testOutput, '1 skipped') @@ -263,16 +267,17 @@ versions.forEach((version) => { { playwright: { 'landing-page-test.js': [ - // 'should work with passing tests', // it will be considered new - 'should work with skipped tests', - 'should work with fixme', - 'should work with annotated tests' + // it will be considered new + // 'highest-level-describe leading and trailing spaces should work with passing tests', + 'highest-level-describe leading and trailing spaces should work with skipped tests', + 'highest-level-describe leading and trailing spaces should work with fixme', + 'highest-level-describe leading and trailing spaces should work with annotated tests' ], 'skipped-suite-test.js': [ 'should work with fixme root' ], 'todo-list-page-test.js': [ - 'should work with failing tests', + 'playwright should work with failing tests', 'should work with fixme root' ] } @@ -288,8 +293,7 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter(test => - test.resource === - 'landing-page-test.js.should work with passing tests' + test.resource.endsWith('should work with passing tests') ) newTests.forEach(test => { assert.propertyVal(test.meta, TEST_IS_NEW, 'true') @@ -337,16 +341,17 @@ versions.forEach((version) => { { playwright: { 'landing-page-test.js': [ - // 'should work with passing tests', // it will be considered new - 'should work with skipped tests', - 'should work with fixme', - 'should work with annotated tests' + // it will be considered new + // 'highest-level-describe leading and trailing spaces should work with passing tests', + 'highest-level-describe leading and trailing spaces should work with skipped tests', + 'highest-level-describe leading and trailing spaces should work with fixme', + 'highest-level-describe leading and trailing spaces should work with annotated tests' ], 'skipped-suite-test.js': [ 'should work with fixme root' ], 'todo-list-page-test.js': [ - 'should work with failing tests', + 'playwright should work with failing tests', 'should work with fixme root' ] } @@ -359,8 +364,7 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter(test => - test.resource === - 'landing-page-test.js.should work with passing tests' + test.resource.endsWith('should work with passing tests') ) newTests.forEach(test => { assert.notProperty(test.meta, TEST_IS_NEW) @@ -406,16 +410,18 @@ versions.forEach((version) => { { playwright: { 'landing-page-test.js': [ - 'should work with passing tests', - // 'should work with skipped tests', // new but not retried because it's skipped - // 'should work with fixme', // new but not retried because it's skipped - 'should work with annotated tests' + 'highest-level-describe leading and trailing spaces should work with passing tests', + // new but not retried because it's skipped + // 'highest-level-describe leading and trailing spaces should work with skipped tests', + // new but not retried because it's skipped + // 'highest-level-describe leading and trailing spaces should work with fixme', + 'highest-level-describe leading and trailing spaces should work with annotated tests' ], 'skipped-suite-test.js': [ 'should work with fixme root' ], 'todo-list-page-test.js': [ - 'should work with failing tests', + 'playwright should work with failing tests', 'should work with fixme root' ] } @@ -428,9 +434,8 @@ versions.forEach((version) => { const tests = events.filter(event => event.type === 'test').map(event => event.content) const newTests = tests.filter(test => - test.resource === - 'landing-page-test.js.should work with skipped tests' || - test.resource === 'landing-page-test.js.should work with fixme' + test.resource.endsWith('should work with skipped tests') || + test.resource.endsWith('should work with fixme') ) // no retries assert.equal(newTests.length, 2) diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index e8332d65c8d..ecc5f61521e 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -47,7 +47,7 @@ function isNewTest (test) { const testSuite = getTestSuitePath(test._requireFile, rootDir) const testsForSuite = knownTests?.playwright?.[testSuite] || [] - return !testsForSuite.includes(test.title) + return !testsForSuite.includes(getTestFullname(test)) } function getSuiteType (test, type) { @@ -224,10 +224,21 @@ function testWillRetry (test, testStatus) { return testStatus === 'fail' && test.results.length <= test.retries } +function getTestFullname (test) { + let parent = test.parent + const names = [test.title] + while (parent?._type === 'describe' || parent?._isDescribe) { + if (parent.title) { + names.unshift(parent.title) + } + parent = parent.parent + } + return names.join(' ') +} + function testBeginHandler (test, browserName) { const { _requireFile: testSuiteAbsolutePath, - title: testName, _type, location: { line: testSourceLine @@ -238,6 +249,8 @@ function testBeginHandler (test, browserName) { return } + const testName = getTestFullname(test) + const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath) if (isNewTestSuite) { From 4e9b1ffa7ddb867292f13009e3af68eead4f8cc9 Mon Sep 17 00:00:00 2001 From: Ugaitz Urien Date: Tue, 10 Dec 2024 14:09:40 +0100 Subject: [PATCH 28/61] Force update of nanoid to 3.3.8 (#4986) --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2eaf99af6fc..c5982d831bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3623,7 +3623,7 @@ mocha@^9: log-symbols "4.1.0" minimatch "4.2.1" ms "2.1.3" - nanoid "3.3.1" + nanoid "3.3.8" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" @@ -3681,10 +3681,10 @@ multer@^1.4.5-lts.1: type-is "^1.6.4" xtend "^4.0.0" -nanoid@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@3.3.8: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== natural-compare@^1.4.0: version "1.4.0" From b04ced437aad31d5d2f472d184b786872e2a93cf Mon Sep 17 00:00:00 2001 From: ishabi Date: Tue, 10 Dec 2024 16:47:02 +0100 Subject: [PATCH 29/61] Express 5 Instrumentation (#4913) Co-authored-by: William Conti Co-authored-by: simon-id --- .../datadog-instrumentations/src/express.js | 42 ++++- .../src/helpers/hooks.js | 1 - packages/datadog-instrumentations/src/qs.js | 24 --- .../datadog-instrumentations/src/router.js | 98 ++++++++++- .../test/express.spec.js | 2 +- .../datadog-plugin-express/test/index.spec.js | 154 +++++++++++++----- .../test/integration-test/client.spec.js | 4 +- .../datadog-plugin-router/test/index.spec.js | 5 +- packages/dd-trace/src/appsec/channels.js | 1 + .../src/appsec/iast/taint-tracking/plugin.js | 18 +- packages/dd-trace/src/appsec/index.js | 10 +- ...cker-fingerprinting.express.plugin.spec.js | 108 ++++++------ ...yzer.express-mongo-sanitize.plugin.spec.js | 3 +- ...n-mongodb-analyzer.mongoose.plugin.spec.js | 16 +- ...ion-mongodb-analyzer.mquery.plugin.spec.js | 5 +- .../appsec/iast/taint-tracking/plugin.spec.js | 40 +++-- .../taint-tracking.express.plugin.spec.js | 15 +- .../test/appsec/index.express.plugin.spec.js | 12 +- packages/dd-trace/test/appsec/index.spec.js | 4 + .../appsec/rasp/lfi.express.plugin.spec.js | 2 +- packages/dd-trace/test/plugins/externals.json | 4 +- 21 files changed, 397 insertions(+), 171 deletions(-) delete mode 100644 packages/datadog-instrumentations/src/qs.js diff --git a/packages/datadog-instrumentations/src/express.js b/packages/datadog-instrumentations/src/express.js index 74e159fb042..1b328ba4c13 100644 --- a/packages/datadog-instrumentations/src/express.js +++ b/packages/datadog-instrumentations/src/express.js @@ -59,8 +59,6 @@ function wrapResponseRender (render) { addHook({ name: 'express', versions: ['>=4'] }, express => { shimmer.wrap(express.application, 'handle', wrapHandle) - shimmer.wrap(express.Router, 'use', wrapRouterMethod) - shimmer.wrap(express.Router, 'route', wrapRouterMethod) shimmer.wrap(express.response, 'json', wrapResponseJson) shimmer.wrap(express.response, 'jsonp', wrapResponseJson) @@ -69,6 +67,20 @@ addHook({ name: 'express', versions: ['>=4'] }, express => { return express }) +addHook({ name: 'express', versions: ['4'] }, express => { + shimmer.wrap(express.Router, 'use', wrapRouterMethod) + shimmer.wrap(express.Router, 'route', wrapRouterMethod) + + return express +}) + +addHook({ name: 'express', versions: ['>=5.0.0'] }, express => { + shimmer.wrap(express.Router.prototype, 'use', wrapRouterMethod) + shimmer.wrap(express.Router.prototype, 'route', wrapRouterMethod) + + return express +}) + const queryParserReadCh = channel('datadog:query:read:finish') function publishQueryParsedAndNext (req, res, next) { @@ -88,7 +100,7 @@ function publishQueryParsedAndNext (req, res, next) { addHook({ name: 'express', - versions: ['>=4'], + versions: ['4'], file: 'lib/middleware/query.js' }, query => { return shimmer.wrapFunction(query, query => function () { @@ -129,7 +141,29 @@ addHook({ name: 'express', versions: ['>=4.0.0 <4.3.0'] }, express => { return express }) -addHook({ name: 'express', versions: ['>=4.3.0'] }, express => { +addHook({ name: 'express', versions: ['>=4.3.0 <5.0.0'] }, express => { shimmer.wrap(express.Router, 'process_params', wrapProcessParamsMethod(2)) return express }) + +const queryReadCh = channel('datadog:express:query:finish') + +addHook({ name: 'express', file: ['lib/request.js'], versions: ['>=5.0.0'] }, request => { + const requestDescriptor = Object.getOwnPropertyDescriptor(request, 'query') + + shimmer.wrap(requestDescriptor, 'get', function (originalGet) { + return function wrappedGet () { + const query = originalGet.apply(this, arguments) + + if (queryReadCh.hasSubscribers && query) { + queryReadCh.publish({ query }) + } + + return query + } + }) + + Object.defineProperty(request, 'query', requestDescriptor) + + return request +}) diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 4261d4dae44..4ea35f50218 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -111,7 +111,6 @@ module.exports = { protobufjs: () => require('../protobufjs'), pug: () => require('../pug'), q: () => require('../q'), - qs: () => require('../qs'), redis: () => require('../redis'), restify: () => require('../restify'), rhea: () => require('../rhea'), diff --git a/packages/datadog-instrumentations/src/qs.js b/packages/datadog-instrumentations/src/qs.js deleted file mode 100644 index 3901f61b169..00000000000 --- a/packages/datadog-instrumentations/src/qs.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -const { addHook, channel } = require('./helpers/instrument') -const shimmer = require('../../datadog-shimmer') - -const qsParseCh = channel('datadog:qs:parse:finish') - -function wrapParse (originalParse) { - return function () { - const qsParsedObj = originalParse.apply(this, arguments) - if (qsParseCh.hasSubscribers && qsParsedObj) { - qsParseCh.publish({ qs: qsParsedObj }) - } - return qsParsedObj - } -} - -addHook({ - name: 'qs', - versions: ['>=1'] -}, qs => { - shimmer.wrap(qs, 'parse', wrapParse) - return qs -}) diff --git a/packages/datadog-instrumentations/src/router.js b/packages/datadog-instrumentations/src/router.js index 00fbb6cec1a..bc9ff6152e5 100644 --- a/packages/datadog-instrumentations/src/router.js +++ b/packages/datadog-instrumentations/src/router.js @@ -169,11 +169,107 @@ function createWrapRouterMethod (name) { const wrapRouterMethod = createWrapRouterMethod('router') -addHook({ name: 'router', versions: ['>=1'] }, Router => { +addHook({ name: 'router', versions: ['>=1 <2'] }, Router => { shimmer.wrap(Router.prototype, 'use', wrapRouterMethod) shimmer.wrap(Router.prototype, 'route', wrapRouterMethod) return Router }) +const queryParserReadCh = channel('datadog:query:read:finish') + +addHook({ name: 'router', versions: ['>=2'] }, Router => { + const WrappedRouter = shimmer.wrapFunction(Router, function (originalRouter) { + return function wrappedMethod () { + const router = originalRouter.apply(this, arguments) + + shimmer.wrap(router, 'handle', function wrapHandle (originalHandle) { + return function wrappedHandle (req, res, next) { + const abortController = new AbortController() + + if (queryParserReadCh.hasSubscribers && req) { + queryParserReadCh.publish({ req, res, query: req.query, abortController }) + + if (abortController.signal.aborted) return + } + + return originalHandle.apply(this, arguments) + } + }) + + return router + } + }) + + shimmer.wrap(WrappedRouter.prototype, 'use', wrapRouterMethod) + shimmer.wrap(WrappedRouter.prototype, 'route', wrapRouterMethod) + + return WrappedRouter +}) + +const routerParamStartCh = channel('datadog:router:param:start') +const visitedParams = new WeakSet() + +function wrapHandleRequest (original) { + return function wrappedHandleRequest (req, res, next) { + if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) { + visitedParams.add(req.params) + + const abortController = new AbortController() + + routerParamStartCh.publish({ + req, + res, + params: req?.params, + abortController + }) + + if (abortController.signal.aborted) return + } + + return original.apply(this, arguments) + } +} + +addHook({ + name: 'router', file: 'lib/layer.js', versions: ['>=2'] +}, Layer => { + shimmer.wrap(Layer.prototype, 'handleRequest', wrapHandleRequest) + return Layer +}) + +function wrapParam (original) { + return function wrappedProcessParams () { + arguments[1] = shimmer.wrapFunction(arguments[1], (originalFn) => { + return function wrappedFn (req, res) { + if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) { + visitedParams.add(req.params) + + const abortController = new AbortController() + + routerParamStartCh.publish({ + req, + res, + params: req?.params, + abortController + }) + + if (abortController.signal.aborted) return + } + + return originalFn.apply(this, arguments) + } + }) + + return original.apply(this, arguments) + } +} + +addHook({ + name: 'router', versions: ['>=2'] +}, router => { + shimmer.wrap(router.prototype, 'param', wrapParam) + return router +}) + module.exports = { createWrapRouterMethod } diff --git a/packages/datadog-instrumentations/test/express.spec.js b/packages/datadog-instrumentations/test/express.spec.js index d21b9be3e0a..534bfd041e8 100644 --- a/packages/datadog-instrumentations/test/express.spec.js +++ b/packages/datadog-instrumentations/test/express.spec.js @@ -14,7 +14,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../versions/express').get() + const express = require(`../../../versions/express@${version}`).get() const app = express() app.get('/', (req, res) => { requestBody() diff --git a/packages/datadog-plugin-express/test/index.spec.js b/packages/datadog-plugin-express/test/index.spec.js index 55a608f4adf..8899c34ecb3 100644 --- a/packages/datadog-plugin-express/test/index.spec.js +++ b/packages/datadog-plugin-express/test/index.spec.js @@ -2,6 +2,7 @@ const { AsyncLocalStorage } = require('async_hooks') const axios = require('axios') +const semver = require('semver') const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') const agent = require('../../dd-trace/test/plugins/agent') const plugin = require('../src') @@ -214,34 +215,56 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const isExpress4 = semver.intersects(version, '<5.0.0') + let index = 0 + + const rootSpan = spans[index++] + expect(rootSpan).to.have.property('resource', 'GET /app/user/:id') + expect(rootSpan).to.have.property('name', 'express.request') + expect(rootSpan.meta).to.have.property('component', 'express') + + if (isExpress4) { + expect(spans[index]).to.have.property('resource', 'query') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + expect(spans[index]).to.have.property('resource', 'expressInit') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + } - expect(spans[0]).to.have.property('resource', 'GET /app/user/:id') - expect(spans[0]).to.have.property('name', 'express.request') - expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[1]).to.have.property('resource', 'query') - expect(spans[1]).to.have.property('name', 'express.middleware') - expect(spans[1].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[1].meta).to.have.property('component', 'express') - expect(spans[2]).to.have.property('resource', 'expressInit') - expect(spans[2]).to.have.property('name', 'express.middleware') - expect(spans[2].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[2].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('resource', 'named') - expect(spans[3]).to.have.property('name', 'express.middleware') - expect(spans[3].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[3].meta).to.have.property('component', 'express') - expect(spans[4]).to.have.property('resource', 'router') - expect(spans[4]).to.have.property('name', 'express.middleware') - expect(spans[4].parent_id.toString()).to.equal(spans[0].span_id.toString()) - expect(spans[4].meta).to.have.property('component', 'express') - expect(spans[5].resource).to.match(/^bound\s.*$/) - expect(spans[5]).to.have.property('name', 'express.middleware') - expect(spans[5].parent_id.toString()).to.equal(spans[4].span_id.toString()) - expect(spans[5].meta).to.have.property('component', 'express') - expect(spans[6]).to.have.property('resource', '') - expect(spans[6]).to.have.property('name', 'express.middleware') - expect(spans[6].parent_id.toString()).to.equal(spans[5].span_id.toString()) - expect(spans[6].meta).to.have.property('component', 'express') + expect(spans[index]).to.have.property('resource', 'named') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + expect(spans[index]).to.have.property('resource', 'router') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(rootSpan.span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + if (isExpress4) { + expect(spans[index].resource).to.match(/^bound\s.*$/) + } else { + expect(spans[index]).to.have.property('resource', 'handle') + } + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(spans[index - 1].span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + index++ + + expect(spans[index]).to.have.property('resource', '') + expect(spans[index]).to.have.property('name', 'express.middleware') + expect(spans[index].parent_id.toString()).to.equal(spans[index - 1].span_id.toString()) + expect(spans[index].meta).to.have.property('component', 'express') + + expect(index).to.equal(spans.length - 1) }) .then(done) .catch(done) @@ -277,12 +300,14 @@ describe('Plugin', () => { .use(traces => { const spans = sort(traces[0]) + const breakingSpanIndex = semver.intersects(version, '<5.0.0') ? 3 : 1 + expect(spans[0]).to.have.property('resource', 'GET /user/:id') expect(spans[0]).to.have.property('name', 'express.request') expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('resource', 'breaking') - expect(spans[3]).to.have.property('name', 'express.middleware') - expect(spans[3].meta).to.have.property('component', 'express') + expect(spans[breakingSpanIndex]).to.have.property('resource', 'breaking') + expect(spans[breakingSpanIndex]).to.have.property('name', 'express.middleware') + expect(spans[breakingSpanIndex].meta).to.have.property('component', 'express') }) .then(done) .catch(done) @@ -321,12 +346,13 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const errorSpanIndex = semver.intersects(version, '<5.0.0') ? 4 : 2 expect(spans[0]).to.have.property('name', 'express.request') - expect(spans[4]).to.have.property('name', 'express.middleware') - expect(spans[4].meta).to.have.property(ERROR_TYPE, error.name) + expect(spans[errorSpanIndex]).to.have.property('name', 'express.middleware') + expect(spans[errorSpanIndex].meta).to.have.property(ERROR_TYPE, error.name) expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[4].meta).to.have.property('component', 'express') + expect(spans[errorSpanIndex].meta).to.have.property('component', 'express') }) .then(done) .catch(done) @@ -398,14 +424,14 @@ describe('Plugin', () => { const router = express.Router() router.use('/', (req, res, next) => next()) - router.use('*', (req, res, next) => next()) + router.use('/*splat', (req, res, next) => next()) router.use('/bar', (req, res, next) => next()) router.use('/bar', (req, res, next) => { res.status(200).send() }) app.use('/', (req, res, next) => next()) - app.use('*', (req, res, next) => next()) + app.use('/*splat', (req, res, next) => next()) app.use('/foo/bar', (req, res, next) => next()) app.use('/foo', router) @@ -1138,17 +1164,18 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const secondErrorIndex = spans.length - 2 expect(spans[0]).to.have.property('error', 1) expect(spans[0].meta).to.have.property(ERROR_TYPE, error.name) expect(spans[0].meta).to.have.property(ERROR_MESSAGE, error.message) expect(spans[0].meta).to.have.property(ERROR_STACK, error.stack) expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('error', 1) - expect(spans[3].meta).to.have.property(ERROR_TYPE, error.name) - expect(spans[3].meta).to.have.property(ERROR_MESSAGE, error.message) - expect(spans[3].meta).to.have.property(ERROR_STACK, error.stack) - expect(spans[3].meta).to.have.property('component', 'express') + expect(spans[secondErrorIndex]).to.have.property('error', 1) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_TYPE, error.name) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_MESSAGE, error.message) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_STACK, error.stack) + expect(spans[secondErrorIndex].meta).to.have.property('component', 'express') }) .then(done) .catch(done) @@ -1175,16 +1202,17 @@ describe('Plugin', () => { agent .use(traces => { const spans = sort(traces[0]) + const secondErrorIndex = spans.length - 2 expect(spans[0]).to.have.property('error', 1) expect(spans[0].meta).to.have.property(ERROR_TYPE, error.name) expect(spans[0].meta).to.have.property(ERROR_MESSAGE, error.message) expect(spans[0].meta).to.have.property(ERROR_STACK, error.stack) expect(spans[0].meta).to.have.property('component', 'express') - expect(spans[3]).to.have.property('error', 1) - expect(spans[3].meta).to.have.property(ERROR_TYPE, error.name) - expect(spans[3].meta).to.have.property(ERROR_MESSAGE, error.message) - expect(spans[3].meta).to.have.property(ERROR_STACK, error.stack) + expect(spans[secondErrorIndex]).to.have.property('error', 1) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_TYPE, error.name) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_MESSAGE, error.message) + expect(spans[secondErrorIndex].meta).to.have.property(ERROR_STACK, error.stack) expect(spans[0].meta).to.have.property('component', 'express') }) .then(done) @@ -1199,6 +1227,11 @@ describe('Plugin', () => { }) it('should support capturing groups in routes', done => { + if (semver.intersects(version, '>=5.0.0')) { + this.skip && this.skip() // mocha allows dynamic skipping, tap does not + return done() + } + const app = express() app.get('/:path(*)', (req, res) => { @@ -1224,6 +1257,32 @@ describe('Plugin', () => { }) }) + it('should support wildcard path prefix matching in routes', done => { + const app = express() + + app.get('/*user', (req, res) => { + res.status(200).send() + }) + + appListener = app.listen(0, 'localhost', () => { + const port = appListener.address().port + + agent + .use(traces => { + const spans = sort(traces[0]) + + expect(spans[0]).to.have.property('resource', 'GET /*user') + expect(spans[0].meta).to.have.property('http.url', `http://localhost:${port}/user`) + }) + .then(done) + .catch(done) + + axios + .get(`http://localhost:${port}/user`) + .catch(done) + }) + }) + it('should keep the properties untouched on nested router handlers', () => { const router = express.Router() const childRouter = express.Router() @@ -1234,7 +1293,12 @@ describe('Plugin', () => { router.use('/users', childRouter) - const layer = router.stack.find(layer => layer.regexp.test('/users')) + const layer = router.stack.find(layer => { + if (semver.intersects(version, '>=5.0.0')) { + return layer.matchers.find(matcher => matcher('/users')) + } + return layer.regexp.test('/users') + }) expect(layer.handle).to.have.ownProperty('stack') }) diff --git a/packages/datadog-plugin-express/test/integration-test/client.spec.js b/packages/datadog-plugin-express/test/integration-test/client.spec.js index a5c08d60ecb..c13a4249892 100644 --- a/packages/datadog-plugin-express/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-express/test/integration-test/client.spec.js @@ -7,6 +7,7 @@ const { spawnPluginIntegrationTestProc } = require('../../../../integration-tests/helpers') const { assert } = require('chai') +const semver = require('semver') describe('esm', () => { let agent @@ -36,13 +37,14 @@ describe('esm', () => { it('is instrumented', async () => { proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'server.mjs', agent.port) + const numberOfSpans = semver.intersects(version, '<5.0.0') ? 4 : 3 return curlAndAssertMessage(agent, proc, ({ headers, payload }) => { assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) assert.isArray(payload) assert.strictEqual(payload.length, 1) assert.isArray(payload[0]) - assert.strictEqual(payload[0].length, 4) + assert.strictEqual(payload[0].length, numberOfSpans) assert.propertyVal(payload[0][0], 'name', 'express.request') assert.propertyVal(payload[0][1], 'name', 'express.middleware') }) diff --git a/packages/datadog-plugin-router/test/index.spec.js b/packages/datadog-plugin-router/test/index.spec.js index ac208f0e2a1..31c3cde8bf5 100644 --- a/packages/datadog-plugin-router/test/index.spec.js +++ b/packages/datadog-plugin-router/test/index.spec.js @@ -71,8 +71,9 @@ describe('Plugin', () => { }) router.use('/parent', childRouter) - expect(router.stack[0].handle.hello).to.equal('goodbye') - expect(router.stack[0].handle.foo).to.equal('bar') + const index = router.stack.length - 1 + expect(router.stack[index].handle.hello).to.equal('goodbye') + expect(router.stack[index].handle.foo).to.equal('bar') }) it('should add the route to the request span', done => { diff --git a/packages/dd-trace/src/appsec/channels.js b/packages/dd-trace/src/appsec/channels.js index 8e7f27211c6..1368e937dc9 100644 --- a/packages/dd-trace/src/appsec/channels.js +++ b/packages/dd-trace/src/appsec/channels.js @@ -19,6 +19,7 @@ module.exports = { nextBodyParsed: dc.channel('apm:next:body-parsed'), nextQueryParsed: dc.channel('apm:next:query-parsed'), expressProcessParams: dc.channel('datadog:express:process_params:start'), + routerParam: dc.channel('datadog:router:param:start'), responseBody: dc.channel('datadog:express:response:json:start'), responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'), httpClientRequestStart: dc.channel('apm:http:client:request:start'), diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js index ed46cbe5f2e..62fdd46d027 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js @@ -46,8 +46,13 @@ class TaintTrackingPlugin extends SourceIastPlugin { ) this.addSub( - { channelName: 'datadog:qs:parse:finish', tag: HTTP_REQUEST_PARAMETER }, - ({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs) + { channelName: 'datadog:query:read:finish', tag: HTTP_REQUEST_PARAMETER }, + ({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query) + ) + + this.addSub( + { channelName: 'datadog:express:query:finish', tag: HTTP_REQUEST_PARAMETER }, + ({ query }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, query) ) this.addSub( @@ -77,6 +82,15 @@ class TaintTrackingPlugin extends SourceIastPlugin { } ) + this.addSub( + { channelName: 'datadog:router:param:start', tag: HTTP_REQUEST_PATH_PARAM }, + ({ req }) => { + if (req && req.params !== null && typeof req.params === 'object') { + this._taintTrackingHandler(HTTP_REQUEST_PATH_PARAM, req, 'params') + } + } + ) + this.addSub( { channelName: 'apm:graphql:resolve:start', tag: HTTP_REQUEST_BODY }, (data) => { diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index 4748148a2de..d4f4adc6554 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -16,7 +16,8 @@ const { expressProcessParams, responseBody, responseWriteHead, - responseSetHeader + responseSetHeader, + routerParam } = require('./channels') const waf = require('./waf') const addresses = require('./addresses') @@ -67,6 +68,7 @@ function enable (_config) { nextBodyParsed.subscribe(onRequestBodyParsed) nextQueryParsed.subscribe(onRequestQueryParsed) expressProcessParams.subscribe(onRequestProcessParams) + routerParam.subscribe(onRequestProcessParams) responseBody.subscribe(onResponseBody) responseWriteHead.subscribe(onResponseWriteHead) responseSetHeader.subscribe(onResponseSetHeader) @@ -164,8 +166,9 @@ function incomingHttpEndTranslator ({ req, res }) { } // we need to keep this to support nextjs - if (req.query !== null && typeof req.query === 'object') { - persistent[addresses.HTTP_INCOMING_QUERY] = req.query + const query = req.query + if (query !== null && typeof query === 'object') { + persistent[addresses.HTTP_INCOMING_QUERY] = query } if (apiSecuritySampler.sampleRequest(req, res, true)) { @@ -310,6 +313,7 @@ function disable () { if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed) if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed) if (expressProcessParams.hasSubscribers) expressProcessParams.unsubscribe(onRequestProcessParams) + if (routerParam.hasSubscribers) routerParam.unsubscribe(onRequestProcessParams) if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody) if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead) if (responseSetHeader.hasSubscribers) responseSetHeader.unsubscribe(onResponseSetHeader) diff --git a/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js b/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js index bc7c918965c..112d634cca9 100644 --- a/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/attacker-fingerprinting.express.plugin.spec.js @@ -8,72 +8,74 @@ const agent = require('../plugins/agent') const appsec = require('../../src/appsec') const Config = require('../../src/config') -describe('Attacker fingerprinting', () => { - let port, server +withVersions('express', 'express', expressVersion => { + describe('Attacker fingerprinting', () => { + let port, server - before(() => { - return agent.load(['express', 'http'], { client: false }) - }) + before(() => { + return agent.load(['express', 'http'], { client: false }) + }) - before((done) => { - const express = require('../../../../versions/express').get() - const bodyParser = require('../../../../versions/body-parser').get() + before((done) => { + const express = require(`../../../../versions/express@${expressVersion}`).get() + const bodyParser = require('../../../../versions/body-parser').get() - const app = express() - app.use(bodyParser.json()) + const app = express() + app.use(bodyParser.json()) - app.post('/', (req, res) => { - res.end('DONE') - }) + app.post('/', (req, res) => { + res.end('DONE') + }) - server = app.listen(port, () => { - port = server.address().port - done() + server = app.listen(port, () => { + port = server.address().port + done() + }) }) - }) - after(() => { - server.close() - return agent.close({ ritmReset: false }) - }) + after(() => { + server.close() + return agent.close({ ritmReset: false }) + }) - beforeEach(() => { - appsec.enable(new Config( - { - appsec: { - enabled: true, - rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') + beforeEach(() => { + appsec.enable(new Config( + { + appsec: { + enabled: true, + rules: path.join(__dirname, 'attacker-fingerprinting-rules.json') + } } - } - )) - }) + )) + }) - afterEach(() => { - appsec.disable() - }) + afterEach(() => { + appsec.disable() + }) - it('should report http fingerprints', async () => { - await axios.post( - `http://localhost:${port}/?key=testattack`, - { - bodyParam: 'bodyValue' - }, - { - headers: { - headerName: 'headerValue', - 'x-real-ip': '255.255.255.255' + it('should report http fingerprints', async () => { + await axios.post( + `http://localhost:${port}/?key=testattack`, + { + bodyParam: 'bodyValue' + }, + { + headers: { + headerName: 'headerValue', + 'x-real-ip': '255.255.255.255' + } } - } - ) + ) - await agent.use((traces) => { - const span = traces[0][0] - assert.property(span.meta, '_dd.appsec.fp.http.header') - assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') - assert.property(span.meta, '_dd.appsec.fp.http.network') - assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') - assert.property(span.meta, '_dd.appsec.fp.http.endpoint') - assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') + await agent.use((traces) => { + const span = traces[0][0] + assert.property(span.meta, '_dd.appsec.fp.http.header') + assert.equal(span.meta['_dd.appsec.fp.http.header'], 'hdr-0110000110-6431a3e6-5-55682ec1') + assert.property(span.meta, '_dd.appsec.fp.http.network') + assert.equal(span.meta['_dd.appsec.fp.http.network'], 'net-1-0100000000') + assert.property(span.meta, '_dd.appsec.fp.http.endpoint') + assert.equal(span.meta['_dd.appsec.fp.http.endpoint'], 'http-post-8a5edab2-2c70e12b-be31090f') + }) }) }) }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js index e05537ce04b..f1042142100 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.express-mongo-sanitize.plugin.spec.js @@ -9,7 +9,8 @@ const { prepareTestServerForIastInExpress } = require('../utils') const agent = require('../../../plugins/agent') describe('nosql injection detection in mongodb - whole feature', () => { - withVersions('express', 'express', '>4.18.0', expressVersion => { + // https://github.com/fiznool/express-mongo-sanitize/issues/200 + withVersions('mongodb', 'express', '>4.18.0 <5.0.0', expressVersion => { withVersions('mongodb', 'mongodb', mongodbVersion => { const mongodb = require(`../../../../../../versions/mongodb@${mongodbVersion}`) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js index f09264225a9..75337c63b3f 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mongoose.plugin.spec.js @@ -10,7 +10,7 @@ const fs = require('fs') const { NODE_MAJOR } = require('../../../../../../version') describe('nosql injection detection in mongodb - whole feature', () => { - withVersions('express', 'express', '>4.18.0', expressVersion => { + withVersions('mongoose', 'express', expressVersion => { withVersions('mongoose', 'mongoose', '>4.0.0', mongooseVersion => { const specificMongooseVersion = require(`../../../../../../versions/mongoose@${mongooseVersion}`).version() if (NODE_MAJOR === 14 && semver.satisfies(specificMongooseVersion, '>=8')) return @@ -27,11 +27,16 @@ describe('nosql injection detection in mongodb - whole feature', () => { const dbName = id().toString() mongoose = require(`../../../../../../versions/mongoose@${mongooseVersion}`).get() - mongoose.connect(`mongodb://localhost:27017/${dbName}`, { + await mongoose.connect(`mongodb://localhost:27017/${dbName}`, { useNewUrlParser: true, useUnifiedTopology: true }) + if (mongoose.models.Test) { + delete mongoose.models?.Test + delete mongoose.modelSchemas?.Test + } + Test = mongoose.model('Test', { name: String }) const src = path.join(__dirname, 'resources', vulnerableMethodFilename) @@ -46,7 +51,12 @@ describe('nosql injection detection in mongodb - whole feature', () => { }) after(() => { - fs.unlinkSync(tmpFilePath) + try { + fs.unlinkSync(tmpFilePath) + } catch (e) { + // ignore the error + } + return mongoose.disconnect() }) diff --git a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js index 7cf71f7a86e..a91b428211c 100644 --- a/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.mquery.plugin.spec.js @@ -9,7 +9,8 @@ const semver = require('semver') const fs = require('fs') describe('nosql injection detection with mquery', () => { - withVersions('express', 'express', '>4.18.0', expressVersion => { + // https://github.com/fiznool/express-mongo-sanitize/issues/200 + withVersions('mongodb', 'express', '>4.18.0 <5.0.0', expressVersion => { withVersions('mongodb', 'mongodb', mongodbVersion => { const mongodb = require(`../../../../../../versions/mongodb@${mongodbVersion}`) @@ -316,7 +317,7 @@ describe('nosql injection detection with mquery', () => { withVersions('express-mongo-sanitize', 'express-mongo-sanitize', expressMongoSanitizeVersion => { prepareTestServerForIastInExpress('Test with sanitization middleware', expressVersion, (expressApp) => { const mongoSanitize = - require(`../../../../../../versions/express-mongo-sanitize@${expressMongoSanitizeVersion}`).get() + require(`../../../../../../versions/express-mongo-sanitize@${expressMongoSanitizeVersion}`).get() expressApp.use(mongoSanitize()) }, (testThatRequestHasVulnerability, testThatRequestHasNoVulnerability) => { testThatRequestHasNoVulnerability({ diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js index 1a21b0a5b08..5f9c4f4860f 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/plugin.spec.js @@ -12,10 +12,11 @@ const { } = require('../../../../src/appsec/iast/taint-tracking/source-types') const middlewareNextChannel = dc.channel('apm:express:middleware:next') -const queryParseFinishChannel = dc.channel('datadog:qs:parse:finish') +const queryReadFinishChannel = dc.channel('datadog:query:read:finish') const bodyParserFinishChannel = dc.channel('datadog:body-parser:read:finish') const cookieParseFinishCh = dc.channel('datadog:cookie:parse:finish') const processParamsStartCh = dc.channel('datadog:express:process_params:start') +const routerParamStartCh = dc.channel('datadog:router:param:start') describe('IAST Taint tracking plugin', () => { let taintTrackingPlugin @@ -42,16 +43,18 @@ describe('IAST Taint tracking plugin', () => { }) it('Should subscribe to body parser, qs, cookie and process_params channel', () => { - expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(9) + expect(taintTrackingPlugin._subscriptions).to.have.lengthOf(11) expect(taintTrackingPlugin._subscriptions[0]._channel.name).to.equals('datadog:body-parser:read:finish') expect(taintTrackingPlugin._subscriptions[1]._channel.name).to.equals('datadog:multer:read:finish') - expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('datadog:qs:parse:finish') - expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('apm:express:middleware:next') - expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('datadog:cookie:parse:finish') - expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('datadog:express:process_params:start') - expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('apm:graphql:resolve:start') - expect(taintTrackingPlugin._subscriptions[7]._channel.name).to.equals('datadog:url:parse:finish') - expect(taintTrackingPlugin._subscriptions[8]._channel.name).to.equals('datadog:url:getter:finish') + expect(taintTrackingPlugin._subscriptions[2]._channel.name).to.equals('datadog:query:read:finish') + expect(taintTrackingPlugin._subscriptions[3]._channel.name).to.equals('datadog:express:query:finish') + expect(taintTrackingPlugin._subscriptions[4]._channel.name).to.equals('apm:express:middleware:next') + expect(taintTrackingPlugin._subscriptions[5]._channel.name).to.equals('datadog:cookie:parse:finish') + expect(taintTrackingPlugin._subscriptions[6]._channel.name).to.equals('datadog:express:process_params:start') + expect(taintTrackingPlugin._subscriptions[7]._channel.name).to.equals('datadog:router:param:start') + expect(taintTrackingPlugin._subscriptions[8]._channel.name).to.equals('apm:graphql:resolve:start') + expect(taintTrackingPlugin._subscriptions[9]._channel.name).to.equals('datadog:url:parse:finish') + expect(taintTrackingPlugin._subscriptions[10]._channel.name).to.equals('datadog:url:getter:finish') }) describe('taint sources', () => { @@ -136,7 +139,7 @@ describe('IAST Taint tracking plugin', () => { } } - queryParseFinishChannel.publish({ qs: req.query }) + queryReadFinishChannel.publish({ query: req.query }) expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( iastContext, @@ -209,7 +212,7 @@ describe('IAST Taint tracking plugin', () => { ) }) - it('Should taint request params when process params event is published', () => { + it('Should taint request params when process params event is published with processParamsStartCh', () => { const req = { params: { parameter1: 'tainted1' @@ -224,6 +227,21 @@ describe('IAST Taint tracking plugin', () => { ) }) + it('Should taint request params when process params event is published with routerParamStartCh', () => { + const req = { + params: { + parameter1: 'tainted1' + } + } + + routerParamStartCh.publish({ req }) + expect(taintTrackingOperations.taintObject).to.be.calledOnceWith( + iastContext, + req.params, + HTTP_REQUEST_PATH_PARAM + ) + }) + it('Should not taint request params when process params event is published with non params request', () => { const req = {} diff --git a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js index 7465f6b2408..8fc32f1c03a 100644 --- a/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/iast/taint-tracking/sources/taint-tracking.express.plugin.spec.js @@ -46,9 +46,10 @@ describe('URI sourcing with express', () => { iast.disable() }) - it('should taint uri', done => { + it('should taint uri', (done) => { const app = express() - app.get('/path/*', (req, res) => { + const pathPattern = semver.intersects(version, '>=5.0.0') ? '/path/*splat' : '/path/*' + app.get(pathPattern, (req, res) => { const store = storage.getStore() const iastContext = iastContextFunctions.getIastContext(store) const isPathTainted = isTainted(iastContext, req.url) @@ -76,11 +77,11 @@ describe('Path params sourcing with express', () => { let appListener withVersions('express', 'express', version => { - const checkParamIsTaintedAndNext = (req, res, next, param) => { + const checkParamIsTaintedAndNext = (req, res, next, param, name) => { const store = storage.getStore() const iastContext = iastContextFunctions.getIastContext(store) - const pathParamValue = param + const pathParamValue = name ? req.params[name] : req.params const isParameterTainted = isTainted(iastContext, pathParamValue) expect(isParameterTainted).to.be.true const taintedParameterValueRanges = getRanges(iastContext, pathParamValue) @@ -188,8 +189,7 @@ describe('Path params sourcing with express', () => { res.status(200).send() }) - app.param('parameter1', checkParamIsTaintedAndNext) - app.param('parameter2', checkParamIsTaintedAndNext) + app.param(['parameter1', 'parameter2'], checkParamIsTaintedAndNext) appListener = app.listen(0, 'localhost', () => { const port = appListener.address().port @@ -202,6 +202,9 @@ describe('Path params sourcing with express', () => { }) it('should taint path param on router.params callback with custom implementation', function (done) { + if (!semver.satisfies(expressVersion, '4')) { + this.skip() + } const app = express() app.use('/:parameter1/:parameter2', (req, res) => { diff --git a/packages/dd-trace/test/appsec/index.express.plugin.spec.js b/packages/dd-trace/test/appsec/index.express.plugin.spec.js index bb674015f78..1c6a8aeb86d 100644 --- a/packages/dd-trace/test/appsec/index.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/index.express.plugin.spec.js @@ -19,7 +19,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../../versions/express').get() + const express = require(`../../../../versions/express@${version}`).get() const app = express() @@ -44,11 +44,7 @@ withVersions('express', 'express', version => { paramCallbackSpy = sinon.spy(paramCallback) - app.param(() => { - return paramCallbackSpy - }) - - app.param('callbackedParameter') + app.param('callbackedParameter', paramCallbackSpy) getPort().then((port) => { server = app.listen(port, () => { @@ -191,7 +187,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../../versions/express').get() + const express = require(`../../../../versions/express@${version}`).get() const app = express() @@ -256,7 +252,7 @@ withVersions('express', 'express', version => { }) before((done) => { - const express = require('../../../../versions/express').get() + const express = require(`../../../../versions/express@${version}`).get() const bodyParser = require('../../../../versions/body-parser').get() const app = express() diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 26a1c709cd9..4ec92f7b0e6 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -15,6 +15,7 @@ const { nextBodyParsed, nextQueryParsed, expressProcessParams, + routerParam, responseBody, responseWriteHead, responseSetHeader @@ -178,6 +179,7 @@ describe('AppSec Index', function () { expect(nextBodyParsed.hasSubscribers).to.be.false expect(nextQueryParsed.hasSubscribers).to.be.false expect(expressProcessParams.hasSubscribers).to.be.false + expect(routerParam.hasSubscribers).to.be.false expect(responseWriteHead.hasSubscribers).to.be.false expect(responseSetHeader.hasSubscribers).to.be.false @@ -190,6 +192,7 @@ describe('AppSec Index', function () { expect(nextBodyParsed.hasSubscribers).to.be.true expect(nextQueryParsed.hasSubscribers).to.be.true expect(expressProcessParams.hasSubscribers).to.be.true + expect(routerParam.hasSubscribers).to.be.true expect(responseWriteHead.hasSubscribers).to.be.true expect(responseSetHeader.hasSubscribers).to.be.true }) @@ -271,6 +274,7 @@ describe('AppSec Index', function () { expect(nextBodyParsed.hasSubscribers).to.be.false expect(nextQueryParsed.hasSubscribers).to.be.false expect(expressProcessParams.hasSubscribers).to.be.false + expect(routerParam.hasSubscribers).to.be.false expect(responseWriteHead.hasSubscribers).to.be.false expect(responseSetHeader.hasSubscribers).to.be.false }) diff --git a/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js b/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js index b5b825cc628..210c3849ece 100644 --- a/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js +++ b/packages/dd-trace/test/appsec/rasp/lfi.express.plugin.spec.js @@ -102,7 +102,7 @@ describe('RASP - lfi', () => { describe(description, () => { const getAppFn = options.getAppFn ?? getApp - it('should block param from the request', async () => { + it('should block param from the request', () => { app = getAppFn(fn, args, options) const file = args[vulnerableIndex] diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index 600df395d84..288fb9350c6 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -98,7 +98,7 @@ }, { "name": "express", - "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.3.0"] + "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.0.0 <5.0.0", ">=4.3.0 <5.0.0"] }, { "name": "body-parser", @@ -332,7 +332,7 @@ }, { "name": "express", - "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.3.0"] + "versions": [">=4", ">=4.0.0 <4.3.0", ">=4.0.0 <5.0.0", ">=4.3.0 <5.0.0"] }, { "name": "body-parser", From ea3ab7d23cd347fb96163d1f77f20687429559ca Mon Sep 17 00:00:00 2001 From: Ugaitz Urien Date: Wed, 11 Dec 2024 10:12:49 +0100 Subject: [PATCH 30/61] Update @datadog/native-iast-rewriter to 2.6.0 to support optional chainings (#4990) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 94b0114f651..dd90ee51661 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "dependencies": { "@datadog/libdatadog": "^0.2.2", "@datadog/native-appsec": "8.3.0", - "@datadog/native-iast-rewriter": "2.5.0", + "@datadog/native-iast-rewriter": "2.6.0", "@datadog/native-iast-taint-tracking": "3.2.0", "@datadog/native-metrics": "^3.0.1", "@datadog/pprof": "5.4.1", diff --git a/yarn.lock b/yarn.lock index c5982d831bb..54222f765ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -413,10 +413,10 @@ dependencies: node-gyp-build "^3.9.0" -"@datadog/native-iast-rewriter@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.5.0.tgz#b613defe86e78168f750d1f1662d4ffb3cf002e6" - integrity sha512-WRu34A3Wwp6oafX8KWNAbedtDaaJO+nzfYQht7pcJKjyC2ggfPeF7SoP+eDo9wTn4/nQwEOscSR4hkJqTRlpXQ== +"@datadog/native-iast-rewriter@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.6.0.tgz#745148ac630cace48372fb3b3aaa50e32460b693" + integrity sha512-TCRe3QNm7hGWlfvW1RnE959sV/kBqDiSEGAHS+HlQYaIwG2y0WcxA5TjLxhcIJJsfmgou5ycIlknAvNkbaoDDQ== dependencies: lru-cache "^7.14.0" node-gyp-build "^4.5.0" From 01c3ba1eb5abf82f68b382030524f1e20f3dedb2 Mon Sep 17 00:00:00 2001 From: Fayssal DEFAA <82442451+faydef@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:46:36 +0100 Subject: [PATCH 31/61] install node22 (#4985) --- benchmark/sirun/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmark/sirun/Dockerfile b/benchmark/sirun/Dockerfile index 6ce6d8557fe..5c6e883b62d 100644 --- a/benchmark/sirun/Dockerfile +++ b/benchmark/sirun/Dockerfile @@ -34,6 +34,7 @@ RUN mkdir -p /usr/local/nvm \ && nvm install --no-progress 16.20.1 \ && nvm install --no-progress 18.16.1 \ && nvm install --no-progress 20.4.0 \ + && nvm install --no-progress 22.10.0 \ && nvm alias default 18 \ && nvm use 18 From 50bb0dd2d415387436a4adbe18034c2790fe5ede Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Wed, 11 Dec 2024 12:02:00 +0100 Subject: [PATCH 32/61] Add support for endpoint_counts (#4980) Also: * Extract event.json creation in profile exporters so it can be shared between all exporters. File exporter will be writing it so we can more easily write integration tests. * Extract web span handling in profiler --- integration-tests/profiler/profiler.spec.js | 14 +++- .../dd-trace/src/profiling/exporters/agent.js | 77 +++---------------- .../profiling/exporters/event_serializer.js | 76 ++++++++++++++++++ .../dd-trace/src/profiling/exporters/file.js | 12 ++- packages/dd-trace/src/profiling/profiler.js | 72 ++++++++++++++--- .../dd-trace/src/profiling/profilers/wall.js | 19 +---- .../dd-trace/src/profiling/webspan-utils.js | 23 ++++++ .../test/profiling/exporters/file.spec.js | 16 +++- 8 files changed, 203 insertions(+), 106 deletions(-) create mode 100644 packages/dd-trace/src/profiling/exporters/event_serializer.js create mode 100644 packages/dd-trace/src/profiling/webspan-utils.js diff --git a/integration-tests/profiler/profiler.spec.js b/integration-tests/profiler/profiler.spec.js index 5a5a68be392..9a963202934 100644 --- a/integration-tests/profiler/profiler.spec.js +++ b/integration-tests/profiler/profiler.spec.js @@ -75,6 +75,12 @@ function processExitPromise (proc, timeout, expectBadExit = false) { } async function getLatestProfile (cwd, pattern) { + const pprofGzipped = await readLatestFile(cwd, pattern) + const pprofUnzipped = zlib.gunzipSync(pprofGzipped) + return { profile: Profile.decode(pprofUnzipped), encoded: pprofGzipped.toString('base64') } +} + +async function readLatestFile (cwd, pattern) { const dirEntries = await fs.readdir(cwd) // Get the latest file matching the pattern const pprofEntries = dirEntries.filter(name => pattern.test(name)) @@ -83,9 +89,7 @@ async function getLatestProfile (cwd, pattern) { .map(name => ({ name, modified: fsync.statSync(path.join(cwd, name), { bigint: true }).mtimeNs })) .reduce((a, b) => a.modified > b.modified ? a : b) .name - const pprofGzipped = await fs.readFile(path.join(cwd, pprofEntry)) - const pprofUnzipped = zlib.gunzipSync(pprofGzipped) - return { profile: Profile.decode(pprofUnzipped), encoded: pprofGzipped.toString('base64') } + return await fs.readFile(path.join(cwd, pprofEntry)) } function expectTimeout (messagePromise, allowErrors = false) { @@ -212,6 +216,10 @@ describe('profiler', () => { await processExitPromise(proc, 30000) const procEnd = BigInt(Date.now() * 1000000) + // Must've counted the number of times each endpoint was hit + const event = JSON.parse((await readLatestFile(cwd, /^event_.+\.json$/)).toString()) + assert.deepEqual(event.endpoint_counts, { 'endpoint-0': 1, 'endpoint-1': 1, 'endpoint-2': 1 }) + const { profile, encoded } = await getLatestProfile(cwd, /^wall_.+\.pprof$/) // We check the profile for following invariants: diff --git a/packages/dd-trace/src/profiling/exporters/agent.js b/packages/dd-trace/src/profiling/exporters/agent.js index 485636ee240..6ad63486a87 100644 --- a/packages/dd-trace/src/profiling/exporters/agent.js +++ b/packages/dd-trace/src/profiling/exporters/agent.js @@ -3,13 +3,13 @@ const retry = require('retry') const { request: httpRequest } = require('http') const { request: httpsRequest } = require('https') +const { EventSerializer } = require('./event_serializer') // TODO: avoid using dd-trace internals. Make this a separate module? const docker = require('../../exporters/common/docker') const FormData = require('../../exporters/common/form-data') const { storage } = require('../../../../datadog-core') const version = require('../../../../../package.json').version -const os = require('os') const { urlToHttpOptions } = require('url') const perf = require('perf_hooks').performance @@ -89,8 +89,10 @@ function computeRetries (uploadTimeout) { return [tries, Math.floor(uploadTimeout)] } -class AgentExporter { - constructor ({ url, logger, uploadTimeout, env, host, service, version, libraryInjected, activation } = {}) { +class AgentExporter extends EventSerializer { + constructor (config = {}) { + super(config) + const { url, logger, uploadTimeout } = config this._url = url this._logger = logger @@ -98,74 +100,13 @@ class AgentExporter { this._backoffTime = backoffTime this._backoffTries = backoffTries - this._env = env - this._host = host - this._service = service - this._appVersion = version - this._libraryInjected = !!libraryInjected - this._activation = activation || 'unknown' } - export ({ profiles, start, end, tags }) { + export (exportSpec) { + const { profiles } = exportSpec const fields = [] - function typeToFile (type) { - return `${type}.pprof` - } - - const event = JSON.stringify({ - attachments: Object.keys(profiles).map(typeToFile), - start: start.toISOString(), - end: end.toISOString(), - family: 'node', - version: '4', - tags_profiler: [ - 'language:javascript', - 'runtime:nodejs', - `runtime_arch:${process.arch}`, - `runtime_os:${process.platform}`, - `runtime_version:${process.version}`, - `process_id:${process.pid}`, - `profiler_version:${version}`, - 'format:pprof', - ...Object.entries(tags).map(([key, value]) => `${key}:${value}`) - ].join(','), - info: { - application: { - env: this._env, - service: this._service, - start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(), - version: this._appVersion - }, - platform: { - hostname: this._host, - kernel_name: os.type(), - kernel_release: os.release(), - kernel_version: os.version() - }, - profiler: { - activation: this._activation, - ssi: { - mechanism: this._libraryInjected ? 'injected_agent' : 'none' - }, - version - }, - runtime: { - // Using `nodejs` for consistency with the existing `runtime` tag. - // Note that the event `family` property uses `node`, as that's what's - // proscribed by the Intake API, but that's an internal enum and is - // not customer visible. - engine: 'nodejs', - // strip off leading 'v'. This makes the format consistent with other - // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag. - // We'll keep it like this as we want cross-engine consistency. We - // also aren't changing the format of the existing tag as we don't want - // to break it. - version: process.version.substring(1) - } - } - }) - + const event = this.getEventJSON(exportSpec) fields.push(['event', event, { filename: 'event.json', contentType: 'application/json' @@ -181,7 +122,7 @@ class AgentExporter { return `Adding ${type} profile to agent export: ` + bytes }) - const filename = typeToFile(type) + const filename = this.typeToFile(type) fields.push([filename, buffer, { filename, contentType: 'application/octet-stream' diff --git a/packages/dd-trace/src/profiling/exporters/event_serializer.js b/packages/dd-trace/src/profiling/exporters/event_serializer.js new file mode 100644 index 00000000000..1bd16ea21bc --- /dev/null +++ b/packages/dd-trace/src/profiling/exporters/event_serializer.js @@ -0,0 +1,76 @@ +const os = require('os') +const perf = require('perf_hooks').performance +const version = require('../../../../../package.json').version + +class EventSerializer { + constructor ({ env, host, service, version, libraryInjected, activation } = {}) { + this._env = env + this._host = host + this._service = service + this._appVersion = version + this._libraryInjected = !!libraryInjected + this._activation = activation || 'unknown' + } + + typeToFile (type) { + return `${type}.pprof` + } + + getEventJSON ({ profiles, start, end, tags = {}, endpointCounts }) { + return JSON.stringify({ + attachments: Object.keys(profiles).map(t => this.typeToFile(t)), + start: start.toISOString(), + end: end.toISOString(), + family: 'node', + version: '4', + tags_profiler: [ + 'language:javascript', + 'runtime:nodejs', + `runtime_arch:${process.arch}`, + `runtime_os:${process.platform}`, + `runtime_version:${process.version}`, + `process_id:${process.pid}`, + `profiler_version:${version}`, + 'format:pprof', + ...Object.entries(tags).map(([key, value]) => `${key}:${value}`) + ].join(','), + endpoint_counts: endpointCounts, + info: { + application: { + env: this._env, + service: this._service, + start_time: new Date(perf.nodeTiming.nodeStart + perf.timeOrigin).toISOString(), + version: this._appVersion + }, + platform: { + hostname: this._host, + kernel_name: os.type(), + kernel_release: os.release(), + kernel_version: os.version() + }, + profiler: { + activation: this._activation, + ssi: { + mechanism: this._libraryInjected ? 'injected_agent' : 'none' + }, + version + }, + runtime: { + // Using `nodejs` for consistency with the existing `runtime` tag. + // Note that the event `family` property uses `node`, as that's what's + // proscribed by the Intake API, but that's an internal enum and is + // not customer visible. + engine: 'nodejs', + // strip off leading 'v'. This makes the format consistent with other + // runtimes (e.g. Ruby) but not with the existing `runtime_version` tag. + // We'll keep it like this as we want cross-engine consistency. We + // also aren't changing the format of the existing tag as we don't want + // to break it. + version: process.version.substring(1) + } + } + }) + } +} + +module.exports = { EventSerializer } diff --git a/packages/dd-trace/src/profiling/exporters/file.js b/packages/dd-trace/src/profiling/exporters/file.js index 724eac4656b..a7b87b2025d 100644 --- a/packages/dd-trace/src/profiling/exporters/file.js +++ b/packages/dd-trace/src/profiling/exporters/file.js @@ -4,6 +4,7 @@ const fs = require('fs') const { promisify } = require('util') const { threadId } = require('worker_threads') const writeFile = promisify(fs.writeFile) +const { EventSerializer } = require('./event_serializer') function formatDateTime (t) { const pad = (n) => String(n).padStart(2, '0') @@ -11,18 +12,21 @@ function formatDateTime (t) { `T${pad(t.getUTCHours())}${pad(t.getUTCMinutes())}${pad(t.getUTCSeconds())}Z` } -class FileExporter { - constructor ({ pprofPrefix } = {}) { +class FileExporter extends EventSerializer { + constructor (config = {}) { + super(config) + const { pprofPrefix } = config this._pprofPrefix = pprofPrefix || '' } - export ({ profiles, end }) { + export (exportSpec) { + const { profiles, end } = exportSpec const types = Object.keys(profiles) const dateStr = formatDateTime(end) const tasks = types.map(type => { return writeFile(`${this._pprofPrefix}${type}_worker_${threadId}_${dateStr}.pprof`, profiles[type]) }) - + tasks.push(writeFile(`event_worker_${threadId}_${dateStr}.json`, this.getEventJSON(exportSpec))) return Promise.all(tasks) } } diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 2233de59f84..d02912dde42 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -4,9 +4,11 @@ const { EventEmitter } = require('events') const { Config } = require('./config') const { snapshotKinds } = require('./constants') const { threadNamePrefix } = require('./profilers/shared') +const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('./webspan-utils') const dc = require('dc-polyfill') const profileSubmittedChannel = dc.channel('datadog:profiling:profile-submitted') +const spanFinishedChannel = dc.channel('dd-trace:span:finish') function maybeSourceMap (sourceMap, SourceMapper, debug) { if (!sourceMap) return @@ -21,6 +23,20 @@ function logError (logger, err) { } } +function findWebSpan (startedSpans, spanId) { + for (let i = startedSpans.length; --i >= 0;) { + const ispan = startedSpans[i] + const context = ispan.context() + if (context._spanId === spanId) { + if (isWebServerSpan(context._tags)) { + return true + } + spanId = context._parentId + } + } + return false +} + class Profiler extends EventEmitter { constructor () { super() @@ -30,6 +46,7 @@ class Profiler extends EventEmitter { this._timer = undefined this._lastStart = undefined this._timeoutInterval = undefined + this.endpointCounts = new Map() } start (options) { @@ -82,6 +99,11 @@ class Profiler extends EventEmitter { this._logger.debug(`Started ${profiler.type} profiler in ${threadNamePrefix} thread`) } + if (config.endpointCollectionEnabled) { + this._spanFinishListener = this._onSpanFinish.bind(this) + spanFinishedChannel.subscribe(this._spanFinishListener) + } + this._capture(this._timeoutInterval, start) return true } catch (e) { @@ -117,6 +139,11 @@ class Profiler extends EventEmitter { this._enabled = false + if (this._spanFinishListener !== undefined) { + spanFinishedChannel.unsubscribe(this._spanFinishListener) + this._spanFinishListener = undefined + } + for (const profiler of this._config.profilers) { profiler.stop() this._logger.debug(`Stopped ${profiler.type} profiler in ${threadNamePrefix} thread`) @@ -137,6 +164,26 @@ class Profiler extends EventEmitter { } } + _onSpanFinish (span) { + const context = span.context() + const tags = context._tags + if (!isWebServerSpan(tags)) return + + const endpointName = endpointNameFromTags(tags) + if (!endpointName) return + + // Make sure this is the outermost web span, just in case so we don't overcount + if (findWebSpan(getStartedSpans(context), context._parentId)) return + + let counter = this.endpointCounts.get(endpointName) + if (counter === undefined) { + counter = { count: 1 } + this.endpointCounts.set(endpointName, counter) + } else { + counter.count++ + } + } + async _collect (snapshotKind, restart = true) { if (!this._enabled) return @@ -194,18 +241,23 @@ class Profiler extends EventEmitter { _submit (profiles, start, end, snapshotKind) { const { tags } = this._config - const tasks = [] - tags.snapshot = snapshotKind - for (const exporter of this._config.exporters) { - const task = exporter.export({ profiles, start, end, tags }) - .catch(err => { - if (this._logger) { - this._logger.warn(err) - } - }) - tasks.push(task) + // Flatten endpoint counts + const endpointCounts = {} + for (const [endpoint, { count }] of this.endpointCounts) { + endpointCounts[endpoint] = count } + this.endpointCounts.clear() + + tags.snapshot = snapshotKind + const exportSpec = { profiles, start, end, tags, endpointCounts } + const tasks = this._config.exporters.map(exporter => + exporter.export(exportSpec).catch(err => { + if (this._logger) { + this._logger.warn(err) + } + }) + ) return Promise.all(tasks) } diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index dc3c0ba61ba..bcc7959074f 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -3,8 +3,6 @@ const { storage } = require('../../../../datadog-core') const dc = require('dc-polyfill') -const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../../ext/tags') -const { WEB } = require('../../../../../ext/types') const runtimeMetrics = require('../../runtime_metrics') const telemetryMetrics = require('../../telemetry/metrics') const { @@ -15,6 +13,8 @@ const { getThreadLabels } = require('./shared') +const { isWebServerSpan, endpointNameFromTags, getStartedSpans } = require('../webspan-utils') + const beforeCh = dc.channel('dd-trace:storage:before') const enterCh = dc.channel('dd-trace:storage:enter') const spanFinishCh = dc.channel('dd-trace:span:finish') @@ -29,21 +29,6 @@ function getActiveSpan () { return store && store.span } -function getStartedSpans (context) { - return context._trace.started -} - -function isWebServerSpan (tags) { - return tags[SPAN_TYPE] === WEB -} - -function endpointNameFromTags (tags) { - return tags[RESOURCE_NAME] || [ - tags[HTTP_METHOD], - tags[HTTP_ROUTE] - ].filter(v => v).join(' ') -} - let channelsActivated = false function ensureChannelsActivated () { if (channelsActivated) return diff --git a/packages/dd-trace/src/profiling/webspan-utils.js b/packages/dd-trace/src/profiling/webspan-utils.js new file mode 100644 index 00000000000..d002dcd2705 --- /dev/null +++ b/packages/dd-trace/src/profiling/webspan-utils.js @@ -0,0 +1,23 @@ +const { HTTP_METHOD, HTTP_ROUTE, RESOURCE_NAME, SPAN_TYPE } = require('../../../../ext/tags') +const { WEB } = require('../../../../ext/types') + +function isWebServerSpan (tags) { + return tags[SPAN_TYPE] === WEB +} + +function endpointNameFromTags (tags) { + return tags[RESOURCE_NAME] || [ + tags[HTTP_METHOD], + tags[HTTP_ROUTE] + ].filter(v => v).join(' ') +} + +function getStartedSpans (context) { + return context._trace.started +} + +module.exports = { + isWebServerSpan, + endpointNameFromTags, + getStartedSpans +} diff --git a/packages/dd-trace/test/profiling/exporters/file.spec.js b/packages/dd-trace/test/profiling/exporters/file.spec.js index bca561dce8b..36b0d257ece 100644 --- a/packages/dd-trace/test/profiling/exporters/file.spec.js +++ b/packages/dd-trace/test/profiling/exporters/file.spec.js @@ -25,9 +25,13 @@ describe('exporters/file', () => { const profiles = { test: buffer } - await exporter.export({ profiles, end: new Date('2023-02-10T21:03:05Z') }) + await exporter.export({ + profiles, + start: new Date('2023-02-10T21:02:05Z'), + end: new Date('2023-02-10T21:03:05Z') + }) - sinon.assert.calledOnce(fs.writeFile) + sinon.assert.calledTwice(fs.writeFile) sinon.assert.calledWith(fs.writeFile, 'test_worker_0_20230210T210305Z.pprof', buffer) }) @@ -37,9 +41,13 @@ describe('exporters/file', () => { const profiles = { test: buffer } - await exporter.export({ profiles, end: new Date('2023-02-10T21:03:05Z') }) + await exporter.export({ + profiles, + start: new Date('2023-02-10T21:02:05Z'), + end: new Date('2023-02-10T21:03:05Z') + }) - sinon.assert.calledOnce(fs.writeFile) + sinon.assert.calledTwice(fs.writeFile) sinon.assert.calledWith(fs.writeFile, 'myprefix_test_worker_0_20230210T210305Z.pprof', buffer) }) }) From 1a95b0b0c56fa5761be59174b98071412fb21990 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 11 Dec 2024 12:13:15 +0100 Subject: [PATCH 33/61] [DI] Handle async errors in mocha tests (#4991) If an async error is thrown in mocha tests, mocha doesn't see it. Best case, the test will just time out, worst case, it will pass. --- integration-tests/debugger/basic.spec.js | 85 ++++++++----------- .../debugger/snapshot-pruning.spec.js | 5 +- integration-tests/debugger/snapshot.spec.js | 21 ++--- integration-tests/helpers/index.js | 23 ++++- 4 files changed, 73 insertions(+), 61 deletions(-) diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 8782bc90449..189032049f2 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -4,7 +4,7 @@ const os = require('os') const { assert } = require('chai') const { pollInterval, setup } = require('./utils') -const { assertObjectContains, assertUUID } = require('../helpers') +const { assertObjectContains, assertUUID, failOnException } = require('../helpers') const { ACKNOWLEDGED, ERROR } = require('../../packages/dd-trace/src/appsec/remote_config/apply_states') const { version } = require('../../package.json') @@ -35,7 +35,7 @@ describe('Dynamic Instrumentation', function () { debugger: { diagnostics: { probeId, probeVersion: 0, status: 'EMITTING' } } }] - t.agent.on('remote-config-ack-update', (id, version, state, error) => { + t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { assert.strictEqual(id, t.rcConfig.id) assert.strictEqual(version, 1) assert.strictEqual(state, ACKNOWLEDGED) @@ -43,9 +43,9 @@ describe('Dynamic Instrumentation', function () { receivedAckUpdate = true endIfDone() - }) + })) - t.agent.on('debugger-diagnostics', ({ payload }) => { + t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) assertUUID(payload.debugger.diagnostics.runtimeId) @@ -60,7 +60,7 @@ describe('Dynamic Instrumentation', function () { } else { endIfDone() } - }) + })) t.agent.addRemoteConfig(t.rcConfig) @@ -97,22 +97,22 @@ describe('Dynamic Instrumentation', function () { () => {} ] - t.agent.on('remote-config-ack-update', (id, version, state, error) => { + t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { assert.strictEqual(id, t.rcConfig.id) assert.strictEqual(version, ++receivedAckUpdates) assert.strictEqual(state, ACKNOWLEDGED) assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail endIfDone() - }) + })) - t.agent.on('debugger-diagnostics', ({ payload }) => { + t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) assertUUID(payload.debugger.diagnostics.runtimeId) if (payload.debugger.diagnostics.status === 'INSTALLED') triggers.shift()() endIfDone() - }) + })) t.agent.addRemoteConfig(t.rcConfig) @@ -135,7 +135,7 @@ describe('Dynamic Instrumentation', function () { debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }] - t.agent.on('remote-config-ack-update', (id, version, state, error) => { + t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { assert.strictEqual(id, t.rcConfig.id) assert.strictEqual(version, 1) assert.strictEqual(state, ACKNOWLEDGED) @@ -143,9 +143,9 @@ describe('Dynamic Instrumentation', function () { receivedAckUpdate = true endIfDone() - }) + })) - t.agent.on('debugger-diagnostics', ({ payload }) => { + t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) assertUUID(payload.debugger.diagnostics.runtimeId) @@ -158,7 +158,7 @@ describe('Dynamic Instrumentation', function () { endIfDone() }, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval } - }) + })) t.agent.addRemoteConfig(t.rcConfig) @@ -183,7 +183,7 @@ describe('Dynamic Instrumentation', function () { it(title, function (done) { let receivedAckUpdate = false - t.agent.on('remote-config-ack-update', (id, version, state, error) => { + t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { assert.strictEqual(id, `logProbe_${config.id}`) assert.strictEqual(version, 1) assert.strictEqual(state, ERROR) @@ -191,7 +191,7 @@ describe('Dynamic Instrumentation', function () { receivedAckUpdate = true endIfDone() - }) + })) const probeId = config.id const expectedPayloads = [{ @@ -201,10 +201,10 @@ describe('Dynamic Instrumentation', function () { }, { ddsource: 'dd_debugger', service: 'node', - debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, version: 0, status: 'ERROR' } } + debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, probeVersion: 0, status: 'ERROR' } } }] - t.agent.on('debugger-diagnostics', ({ payload }) => { + t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) const { diagnostics } = payload.debugger @@ -218,7 +218,7 @@ describe('Dynamic Instrumentation', function () { } endIfDone() - }) + })) t.agent.addRemoteConfig({ product: 'LIVE_DEBUGGING', @@ -237,7 +237,7 @@ describe('Dynamic Instrumentation', function () { it('should capture and send expected payload when a log line probe is triggered', function (done) { t.triggerBreakpoint() - t.agent.on('debugger-input', ({ payload }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload }) => { const expected = { ddsource: 'dd_debugger', hostname: os.hostname(), @@ -284,7 +284,7 @@ describe('Dynamic Instrumentation', function () { assert.strictEqual(topFrame.columnNumber, 3) done() - }) + })) t.agent.addRemoteConfig(t.rcConfig) }) @@ -307,43 +307,31 @@ describe('Dynamic Instrumentation', function () { if (payload.debugger.diagnostics.status === 'INSTALLED') triggers.shift()().catch(done) }) - t.agent.on('debugger-input', ({ payload }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload }) => { assert.strictEqual(payload.message, expectedMessages.shift()) if (expectedMessages.length === 0) done() - }) + })) t.agent.addRemoteConfig(t.rcConfig) }) it('should not trigger if probe is deleted', function (done) { - t.agent.on('debugger-diagnostics', async ({ payload }) => { - try { - if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.agent.once('remote-confg-responded', async () => { - try { - await t.axios.get('/foo') - // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail - // if it does, but not so long that the test times out. - // TODO: Is there some signal we can use instead of a timer? - setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval - } catch (err) { - // Nessecary hack: Any errors thrown inside of an async function is invisible to Mocha unless the outer - // `it` callback is also `async` (which we can't do in this case since we rely on the `done` callback). - done(err) - } - }) + t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + if (payload.debugger.diagnostics.status === 'INSTALLED') { + t.agent.once('remote-confg-responded', failOnException(done, async () => { + await t.axios.get('/foo') + // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail + // if it does, but not so long that the test times out. + // TODO: Is there some signal we can use instead of a timer? + setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval + })) - t.agent.removeRemoteConfig(t.rcConfig.id) - } - } catch (err) { - // Nessecary hack: Any errors thrown inside of an async function is invisible to Mocha unless the outer `it` - // callback is also `async` (which we can't do in this case since we rely on the `done` callback). - done(err) + t.agent.removeRemoteConfig(t.rcConfig.id) } - }) + })) t.agent.on('debugger-input', () => { - assert.fail('should not capture anything when the probe is deleted') + done(new Error('should not capture anything when the probe is deleted')) }) t.agent.addRemoteConfig(t.rcConfig) @@ -354,7 +342,8 @@ describe('Dynamic Instrumentation', function () { it('should remove the last breakpoint completely before trying to add a new one', function (done) { const rcConfig2 = t.generateRemoteConfig() - t.agent.on('debugger-diagnostics', ({ payload: { debugger: { diagnostics: { status, probeId } } } }) => { + t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + const { status, probeId } = payload.debugger.diagnostics if (status !== 'INSTALLED') return if (probeId === t.rcConfig.config.id) { @@ -387,7 +376,7 @@ describe('Dynamic Instrumentation', function () { if (!finished) done(err) }) } - }) + })) t.agent.addRemoteConfig(t.rcConfig) }) diff --git a/integration-tests/debugger/snapshot-pruning.spec.js b/integration-tests/debugger/snapshot-pruning.spec.js index 91190a1c25d..6458491c11c 100644 --- a/integration-tests/debugger/snapshot-pruning.spec.js +++ b/integration-tests/debugger/snapshot-pruning.spec.js @@ -2,6 +2,7 @@ const { assert } = require('chai') const { setup, getBreakpointInfo } = require('./utils') +const { failOnException } = require('../helpers') const { line } = getBreakpointInfo() @@ -13,7 +14,7 @@ describe('Dynamic Instrumentation', function () { beforeEach(t.triggerBreakpoint) it('should prune snapshot if payload is too large', function (done) { - t.agent.on('debugger-input', ({ payload }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload }) => { assert.isBelow(Buffer.byteLength(JSON.stringify(payload)), 1024 * 1024) // 1MB assert.deepEqual(payload['debugger.snapshot'].captures, { lines: { @@ -26,7 +27,7 @@ describe('Dynamic Instrumentation', function () { } }) done() - }) + })) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, diff --git a/integration-tests/debugger/snapshot.spec.js b/integration-tests/debugger/snapshot.spec.js index e3d17b225c4..d296c9c1b53 100644 --- a/integration-tests/debugger/snapshot.spec.js +++ b/integration-tests/debugger/snapshot.spec.js @@ -2,6 +2,7 @@ const { assert } = require('chai') const { setup } = require('./utils') +const { failOnException } = require('../helpers') describe('Dynamic Instrumentation', function () { const t = setup() @@ -11,7 +12,7 @@ describe('Dynamic Instrumentation', function () { beforeEach(t.triggerBreakpoint) it('should capture a snapshot', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { assert.deepEqual(Object.keys(captures), ['lines']) assert.deepEqual(Object.keys(captures.lines), [String(t.breakpoint.line)]) @@ -108,13 +109,13 @@ describe('Dynamic Instrumentation', function () { }) done() - }) + })) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true })) }) it('should respect maxReferenceDepth', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] delete locals.request delete locals.fastify @@ -144,13 +145,13 @@ describe('Dynamic Instrumentation', function () { }) done() - }) + })) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxReferenceDepth: 0 } })) }) it('should respect maxLength', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(locals.lstr, { @@ -161,13 +162,13 @@ describe('Dynamic Instrumentation', function () { }) done() - }) + })) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxLength: 10 } })) }) it('should respect maxCollectionSize', function (done) { - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(locals.arr, { @@ -182,7 +183,7 @@ describe('Dynamic Instrumentation', function () { }) done() - }) + })) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxCollectionSize: 3 } })) }) @@ -205,7 +206,7 @@ describe('Dynamic Instrumentation', function () { } } - t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(Object.keys(locals), [ @@ -230,7 +231,7 @@ describe('Dynamic Instrumentation', function () { } done() - }) + })) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxFieldCount } })) }) diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index 22074c3af20..b67f21a6d92 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -356,6 +356,26 @@ function assertUUID (actual, msg = 'not a valid UUID') { assert.match(actual, /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, msg) } +function failOnException (done, fn) { + if (fn[Symbol.toStringTag] === 'AsyncFunction') { + return async (...args) => { + try { + await fn(...args) + } catch (err) { + done(err) + } + } + } else { + return (...args) => { + try { + fn(...args) + } catch (err) { + done(err) + } + } + } +} + module.exports = { FakeAgent, hookFile, @@ -372,5 +392,6 @@ module.exports = { spawnPluginIntegrationTestProc, useEnv, useSandbox, - sandboxCwd + sandboxCwd, + failOnException } From a50d854dbd099d8c9292ae4cd90336d43cede1d3 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 11 Dec 2024 17:01:44 +0100 Subject: [PATCH 34/61] Ensure the fake agent in integration tests doesn't swallow exceptions (#4995) --- integration-tests/debugger/basic.spec.js | 57 +++++++++---------- .../debugger/snapshot-pruning.spec.js | 5 +- integration-tests/debugger/snapshot.spec.js | 21 ++++--- integration-tests/helpers/fake-agent.js | 8 +++ integration-tests/helpers/index.js | 23 +------- 5 files changed, 49 insertions(+), 65 deletions(-) diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 189032049f2..e48efb343c1 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -4,7 +4,7 @@ const os = require('os') const { assert } = require('chai') const { pollInterval, setup } = require('./utils') -const { assertObjectContains, assertUUID, failOnException } = require('../helpers') +const { assertObjectContains, assertUUID } = require('../helpers') const { ACKNOWLEDGED, ERROR } = require('../../packages/dd-trace/src/appsec/remote_config/apply_states') const { version } = require('../../package.json') @@ -35,7 +35,7 @@ describe('Dynamic Instrumentation', function () { debugger: { diagnostics: { probeId, probeVersion: 0, status: 'EMITTING' } } }] - t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { + t.agent.on('remote-config-ack-update', (id, version, state, error) => { assert.strictEqual(id, t.rcConfig.id) assert.strictEqual(version, 1) assert.strictEqual(state, ACKNOWLEDGED) @@ -43,9 +43,9 @@ describe('Dynamic Instrumentation', function () { receivedAckUpdate = true endIfDone() - })) + }) - t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + t.agent.on('debugger-diagnostics', ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) assertUUID(payload.debugger.diagnostics.runtimeId) @@ -60,7 +60,7 @@ describe('Dynamic Instrumentation', function () { } else { endIfDone() } - })) + }) t.agent.addRemoteConfig(t.rcConfig) @@ -97,22 +97,22 @@ describe('Dynamic Instrumentation', function () { () => {} ] - t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { + t.agent.on('remote-config-ack-update', (id, version, state, error) => { assert.strictEqual(id, t.rcConfig.id) assert.strictEqual(version, ++receivedAckUpdates) assert.strictEqual(state, ACKNOWLEDGED) assert.notOk(error) // falsy check since error will be an empty string, but that's an implementation detail endIfDone() - })) + }) - t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + t.agent.on('debugger-diagnostics', ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) assertUUID(payload.debugger.diagnostics.runtimeId) if (payload.debugger.diagnostics.status === 'INSTALLED') triggers.shift()() endIfDone() - })) + }) t.agent.addRemoteConfig(t.rcConfig) @@ -135,7 +135,7 @@ describe('Dynamic Instrumentation', function () { debugger: { diagnostics: { probeId, probeVersion: 0, status: 'INSTALLED' } } }] - t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { + t.agent.on('remote-config-ack-update', (id, version, state, error) => { assert.strictEqual(id, t.rcConfig.id) assert.strictEqual(version, 1) assert.strictEqual(state, ACKNOWLEDGED) @@ -143,9 +143,9 @@ describe('Dynamic Instrumentation', function () { receivedAckUpdate = true endIfDone() - })) + }) - t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + t.agent.on('debugger-diagnostics', ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) assertUUID(payload.debugger.diagnostics.runtimeId) @@ -158,7 +158,7 @@ describe('Dynamic Instrumentation', function () { endIfDone() }, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval } - })) + }) t.agent.addRemoteConfig(t.rcConfig) @@ -183,7 +183,7 @@ describe('Dynamic Instrumentation', function () { it(title, function (done) { let receivedAckUpdate = false - t.agent.on('remote-config-ack-update', failOnException(done, (id, version, state, error) => { + t.agent.on('remote-config-ack-update', (id, version, state, error) => { assert.strictEqual(id, `logProbe_${config.id}`) assert.strictEqual(version, 1) assert.strictEqual(state, ERROR) @@ -191,7 +191,7 @@ describe('Dynamic Instrumentation', function () { receivedAckUpdate = true endIfDone() - })) + }) const probeId = config.id const expectedPayloads = [{ @@ -204,7 +204,7 @@ describe('Dynamic Instrumentation', function () { debugger: { diagnostics: customErrorDiagnosticsObj ?? { probeId, probeVersion: 0, status: 'ERROR' } } }] - t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + t.agent.on('debugger-diagnostics', ({ payload }) => { const expected = expectedPayloads.shift() assertObjectContains(payload, expected) const { diagnostics } = payload.debugger @@ -218,7 +218,7 @@ describe('Dynamic Instrumentation', function () { } endIfDone() - })) + }) t.agent.addRemoteConfig({ product: 'LIVE_DEBUGGING', @@ -237,7 +237,7 @@ describe('Dynamic Instrumentation', function () { it('should capture and send expected payload when a log line probe is triggered', function (done) { t.triggerBreakpoint() - t.agent.on('debugger-input', failOnException(done, ({ payload }) => { + t.agent.on('debugger-input', ({ payload }) => { const expected = { ddsource: 'dd_debugger', hostname: os.hostname(), @@ -284,7 +284,7 @@ describe('Dynamic Instrumentation', function () { assert.strictEqual(topFrame.columnNumber, 3) done() - })) + }) t.agent.addRemoteConfig(t.rcConfig) }) @@ -307,31 +307,31 @@ describe('Dynamic Instrumentation', function () { if (payload.debugger.diagnostics.status === 'INSTALLED') triggers.shift()().catch(done) }) - t.agent.on('debugger-input', failOnException(done, ({ payload }) => { + t.agent.on('debugger-input', ({ payload }) => { assert.strictEqual(payload.message, expectedMessages.shift()) if (expectedMessages.length === 0) done() - })) + }) t.agent.addRemoteConfig(t.rcConfig) }) it('should not trigger if probe is deleted', function (done) { - t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { + t.agent.on('debugger-diagnostics', ({ payload }) => { if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.agent.once('remote-confg-responded', failOnException(done, async () => { + t.agent.once('remote-confg-responded', async () => { await t.axios.get('/foo') // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail // if it does, but not so long that the test times out. // TODO: Is there some signal we can use instead of a timer? setTimeout(done, pollInterval * 2 * 1000) // wait twice as long as the RC poll interval - })) + }) t.agent.removeRemoteConfig(t.rcConfig.id) } - })) + }) t.agent.on('debugger-input', () => { - done(new Error('should not capture anything when the probe is deleted')) + assert.fail('should not capture anything when the probe is deleted') }) t.agent.addRemoteConfig(t.rcConfig) @@ -342,8 +342,7 @@ describe('Dynamic Instrumentation', function () { it('should remove the last breakpoint completely before trying to add a new one', function (done) { const rcConfig2 = t.generateRemoteConfig() - t.agent.on('debugger-diagnostics', failOnException(done, ({ payload }) => { - const { status, probeId } = payload.debugger.diagnostics + t.agent.on('debugger-diagnostics', ({ payload: { debugger: { diagnostics: { status, probeId } } } }) => { if (status !== 'INSTALLED') return if (probeId === t.rcConfig.config.id) { @@ -376,7 +375,7 @@ describe('Dynamic Instrumentation', function () { if (!finished) done(err) }) } - })) + }) t.agent.addRemoteConfig(t.rcConfig) }) diff --git a/integration-tests/debugger/snapshot-pruning.spec.js b/integration-tests/debugger/snapshot-pruning.spec.js index 6458491c11c..91190a1c25d 100644 --- a/integration-tests/debugger/snapshot-pruning.spec.js +++ b/integration-tests/debugger/snapshot-pruning.spec.js @@ -2,7 +2,6 @@ const { assert } = require('chai') const { setup, getBreakpointInfo } = require('./utils') -const { failOnException } = require('../helpers') const { line } = getBreakpointInfo() @@ -14,7 +13,7 @@ describe('Dynamic Instrumentation', function () { beforeEach(t.triggerBreakpoint) it('should prune snapshot if payload is too large', function (done) { - t.agent.on('debugger-input', failOnException(done, ({ payload }) => { + t.agent.on('debugger-input', ({ payload }) => { assert.isBelow(Buffer.byteLength(JSON.stringify(payload)), 1024 * 1024) // 1MB assert.deepEqual(payload['debugger.snapshot'].captures, { lines: { @@ -27,7 +26,7 @@ describe('Dynamic Instrumentation', function () { } }) done() - })) + }) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, diff --git a/integration-tests/debugger/snapshot.spec.js b/integration-tests/debugger/snapshot.spec.js index d296c9c1b53..e3d17b225c4 100644 --- a/integration-tests/debugger/snapshot.spec.js +++ b/integration-tests/debugger/snapshot.spec.js @@ -2,7 +2,6 @@ const { assert } = require('chai') const { setup } = require('./utils') -const { failOnException } = require('../helpers') describe('Dynamic Instrumentation', function () { const t = setup() @@ -12,7 +11,7 @@ describe('Dynamic Instrumentation', function () { beforeEach(t.triggerBreakpoint) it('should capture a snapshot', function (done) { - t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { assert.deepEqual(Object.keys(captures), ['lines']) assert.deepEqual(Object.keys(captures.lines), [String(t.breakpoint.line)]) @@ -109,13 +108,13 @@ describe('Dynamic Instrumentation', function () { }) done() - })) + }) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true })) }) it('should respect maxReferenceDepth', function (done) { - t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] delete locals.request delete locals.fastify @@ -145,13 +144,13 @@ describe('Dynamic Instrumentation', function () { }) done() - })) + }) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxReferenceDepth: 0 } })) }) it('should respect maxLength', function (done) { - t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(locals.lstr, { @@ -162,13 +161,13 @@ describe('Dynamic Instrumentation', function () { }) done() - })) + }) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxLength: 10 } })) }) it('should respect maxCollectionSize', function (done) { - t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(locals.arr, { @@ -183,7 +182,7 @@ describe('Dynamic Instrumentation', function () { }) done() - })) + }) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxCollectionSize: 3 } })) }) @@ -206,7 +205,7 @@ describe('Dynamic Instrumentation', function () { } } - t.agent.on('debugger-input', failOnException(done, ({ payload: { 'debugger.snapshot': { captures } } }) => { + t.agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => { const { locals } = captures.lines[t.breakpoint.line] assert.deepEqual(Object.keys(locals), [ @@ -231,7 +230,7 @@ describe('Dynamic Instrumentation', function () { } done() - })) + }) t.agent.addRemoteConfig(t.generateRemoteConfig({ captureSnapshot: true, capture: { maxFieldCount } })) }) diff --git a/integration-tests/helpers/fake-agent.js b/integration-tests/helpers/fake-agent.js index f1054720d92..4902c80d9a1 100644 --- a/integration-tests/helpers/fake-agent.js +++ b/integration-tests/helpers/fake-agent.js @@ -363,6 +363,14 @@ function buildExpressServer (agent) { }) }) + // Ensure that any failure inside of Express isn't swallowed and returned as a 500, but instead crashes the test + app.use((err, req, res, next) => { + if (!err) next() + process.nextTick(() => { + throw err + }) + }) + return app } diff --git a/integration-tests/helpers/index.js b/integration-tests/helpers/index.js index b67f21a6d92..22074c3af20 100644 --- a/integration-tests/helpers/index.js +++ b/integration-tests/helpers/index.js @@ -356,26 +356,6 @@ function assertUUID (actual, msg = 'not a valid UUID') { assert.match(actual, /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/, msg) } -function failOnException (done, fn) { - if (fn[Symbol.toStringTag] === 'AsyncFunction') { - return async (...args) => { - try { - await fn(...args) - } catch (err) { - done(err) - } - } - } else { - return (...args) => { - try { - fn(...args) - } catch (err) { - done(err) - } - } - } -} - module.exports = { FakeAgent, hookFile, @@ -392,6 +372,5 @@ module.exports = { spawnPluginIntegrationTestProc, useEnv, useSandbox, - sandboxCwd, - failOnException + sandboxCwd } From 41e8a55e2f27c70a6e87d754ed2ad2f3a0bbc517 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 11 Dec 2024 18:33:44 +0100 Subject: [PATCH 35/61] [DI] Ensure the tracer doesn't block instrumented app from exiting (#4993) The `MessagePort` objects should be unref'ed (has to be after any message handler has been attached). Otherwise their handle will keep the instrumented app running. Technically there's no need to unref `port1`, but let's just unref everything show the intent. --- .../debugger/target-app/unreffed.js | 15 +++++++++++++++ integration-tests/debugger/unreffed.spec.js | 17 +++++++++++++++++ integration-tests/debugger/utils.js | 8 ++++---- packages/dd-trace/src/debugger/index.js | 8 ++++++-- 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 integration-tests/debugger/target-app/unreffed.js create mode 100644 integration-tests/debugger/unreffed.spec.js diff --git a/integration-tests/debugger/target-app/unreffed.js b/integration-tests/debugger/target-app/unreffed.js new file mode 100644 index 00000000000..3a5353d7399 --- /dev/null +++ b/integration-tests/debugger/target-app/unreffed.js @@ -0,0 +1,15 @@ +'use strict' + +require('dd-trace/init') +const http = require('http') + +const server = http.createServer((req, res) => { + res.end('hello world') // BREAKPOINT + setImmediate(() => { + server.close() + }) +}) + +server.listen(process.env.APP_PORT, () => { + process.send({ port: process.env.APP_PORT }) +}) diff --git a/integration-tests/debugger/unreffed.spec.js b/integration-tests/debugger/unreffed.spec.js new file mode 100644 index 00000000000..2873d80e190 --- /dev/null +++ b/integration-tests/debugger/unreffed.spec.js @@ -0,0 +1,17 @@ +'use strict' + +const { assert } = require('chai') +const { setup } = require('./utils') + +describe('Dynamic Instrumentation', function () { + const t = setup() + + it('should not hinder the program from exiting', function (done) { + // Expect the instrumented app to exit after receiving an HTTP request. Will time out otherwise. + t.proc.on('exit', (code) => { + assert.strictEqual(code, 0) + done() + }) + t.axios.get('/') + }) +}) diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index c5760a0e9d4..c65bd5c0d88 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -18,7 +18,7 @@ module.exports = { } function setup () { - let sandbox, cwd, appPort, proc + let sandbox, cwd, appPort const breakpoint = getBreakpointInfo(1) // `1` to disregard the `setup` function const t = { breakpoint, @@ -68,7 +68,7 @@ function setup () { } before(async function () { - sandbox = await createSandbox(['fastify']) + sandbox = await createSandbox(['fastify']) // TODO: Make this dynamic cwd = sandbox.folder t.appFile = join(cwd, ...breakpoint.file.split('/')) }) @@ -81,7 +81,7 @@ function setup () { t.rcConfig = generateRemoteConfig(breakpoint) appPort = await getPort() t.agent = await new FakeAgent().start() - proc = await spawnProc(t.appFile, { + t.proc = await spawnProc(t.appFile, { cwd, env: { APP_PORT: appPort, @@ -97,7 +97,7 @@ function setup () { }) afterEach(async function () { - proc.kill() + t.proc.kill() await t.agent.stop() }) diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index 3638119c6f1..ea2a36d4d25 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -57,8 +57,6 @@ function start (config, rc) { } ) - worker.unref() - worker.on('online', () => { log.debug(`Dynamic Instrumentation worker thread started successfully (thread id: ${worker.threadId})`) }) @@ -80,6 +78,12 @@ function start (config, rc) { rcAckCallbacks.delete(ackId) } }) + + worker.unref() + rcChannel.port1.unref() + rcChannel.port2.unref() + configChannel.port1.unref() + configChannel.port2.unref() } function configure (config) { From e8ff00a1278023d1c5860130352a3b5277de2676 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 11 Dec 2024 18:34:31 +0100 Subject: [PATCH 36/61] [DI] Improve separation between RC and breakpoint logic (#4992) This will make it easier to mock either one or the other in tests or to add probes without relying on RC. --- .../debugger/devtools_client/breakpoints.js | 69 +++++++++++++++++++ .../debugger/devtools_client/remote_config.js | 66 +----------------- 2 files changed, 72 insertions(+), 63 deletions(-) create mode 100644 packages/dd-trace/src/debugger/devtools_client/breakpoints.js diff --git a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js new file mode 100644 index 00000000000..5f12f83f11d --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js @@ -0,0 +1,69 @@ +'use strict' + +const session = require('./session') +const { findScriptFromPartialPath, probes, breakpoints } = require('./state') +const log = require('../../log') + +let sessionStarted = false + +module.exports = { + addBreakpoint, + removeBreakpoint +} + +async function addBreakpoint (probe) { + if (!sessionStarted) await start() + + const file = probe.where.sourceFile + const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints + + // Optimize for sending data to /debugger/v1/input endpoint + probe.location = { file, lines: [String(line)] } + delete probe.where + + // TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached. + // Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will + // not continue untill all scripts have been parsed? + const script = findScriptFromPartialPath(file) + if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`) + const [path, scriptId] = script + + log.debug(`Adding breakpoint at ${path}:${line} (probe: ${probe.id}, version: ${probe.version})`) + + const { breakpointId } = await session.post('Debugger.setBreakpoint', { + location: { + scriptId, + lineNumber: line - 1 // Beware! lineNumber is zero-indexed + } + }) + + probes.set(probe.id, breakpointId) + breakpoints.set(breakpointId, probe) +} + +async function removeBreakpoint ({ id }) { + if (!sessionStarted) { + // We should not get in this state, but abort if we do, so the code doesn't fail unexpected + throw Error(`Cannot remove probe ${id}: Debugger not started`) + } + if (!probes.has(id)) { + throw Error(`Unknown probe id: ${id}`) + } + + const breakpointId = probes.get(id) + await session.post('Debugger.removeBreakpoint', { breakpointId }) + probes.delete(id) + breakpoints.delete(breakpointId) + + if (breakpoints.size === 0) await stop() +} + +async function start () { + sessionStarted = true + return session.post('Debugger.enable') // return instead of await to reduce number of promises created +} + +async function stop () { + sessionStarted = false + return session.post('Debugger.disable') // return instead of await to reduce number of promises created +} diff --git a/packages/dd-trace/src/debugger/devtools_client/remote_config.js b/packages/dd-trace/src/debugger/devtools_client/remote_config.js index b0cffee3732..165a68ce503 100644 --- a/packages/dd-trace/src/debugger/devtools_client/remote_config.js +++ b/packages/dd-trace/src/debugger/devtools_client/remote_config.js @@ -1,13 +1,10 @@ 'use strict' const { workerData: { rcPort } } = require('node:worker_threads') -const { findScriptFromPartialPath, probes, breakpoints } = require('./state') -const session = require('./session') +const { addBreakpoint, removeBreakpoint } = require('./breakpoints') const { ackReceived, ackInstalled, ackError } = require('./status') const log = require('../../log') -let sessionStarted = false - // Example log line probe (simplified): // { // id: '100c9a5c-45ad-49dc-818b-c570d31e11d1', @@ -46,16 +43,6 @@ rcPort.on('message', async ({ action, conf: probe, ackId }) => { }) rcPort.on('messageerror', (err) => log.error(err)) -async function start () { - sessionStarted = true - return session.post('Debugger.enable') // return instead of await to reduce number of promises created -} - -async function stop () { - sessionStarted = false - return session.post('Debugger.disable') // return instead of await to reduce number of promises created -} - async function processMsg (action, probe) { log.debug(`Received request to ${action} ${probe.type} probe (id: ${probe.id}, version: ${probe.version})`) @@ -90,11 +77,13 @@ async function processMsg (action, probe) { break case 'apply': await addBreakpoint(probe) + ackInstalled(probe) break case 'modify': // TODO: Modify existing probe instead of removing it (DEBUG-2817) await removeBreakpoint(probe) await addBreakpoint(probe) + ackInstalled(probe) // TODO: Should we also send ackInstalled when modifying a probe? break default: throw new Error( @@ -107,55 +96,6 @@ async function processMsg (action, probe) { } } -async function addBreakpoint (probe) { - if (!sessionStarted) await start() - - const file = probe.where.sourceFile - const line = Number(probe.where.lines[0]) // Tracer doesn't support multiple-line breakpoints - - // Optimize for sending data to /debugger/v1/input endpoint - probe.location = { file, lines: [String(line)] } - delete probe.where - - // TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached. - // Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will - // not continue untill all scripts have been parsed? - const script = findScriptFromPartialPath(file) - if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`) - const [path, scriptId] = script - - log.debug(`Adding breakpoint at ${path}:${line} (probe: ${probe.id}, version: ${probe.version})`) - - const { breakpointId } = await session.post('Debugger.setBreakpoint', { - location: { - scriptId, - lineNumber: line - 1 // Beware! lineNumber is zero-indexed - } - }) - - probes.set(probe.id, breakpointId) - breakpoints.set(breakpointId, probe) - - ackInstalled(probe) -} - -async function removeBreakpoint ({ id }) { - if (!sessionStarted) { - // We should not get in this state, but abort if we do, so the code doesn't fail unexpected - throw Error(`Cannot remove probe ${id}: Debugger not started`) - } - if (!probes.has(id)) { - throw Error(`Unknown probe id: ${id}`) - } - - const breakpointId = probes.get(id) - await session.post('Debugger.removeBreakpoint', { breakpointId }) - probes.delete(id) - breakpoints.delete(breakpointId) - - if (breakpoints.size === 0) await stop() -} - async function lock () { if (lock.p) await lock.p let resolve From ab449ca629ab7e28068090d2289afdc900d3261b Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Dec 2024 13:49:32 +0100 Subject: [PATCH 37/61] Fix numbers stated in benchmark README.md (#5002) These numbers have drifted over time as the benchmarks have been tweaked. The number stated in the README.md files are no longer correct. Instead of trying to keep these in sync, this commit just removes any mention of iterations from the README files. --- benchmark/sirun/appsec-iast/README.md | 2 +- benchmark/sirun/appsec/README.md | 2 +- benchmark/sirun/encoding/README.md | 2 +- benchmark/sirun/exporting-pipeline/README.md | 2 +- benchmark/sirun/log/README.md | 2 +- benchmark/sirun/plugin-bluebird/README.md | 4 ++-- benchmark/sirun/plugin-dns/README.md | 2 +- benchmark/sirun/plugin-http/README.md | 2 +- benchmark/sirun/plugin-net/README.md | 2 +- benchmark/sirun/plugin-q/README.md | 2 +- benchmark/sirun/spans/README.md | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/benchmark/sirun/appsec-iast/README.md b/benchmark/sirun/appsec-iast/README.md index 79c5e0d21ab..728ed535fb3 100644 --- a/benchmark/sirun/appsec-iast/README.md +++ b/benchmark/sirun/appsec-iast/README.md @@ -1,4 +1,4 @@ -This creates 150 HTTP requests from client to server. +This benchmarks HTTP requests from client to server. The variants are: - control tracer with non vulnerable endpoint without iast diff --git a/benchmark/sirun/appsec/README.md b/benchmark/sirun/appsec/README.md index fd45c303b23..bbcb424e972 100644 --- a/benchmark/sirun/appsec/README.md +++ b/benchmark/sirun/appsec/README.md @@ -1,4 +1,4 @@ -This creates 1,000 HTTP requests from client to server. +This benchmarks HTTP requests from client to server. The variants are: - control tracer without appsec diff --git a/benchmark/sirun/encoding/README.md b/benchmark/sirun/encoding/README.md index 889bb9dec4b..957102dabc7 100644 --- a/benchmark/sirun/encoding/README.md +++ b/benchmark/sirun/encoding/README.md @@ -1,4 +1,4 @@ -This test sends a single trace 10000 times to the encoder. Each trace is +This test sends a single trace many times to the encoder. Each trace is pre-formatted (as the encoder requires) and consists of 30 spans with the same content in each of them. The IDs are all randomized. A null writer is provided to the encoder, so writing operations are not included here. diff --git a/benchmark/sirun/exporting-pipeline/README.md b/benchmark/sirun/exporting-pipeline/README.md index f7447afc608..28a0f23e5d2 100644 --- a/benchmark/sirun/exporting-pipeline/README.md +++ b/benchmark/sirun/exporting-pipeline/README.md @@ -2,6 +2,6 @@ This test creates a 30 span trace (of similar format to the encoding test). These spans are then passed through the formatting, encoding, and writing steps in our pipeline, and sent to a dummy agent. Once a span (i.e. a trace) is added to the exporter, we then proceed to the next iteration via `setImmediate`, and -run for 25000 iterations. +run for many iterations. There's a variant for each of our encodings/endpoints. diff --git a/benchmark/sirun/log/README.md b/benchmark/sirun/log/README.md index 9f25c806479..422abe0a610 100644 --- a/benchmark/sirun/log/README.md +++ b/benchmark/sirun/log/README.md @@ -1,4 +1,4 @@ -This test calls the internal logger on various log levels for 1000 iterations. +This test calls the internal logger on various log levels for many iterations. * `without-log` is the baseline that has logging disabled completely. * `skip-log` has logs enabled but uses a log level that isn't so that the handler doesn't run. diff --git a/benchmark/sirun/plugin-bluebird/README.md b/benchmark/sirun/plugin-bluebird/README.md index 5d1746b4b24..79fd4f57d0d 100644 --- a/benchmark/sirun/plugin-bluebird/README.md +++ b/benchmark/sirun/plugin-bluebird/README.md @@ -1,3 +1,3 @@ -This creates 50000 promises in a chain using the latest version of `bluebird`. +This creates a lot of promises in a chain using the latest version of `bluebird`. -The variants are with the tracer and without it. \ No newline at end of file +The variants are with the tracer and without it. diff --git a/benchmark/sirun/plugin-dns/README.md b/benchmark/sirun/plugin-dns/README.md index af30cb91095..566ac08842b 100644 --- a/benchmark/sirun/plugin-dns/README.md +++ b/benchmark/sirun/plugin-dns/README.md @@ -1,2 +1,2 @@ -Runs `dns.lookup('localhost', cb)` 10000 times. In the `with-tracer` variant, +Runs `dns.lookup('localhost', cb)` many times. In the `with-tracer` variant, tracing is enabled. Iteration count is set to 10. diff --git a/benchmark/sirun/plugin-http/README.md b/benchmark/sirun/plugin-http/README.md index 0ed9208d040..f42693cb6b2 100644 --- a/benchmark/sirun/plugin-http/README.md +++ b/benchmark/sirun/plugin-http/README.md @@ -1,4 +1,4 @@ -This creates 1,000 HTTP requests from client to server. +This benchmarks HTTP requests from client to server. The variants are with the tracer and without it, and instrumenting on the server and the client separately. diff --git a/benchmark/sirun/plugin-net/README.md b/benchmark/sirun/plugin-net/README.md index 0731413e121..dc2635fdbe9 100644 --- a/benchmark/sirun/plugin-net/README.md +++ b/benchmark/sirun/plugin-net/README.md @@ -1,3 +1,3 @@ -Creates 1000 connections between a net server and net client, doing a simple +Benchmarks connections between a net server and net client, doing a simple echo request. Since we only instrument client-side net connections, our variants are having the client with and without the tracer. diff --git a/benchmark/sirun/plugin-q/README.md b/benchmark/sirun/plugin-q/README.md index 48e57db4360..8dcce34ec93 100644 --- a/benchmark/sirun/plugin-q/README.md +++ b/benchmark/sirun/plugin-q/README.md @@ -1,3 +1,3 @@ -This creates 50000 promises in a chain using the latest version of `q`. +This benchmarks promises in a chain using the latest version of `q`. The variants are with the tracer and without it. diff --git a/benchmark/sirun/spans/README.md b/benchmark/sirun/spans/README.md index 734c9df65ac..7b695939b00 100644 --- a/benchmark/sirun/spans/README.md +++ b/benchmark/sirun/spans/README.md @@ -1,5 +1,5 @@ This test initializes a tracer with the no-op scope manager. It then creates -100000 spans, and depending on the variant, either finishes all of them as they +many spans, and depending on the variant, either finishes all of them as they are created, or later on once they're all created. Prior to creating any spans, it modifies the processor instance so that no span processing (or exporting) is done, and it simply stops storing the spans. From 111c61ba7a9bba44aa5910847fc9b02ee98338c9 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Dec 2024 13:49:51 +0100 Subject: [PATCH 38/61] Add summary.json to the benchmark .gitignore file (#5003) When using Sirun, it's normal to generate a `summary.json` file after running a benchmark. These should not be added to git. --- benchmark/sirun/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmark/sirun/.gitignore b/benchmark/sirun/.gitignore index bc111ce710b..6b557f5a398 100644 --- a/benchmark/sirun/.gitignore +++ b/benchmark/sirun/.gitignore @@ -1,2 +1,3 @@ *.ndjson meta-temp.json +summary.json From 04f3610708abb4641044343981afab047c83ca8a Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Dec 2024 13:51:02 +0100 Subject: [PATCH 39/61] [DI] Improve test setup by allowing breakpoint URL to be dynamic (#4996) --- integration-tests/debugger/basic.spec.js | 12 ++--- .../debugger/snapshot-pruning.spec.js | 6 +-- .../debugger/target-app/basic.js | 2 +- .../debugger/target-app/snapshot-pruning.js | 2 +- .../debugger/target-app/snapshot.js | 2 +- .../debugger/target-app/unreffed.js | 2 +- integration-tests/debugger/unreffed.spec.js | 2 +- integration-tests/debugger/utils.js | 45 +++++++------------ .../test/debugger/devtools_client/utils.js | 25 +++++++++++ 9 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 packages/dd-trace/test/debugger/devtools_client/utils.js diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index e48efb343c1..f42388396ef 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -12,7 +12,7 @@ describe('Dynamic Instrumentation', function () { const t = setup() it('base case: target app should work as expected if no test probe has been added', async function () { - const response = await t.axios.get('/foo') + const response = await t.axios.get(t.breakpoint.url) assert.strictEqual(response.status, 200) assert.deepStrictEqual(response.data, { hello: 'foo' }) }) @@ -51,7 +51,7 @@ describe('Dynamic Instrumentation', function () { assertUUID(payload.debugger.diagnostics.runtimeId) if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get('/foo') + t.axios.get(t.breakpoint.url) .then((response) => { assert.strictEqual(response.status, 200) assert.deepStrictEqual(response.data, { hello: 'foo' }) @@ -293,13 +293,13 @@ describe('Dynamic Instrumentation', function () { const expectedMessages = ['Hello World!', 'Hello Updated World!'] const triggers = [ async () => { - await t.axios.get('/foo') + await t.axios.get(t.breakpoint.url) t.rcConfig.config.version++ t.rcConfig.config.template = 'Hello Updated World!' t.agent.updateRemoteConfig(t.rcConfig.id, t.rcConfig.config) }, async () => { - await t.axios.get('/foo') + await t.axios.get(t.breakpoint.url) } ] @@ -319,7 +319,7 @@ describe('Dynamic Instrumentation', function () { t.agent.on('debugger-diagnostics', ({ payload }) => { if (payload.debugger.diagnostics.status === 'INSTALLED') { t.agent.once('remote-confg-responded', async () => { - await t.axios.get('/foo') + await t.axios.get(t.breakpoint.url) // We want to wait enough time to see if the client triggers on the breakpoint so that the test can fail // if it does, but not so long that the test times out. // TODO: Is there some signal we can use instead of a timer? @@ -368,7 +368,7 @@ describe('Dynamic Instrumentation', function () { }) // Perform HTTP request to try and trigger the probe - t.axios.get('/foo').catch((err) => { + t.axios.get(t.breakpoint.url).catch((err) => { // If the request hasn't fully completed by the time the tests ends and the target app is destroyed, Axios // will complain with a "socket hang up" error. Hence this sanity check before calling `done(err)`. If we // later add more tests below this one, this shouuldn't be an issue. diff --git a/integration-tests/debugger/snapshot-pruning.spec.js b/integration-tests/debugger/snapshot-pruning.spec.js index 91190a1c25d..c1ba218dd1c 100644 --- a/integration-tests/debugger/snapshot-pruning.spec.js +++ b/integration-tests/debugger/snapshot-pruning.spec.js @@ -1,9 +1,7 @@ 'use strict' const { assert } = require('chai') -const { setup, getBreakpointInfo } = require('./utils') - -const { line } = getBreakpointInfo() +const { setup } = require('./utils') describe('Dynamic Instrumentation', function () { const t = setup() @@ -17,7 +15,7 @@ describe('Dynamic Instrumentation', function () { assert.isBelow(Buffer.byteLength(JSON.stringify(payload)), 1024 * 1024) // 1MB assert.deepEqual(payload['debugger.snapshot'].captures, { lines: { - [line]: { + [t.breakpoint.line]: { locals: { notCapturedReason: 'Snapshot was too large', size: 6 diff --git a/integration-tests/debugger/target-app/basic.js b/integration-tests/debugger/target-app/basic.js index f8330012278..2fa9c16d221 100644 --- a/integration-tests/debugger/target-app/basic.js +++ b/integration-tests/debugger/target-app/basic.js @@ -6,7 +6,7 @@ const Fastify = require('fastify') const fastify = Fastify() fastify.get('/:name', function handler (request) { - return { hello: request.params.name } // BREAKPOINT + return { hello: request.params.name } // BREAKPOINT: /foo }) fastify.listen({ port: process.env.APP_PORT }, (err) => { diff --git a/integration-tests/debugger/target-app/snapshot-pruning.js b/integration-tests/debugger/target-app/snapshot-pruning.js index 58752006192..6b14405e61d 100644 --- a/integration-tests/debugger/target-app/snapshot-pruning.js +++ b/integration-tests/debugger/target-app/snapshot-pruning.js @@ -14,7 +14,7 @@ fastify.get('/:name', function handler (request) { // eslint-disable-next-line no-unused-vars const obj = generateObjectWithJSONSizeLargerThan1MB() - return { hello: request.params.name } // BREAKPOINT + return { hello: request.params.name } // BREAKPOINT: /foo }) fastify.listen({ port: process.env.APP_PORT }, (err) => { diff --git a/integration-tests/debugger/target-app/snapshot.js b/integration-tests/debugger/target-app/snapshot.js index bae83a2176e..03cfc758556 100644 --- a/integration-tests/debugger/target-app/snapshot.js +++ b/integration-tests/debugger/target-app/snapshot.js @@ -11,7 +11,7 @@ const fastify = Fastify() fastify.get('/:name', function handler (request) { // eslint-disable-next-line no-unused-vars const { nil, undef, bool, num, bigint, str, lstr, sym, regex, arr, obj, emptyObj, fn, p } = getSomeData() - return { hello: request.params.name } // BREAKPOINT + return { hello: request.params.name } // BREAKPOINT: /foo }) fastify.listen({ port: process.env.APP_PORT }, (err) => { diff --git a/integration-tests/debugger/target-app/unreffed.js b/integration-tests/debugger/target-app/unreffed.js index 3a5353d7399..c3c73d72d8b 100644 --- a/integration-tests/debugger/target-app/unreffed.js +++ b/integration-tests/debugger/target-app/unreffed.js @@ -4,7 +4,7 @@ require('dd-trace/init') const http = require('http') const server = http.createServer((req, res) => { - res.end('hello world') // BREAKPOINT + res.end('hello world') // BREAKPOINT: / setImmediate(() => { server.close() }) diff --git a/integration-tests/debugger/unreffed.spec.js b/integration-tests/debugger/unreffed.spec.js index 2873d80e190..3ce9458f341 100644 --- a/integration-tests/debugger/unreffed.spec.js +++ b/integration-tests/debugger/unreffed.spec.js @@ -12,6 +12,6 @@ describe('Dynamic Instrumentation', function () { assert.strictEqual(code, 0) done() }) - t.axios.get('/') + t.axios.get(t.breakpoint.url) }) }) diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index c65bd5c0d88..1ea6cb9b54c 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -8,13 +8,14 @@ const getPort = require('get-port') const Axios = require('axios') const { createSandbox, FakeAgent, spawnProc } = require('../helpers') +const { generateProbeConfig } = require('../../packages/dd-trace/test/debugger/devtools_client/utils') +const BREAKPOINT_TOKEN = '// BREAKPOINT' const pollInterval = 1 module.exports = { pollInterval, - setup, - getBreakpointInfo + setup } function setup () { @@ -35,7 +36,7 @@ function setup () { // Trigger the breakpoint once probe is successfully installed t.agent.on('debugger-diagnostics', ({ payload }) => { if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get('/foo') + t.axios.get(breakpoint.url) } }) } @@ -45,32 +46,15 @@ function setup () { return { product: 'LIVE_DEBUGGING', id: `logProbe_${overrides.id}`, - config: generateProbeConfig(overrides) - } - } - - function generateProbeConfig (overrides = {}) { - overrides.capture = { maxReferenceDepth: 3, ...overrides.capture } - overrides.sampling = { snapshotsPerSecond: 5000, ...overrides.sampling } - return { - id: randomUUID(), - version: 0, - type: 'LOG_PROBE', - language: 'javascript', - where: { sourceFile: breakpoint.file, lines: [String(breakpoint.line)] }, - tags: [], - template: 'Hello World!', - segments: [{ str: 'Hello World!' }], - captureSnapshot: false, - evaluateAt: 'EXIT', - ...overrides + config: generateProbeConfig(breakpoint, overrides) } } before(async function () { sandbox = await createSandbox(['fastify']) // TODO: Make this dynamic cwd = sandbox.folder - t.appFile = join(cwd, ...breakpoint.file.split('/')) + // The sandbox uses the `integration-tests` folder as its root + t.appFile = join(cwd, 'debugger', breakpoint.file) }) after(async function () { @@ -113,12 +97,15 @@ function getBreakpointInfo (stackIndex = 0) { .split(':')[0] // Then, find the corresponding file in which the breakpoint exists - const filename = basename(testFile).replace('.spec', '') + const file = join('target-app', basename(testFile).replace('.spec', '')) // Finally, find the line number of the breakpoint - const line = readFileSync(join(__dirname, 'target-app', filename), 'utf8') - .split('\n') - .findIndex(line => line.includes('// BREAKPOINT')) + 1 - - return { file: `debugger/target-app/${filename}`, line } + const lines = readFileSync(join(__dirname, file), 'utf8').split('\n') + for (let i = 0; i < lines.length; i++) { + const index = lines[i].indexOf(BREAKPOINT_TOKEN) + if (index !== -1) { + const url = lines[i].slice(index + BREAKPOINT_TOKEN.length + 1).trim() + return { file, line: i + 1, url } + } + } } diff --git a/packages/dd-trace/test/debugger/devtools_client/utils.js b/packages/dd-trace/test/debugger/devtools_client/utils.js new file mode 100644 index 00000000000..e15d567a7c1 --- /dev/null +++ b/packages/dd-trace/test/debugger/devtools_client/utils.js @@ -0,0 +1,25 @@ +'use strict' + +const { randomUUID } = require('node:crypto') + +module.exports = { + generateProbeConfig +} + +function generateProbeConfig (breakpoint, overrides = {}) { + overrides.capture = { maxReferenceDepth: 3, ...overrides.capture } + overrides.sampling = { snapshotsPerSecond: 5000, ...overrides.sampling } + return { + id: randomUUID(), + version: 0, + type: 'LOG_PROBE', + language: 'javascript', + where: { sourceFile: breakpoint.file, lines: [String(breakpoint.line)] }, + tags: [], + template: 'Hello World!', + segments: [{ str: 'Hello World!' }], + captureSnapshot: false, + evaluateAt: 'EXIT', + ...overrides + } +} From f2a3601b09e2042d5b83ec9cf96008785f8b4b42 Mon Sep 17 00:00:00 2001 From: mhlidd Date: Thu, 12 Dec 2024 11:49:29 -0500 Subject: [PATCH 40/61] Add Support for DD_DOGSTATSD_HOST (#4989) * adding support for DD_DOGSTATSD_HOST * fixing typo * adding test --- packages/dd-trace/src/config.js | 3 ++- packages/dd-trace/test/config.spec.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 588dd5e8b9e..02c24207939 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -594,6 +594,7 @@ class Config { DD_DATA_STREAMS_ENABLED, DD_DBM_PROPAGATION_MODE, DD_DOGSTATSD_HOSTNAME, + DD_DOGSTATSD_HOST, DD_DOGSTATSD_PORT, DD_DYNAMIC_INSTRUMENTATION_ENABLED, DD_ENV, @@ -739,7 +740,7 @@ class Config { this._setBoolean(env, 'crashtracking.enabled', DD_CRASHTRACKING_ENABLED) this._setBoolean(env, 'codeOriginForSpans.enabled', DD_CODE_ORIGIN_FOR_SPANS_ENABLED) this._setString(env, 'dbmPropagationMode', DD_DBM_PROPAGATION_MODE) - this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOSTNAME) + this._setString(env, 'dogstatsd.hostname', DD_DOGSTATSD_HOST || DD_DOGSTATSD_HOSTNAME) this._setString(env, 'dogstatsd.port', DD_DOGSTATSD_PORT) this._setBoolean(env, 'dsmEnabled', DD_DATA_STREAMS_ENABLED) this._setBoolean(env, 'dynamicInstrumentationEnabled', DD_DYNAMIC_INSTRUMENTATION_ENABLED) diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 62fe403eaa8..7734708832e 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -1836,6 +1836,15 @@ describe('Config', () => { expect(config.appsec.apiSecurity.enabled).to.be.true }) + it('should prioritize DD_DOGSTATSD_HOST over DD_DOGSTATSD_HOSTNAME', () => { + process.env.DD_DOGSTATSD_HOSTNAME = 'dsd-agent' + process.env.DD_DOGSTATSD_HOST = 'localhost' + + const config = new Config() + + expect(config).to.have.nested.property('dogstatsd.hostname', 'localhost') + }) + context('auto configuration w/ unix domain sockets', () => { context('on windows', () => { it('should not be used', () => { From 95b6f956ead212573ad696e5ab7d7a617f67885a Mon Sep 17 00:00:00 2001 From: Fayssal DEFAA <82442451+faydef@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:37:56 +0100 Subject: [PATCH 41/61] update pyenv (#5005) --- benchmark/sirun/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/sirun/Dockerfile b/benchmark/sirun/Dockerfile index 5c6e883b62d..ad27d5d71b1 100644 --- a/benchmark/sirun/Dockerfile +++ b/benchmark/sirun/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ git hwinfo jq procps \ software-properties-common build-essential libnss3-dev zlib1g-dev libgdbm-dev libncurses5-dev libssl-dev libffi-dev libreadline-dev libsqlite3-dev libbz2-dev -RUN git clone --depth 1 https://github.com/pyenv/pyenv.git --branch "v2.0.4" --single-branch /pyenv +RUN git clone --depth 1 https://github.com/pyenv/pyenv.git --branch "v2.4.1" --single-branch /pyenv ENV PYENV_ROOT "/pyenv" ENV PATH "/pyenv/shims:/pyenv/bin:$PATH" RUN eval "$(pyenv init -)" From c6defbc8b552bb152c87ea891337d1ccccfcb797 Mon Sep 17 00:00:00 2001 From: Igor Unanua Date: Thu, 12 Dec 2024 18:39:19 +0100 Subject: [PATCH 42/61] enable log collection & log calls review (#4932) * Remove template literals * Enable log collection by default * Add a message to some log.error(err) * remove template lit * Add messages to request errors * Fix llmobs test * Some more messages * do not send 'Generic Error' with empty stack * Remove error type from message * A bunch more messages * Missing cypress plugin messages --- .../src/helpers/bundler-register.js | 6 +-- .../src/helpers/register.js | 5 +-- .../src/http/client.js | 2 +- packages/datadog-instrumentations/src/jest.js | 6 +-- .../src/playwright.js | 4 +- .../src/schema_iterator.js | 2 +- .../src/services/eventbridge.js | 2 +- .../src/services/kinesis.js | 2 +- .../src/services/lambda.js | 2 +- .../src/services/sqs.js | 2 +- .../src/cypress-plugin.js | 6 +-- packages/datadog-plugin-grpc/src/util.js | 2 +- packages/datadog-plugin-oracledb/src/index.js | 2 +- packages/datadog-shimmer/src/shimmer.js | 4 +- .../dynamic-instrumentation/index.js | 4 +- .../exporters/agentless/coverage-writer.js | 2 +- .../exporters/agentless/di-logs-writer.js | 2 +- .../exporters/agentless/index.js | 2 +- .../exporters/agentless/writer.js | 2 +- .../exporters/ci-visibility-exporter.js | 4 +- packages/dd-trace/src/config.js | 12 ++--- .../src/crashtracking/crashtracker.js | 4 +- packages/dd-trace/src/datastreams/writer.js | 4 +- .../src/debugger/devtools_client/config.js | 2 +- .../src/debugger/devtools_client/index.js | 2 +- .../debugger/devtools_client/remote_config.js | 2 +- .../src/debugger/devtools_client/status.js | 4 +- packages/dd-trace/src/debugger/index.js | 12 ++--- packages/dd-trace/src/dogstatsd.js | 4 +- .../dd-trace/src/exporters/agent/writer.js | 6 +-- .../dd-trace/src/exporters/common/request.js | 2 +- .../src/exporters/span-stats/writer.js | 2 +- packages/dd-trace/src/flare/index.js | 2 +- packages/dd-trace/src/lambda/runtime/ritm.js | 4 +- packages/dd-trace/src/llmobs/writers/base.js | 4 +- packages/dd-trace/src/opentracing/span.js | 2 +- packages/dd-trace/src/opentracing/tracer.js | 4 +- packages/dd-trace/src/plugins/ci_plugin.js | 6 +-- packages/dd-trace/src/plugins/plugin.js | 2 +- packages/dd-trace/src/plugins/util/git.js | 14 +++--- packages/dd-trace/src/plugins/util/test.js | 4 +- packages/dd-trace/src/plugins/util/web.js | 4 +- packages/dd-trace/src/proxy.js | 4 +- packages/dd-trace/src/runtime_metrics.js | 2 +- packages/dd-trace/src/serverless.js | 2 +- packages/dd-trace/src/span_processor.js | 20 ++++----- packages/dd-trace/src/tagger.js | 2 +- packages/dd-trace/src/telemetry/index.js | 1 + packages/dd-trace/src/telemetry/logs/index.js | 3 +- .../src/telemetry/logs/log-collector.js | 8 +++- packages/dd-trace/src/telemetry/send-data.js | 4 +- .../agentless/coverage-writer.spec.js | 2 +- .../exporters/agentless/writer.spec.js | 2 +- packages/dd-trace/test/config.spec.js | 26 ++++------- .../test/exporters/agent/writer.spec.js | 4 +- .../test/exporters/common/request.spec.js | 2 +- .../test/exporters/span-stats/writer.spec.js | 2 +- .../dd-trace/test/llmobs/writers/base.spec.js | 4 +- packages/dd-trace/test/proxy.spec.js | 3 +- .../test/telemetry/logs/index.spec.js | 7 ++- .../test/telemetry/logs/log-collector.spec.js | 44 ++++++++++++++----- 61 files changed, 165 insertions(+), 142 deletions(-) diff --git a/packages/datadog-instrumentations/src/helpers/bundler-register.js b/packages/datadog-instrumentations/src/helpers/bundler-register.js index a5dfead9669..6c11329bc36 100644 --- a/packages/datadog-instrumentations/src/helpers/bundler-register.js +++ b/packages/datadog-instrumentations/src/helpers/bundler-register.js @@ -30,12 +30,12 @@ dc.subscribe(CHANNEL, (payload) => { try { hooks[payload.package]() } catch (err) { - log.error(`esbuild-wrapped ${payload.package} missing in list of hooks`) + log.error('esbuild-wrapped %s missing in list of hooks', payload.package) throw err } if (!instrumentations[payload.package]) { - log.error(`esbuild-wrapped ${payload.package} missing in list of instrumentations`) + log.error('esbuild-wrapped %s missing in list of instrumentations', payload.package) return } @@ -47,7 +47,7 @@ dc.subscribe(CHANNEL, (payload) => { loadChannel.publish({ name, version: payload.version, file }) payload.module = hook(payload.module, payload.version) } catch (e) { - log.error(e) + log.error('Error executing bundler hook', e) } } }) diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 171db91e224..5a28f066c1f 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -103,8 +103,7 @@ for (const packageName of names) { try { version = version || getVersion(moduleBaseDir) } catch (e) { - log.error(`Error getting version for "${name}": ${e.message}`) - log.error(e) + log.error('Error getting version for "%s": %s', name, e.message, e) continue } if (typeof namesAndSuccesses[`${name}@${version}`] === 'undefined') { @@ -146,7 +145,7 @@ for (const packageName of names) { `integration:${name}`, `integration_version:${version}` ]) - log.info(`Found incompatible integration version: ${nameVersion}`) + log.info('Found incompatible integration version: %s', nameVersion) seenCombo.add(nameVersion) } } diff --git a/packages/datadog-instrumentations/src/http/client.js b/packages/datadog-instrumentations/src/http/client.js index 29547df61dc..6ab01a34513 100644 --- a/packages/datadog-instrumentations/src/http/client.js +++ b/packages/datadog-instrumentations/src/http/client.js @@ -39,7 +39,7 @@ function patch (http, methodName) { try { args = normalizeArgs.apply(null, arguments) } catch (e) { - log.error(e) + log.error('Error normalising http req arguments', e) return request.apply(this, arguments) } diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 0841ab4783a..fd13d2fc805 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -451,7 +451,7 @@ function cliWrapper (cli, jestVersion) { earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold } } catch (err) { - log.error(err) + log.error('Jest library configuration error', err) } if (isEarlyFlakeDetectionEnabled) { @@ -472,7 +472,7 @@ function cliWrapper (cli, jestVersion) { isEarlyFlakeDetectionEnabled = false } } catch (err) { - log.error(err) + log.error('Jest known tests error', err) } } @@ -491,7 +491,7 @@ function cliWrapper (cli, jestVersion) { skippableSuites = receivedSkippableSuites } } catch (err) { - log.error(err) + log.error('Jest test-suite skippable error', err) } } diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index ecc5f61521e..4eab55b1797 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -425,7 +425,7 @@ function runnerHook (runnerExport, playwrightVersion) { } } catch (e) { isEarlyFlakeDetectionEnabled = false - log.error(e) + log.error('Playwright session start error', e) } if (isEarlyFlakeDetectionEnabled && semver.gte(playwrightVersion, MINIMUM_SUPPORTED_VERSION_EFD)) { @@ -438,7 +438,7 @@ function runnerHook (runnerExport, playwrightVersion) { } } catch (err) { isEarlyFlakeDetectionEnabled = false - log.error(err) + log.error('Playwright known tests error', err) } } diff --git a/packages/datadog-plugin-avsc/src/schema_iterator.js b/packages/datadog-plugin-avsc/src/schema_iterator.js index c748bbf9e75..44fce95a765 100644 --- a/packages/datadog-plugin-avsc/src/schema_iterator.js +++ b/packages/datadog-plugin-avsc/src/schema_iterator.js @@ -110,7 +110,7 @@ class SchemaExtractor { } for (const field of schema.fields) { if (!this.extractProperty(field, schemaName, field.name, builder, depth)) { - log.warn(`DSM: Unable to extract field with name: ${field.name} from Avro schema with name: ${schemaName}`) + log.warn('DSM: Unable to extract field with name: %s from Avro schema with name: %s', field.name, schemaName) } } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js index b316f75e6be..a5ca5f08de1 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +++ b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js @@ -45,7 +45,7 @@ class EventBridge extends BaseAwsSdkPlugin { } request.params.Entries[0].Detail = finalData } catch (e) { - log.error(e) + log.error('EventBridge error injecting request', e) } } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js index 64a67d768ea..cdbd7c077e9 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +++ b/packages/datadog-plugin-aws-sdk/src/services/kinesis.js @@ -97,7 +97,7 @@ class Kinesis extends BaseAwsSdkPlugin { parsedAttributes: decodedData._datadog } } catch (e) { - log.error(e) + log.error('Kinesis error extracting response', e) } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/lambda.js b/packages/datadog-plugin-aws-sdk/src/services/lambda.js index f6ea874872e..b5fe1981c20 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/lambda.js +++ b/packages/datadog-plugin-aws-sdk/src/services/lambda.js @@ -43,7 +43,7 @@ class Lambda extends BaseAwsSdkPlugin { const newContextBase64 = Buffer.from(JSON.stringify(clientContext)).toString('base64') request.params.ClientContext = newContextBase64 } catch (err) { - log.error(err) + log.error('Lambda error injecting request', err) } } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/sqs.js b/packages/datadog-plugin-aws-sdk/src/services/sqs.js index e3a76c3e0b9..9857e46bf28 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/sqs.js +++ b/packages/datadog-plugin-aws-sdk/src/services/sqs.js @@ -163,7 +163,7 @@ class Sqs extends BaseAwsSdkPlugin { return JSON.parse(buffer) } } catch (e) { - log.error(e) + log.error('Sqs error parsing DD attributes', e) } } diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index 0a7d0debe48..2ed62070fda 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -223,7 +223,7 @@ class CypressPlugin { this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration) .then((libraryConfigurationResponse) => { if (libraryConfigurationResponse.err) { - log.error(libraryConfigurationResponse.err) + log.error('Cypress plugin library config response error', libraryConfigurationResponse.err) } else { const { libraryConfig: { @@ -360,7 +360,7 @@ class CypressPlugin { this.testConfiguration ) if (knownTestsResponse.err) { - log.error(knownTestsResponse.err) + log.error('Cypress known tests response error', knownTestsResponse.err) this.isEarlyFlakeDetectionEnabled = false } else { // We use TEST_FRAMEWORK_NAME for the name of the module @@ -374,7 +374,7 @@ class CypressPlugin { this.testConfiguration ) if (skippableTestsResponse.err) { - log.error(skippableTestsResponse.err) + log.error('Cypress skippable tests response error', skippableTestsResponse.err) } else { const { skippableTests, correlationId } = skippableTestsResponse this.testsToSkip = skippableTests || [] diff --git a/packages/datadog-plugin-grpc/src/util.js b/packages/datadog-plugin-grpc/src/util.js index 1c1937e7ea7..ec7d0f33570 100644 --- a/packages/datadog-plugin-grpc/src/util.js +++ b/packages/datadog-plugin-grpc/src/util.js @@ -54,7 +54,7 @@ module.exports = { } if (config.hasOwnProperty(filter)) { - log.error(`Expected '${filter}' to be an array or function.`) + log.error('Expected \'%s\' to be an array or function.', filter) } return () => ({}) diff --git a/packages/datadog-plugin-oracledb/src/index.js b/packages/datadog-plugin-oracledb/src/index.js index 7c2f1da029f..eb4fa037cac 100644 --- a/packages/datadog-plugin-oracledb/src/index.js +++ b/packages/datadog-plugin-oracledb/src/index.js @@ -33,7 +33,7 @@ function getUrl (connectString) { try { return new URL(`http://${connectString}`) } catch (e) { - log.error(e) + log.error('Invalid oracle connection string', e) return {} } } diff --git a/packages/datadog-shimmer/src/shimmer.js b/packages/datadog-shimmer/src/shimmer.js index d12c4c130ef..0285c5e5083 100644 --- a/packages/datadog-shimmer/src/shimmer.js +++ b/packages/datadog-shimmer/src/shimmer.js @@ -136,7 +136,7 @@ function wrapMethod (target, name, wrapper, noAssert) { if (callState.completed) { // error was thrown after original function returned/resolved, so // it was us. log it. - log.error(e) + log.error('Shimmer error was thrown after original function returned/resolved', e) // original ran and returned something. return it. return callState.retVal } @@ -144,7 +144,7 @@ function wrapMethod (target, name, wrapper, noAssert) { if (!callState.called) { // error was thrown before original function was called, so // it was us. log it. - log.error(e) + log.error('Shimmer error was thrown before original function was called', e) // original never ran. call it unwrapped. return original.apply(this, args) } diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index ef65489e60d..ec6e2a1fd75 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -90,8 +90,8 @@ class TestVisDynamicInstrumentation { } }).unref() - this.worker.on('error', (err) => log.error(err)) - this.worker.on('messageerror', (err) => log.error(err)) + this.worker.on('error', (err) => log.error('ci-visibility DI worker error', err)) + this.worker.on('messageerror', (err) => log.error('ci-visibility DI worker messageerror', err)) } } diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js index 98eff61a6fd..a36b07201e1 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js @@ -63,7 +63,7 @@ class Writer extends BaseWriter { TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, { endpoint: 'code_coverage' } ) - log.error(err) + log.error('Error sending CI coverage payload', err) done() return } diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js index eebc3c5e6a9..7d8c5ba47a0 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js @@ -40,7 +40,7 @@ class DynamicInstrumentationLogsWriter extends BaseWriter { request(data, options, (err, res) => { if (err) { - log.error(err) + log.error('Error sending DI logs payload', err) done() return } diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js index 5895bb573cd..a5b677ef98b 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js @@ -38,7 +38,7 @@ class AgentlessCiVisibilityExporter extends CiVisibilityExporter { apiUrl = new URL(apiUrl) this._apiUrl = apiUrl } catch (e) { - log.error(e) + log.error('Error setting CI exporter api url', e) } } diff --git a/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js b/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js index 466c5230b22..34cad3862bc 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +++ b/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js @@ -64,7 +64,7 @@ class Writer extends BaseWriter { TELEMETRY_ENDPOINT_PAYLOAD_DROPPED, { endpoint: 'test_cycle' } ) - log.error(err) + log.error('Error sending CI agentless payload', err) done() return } diff --git a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js index 0a12d5f8c5a..dde5955bc75 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +++ b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js @@ -225,7 +225,7 @@ class CiVisibilityExporter extends AgentInfoExporter { repositoryUrl, (err) => { if (err) { - log.error(`Error uploading git metadata: ${err.message}`) + log.error('Error uploading git metadata: %s', err.message) } else { log.debug('Successfully uploaded git metadata') } @@ -345,7 +345,7 @@ class CiVisibilityExporter extends AgentInfoExporter { this._writer.setUrl(url) this._coverageWriter.setUrl(coverageUrl) } catch (e) { - log.error(e) + log.error('Error setting CI exporter url', e) } } diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 02c24207939..808704bd7e4 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -145,7 +145,7 @@ function maybeFile (filepath) { try { return fs.readFileSync(filepath, 'utf8') } catch (e) { - log.error(e) + log.error('Error reading file %s', filepath, e) return undefined } } @@ -378,7 +378,7 @@ class Config { } catch (e) { // Only log error if the user has set a git.properties path if (process.env.DD_GIT_PROPERTIES_FILE) { - log.error(e) + log.error('Error reading DD_GIT_PROPERTIES_FILE: %s', DD_GIT_PROPERTIES_FILE, e) } } if (gitPropertiesString) { @@ -553,7 +553,7 @@ class Config { this._setValue(defaults, 'telemetry.dependencyCollection', true) this._setValue(defaults, 'telemetry.enabled', true) this._setValue(defaults, 'telemetry.heartbeatInterval', 60000) - this._setValue(defaults, 'telemetry.logCollection', false) + this._setValue(defaults, 'telemetry.logCollection', true) this._setValue(defaults, 'telemetry.metrics', true) this._setValue(defaults, 'traceEnabled', true) this._setValue(defaults, 'traceId128BitGenerationEnabled', true) @@ -1143,12 +1143,6 @@ class Config { calc['tracePropagationStyle.extract'] = calc['tracePropagationStyle.extract'] || defaultPropagationStyle } - const iastEnabled = coalesce(this._options['iast.enabled'], this._env['iast.enabled']) - const profilingEnabled = coalesce(this._options['profiling.enabled'], this._env['profiling.enabled']) - const injectionIncludesProfiler = (this._env.injectionEnabled || []).includes('profiler') - if (iastEnabled || ['auto', 'true'].includes(profilingEnabled) || injectionIncludesProfiler) { - this._setBoolean(calc, 'telemetry.logCollection', true) - } if (this._env.injectionEnabled?.length > 0) { this._setBoolean(calc, 'crashtracking.enabled', true) } diff --git a/packages/dd-trace/src/crashtracking/crashtracker.js b/packages/dd-trace/src/crashtracking/crashtracker.js index fc42195c953..a2d3ec2eb52 100644 --- a/packages/dd-trace/src/crashtracking/crashtracker.js +++ b/packages/dd-trace/src/crashtracking/crashtracker.js @@ -20,7 +20,7 @@ class Crashtracker { binding.updateConfig(this._getConfig(config)) binding.updateMetadata(this._getMetadata(config)) } catch (e) { - log.error(e) + log.error('Error configuring crashtracker', e) } } @@ -36,7 +36,7 @@ class Crashtracker { this._getMetadata(config) ) } catch (e) { - log.error(e) + log.error('Error initialising crashtracker', e) } } diff --git a/packages/dd-trace/src/datastreams/writer.js b/packages/dd-trace/src/datastreams/writer.js index f8c9e021ecc..5f789f2e056 100644 --- a/packages/dd-trace/src/datastreams/writer.js +++ b/packages/dd-trace/src/datastreams/writer.js @@ -45,13 +45,13 @@ class DataStreamsWriter { zlib.gzip(encodedPayload, { level: 1 }, (err, compressedData) => { if (err) { - log.error(err) + log.error('Error zipping datastream', err) return } makeRequest(compressedData, this._url, (err, res) => { log.debug(`Response from the agent: ${res}`) if (err) { - log.error(err) + log.error('Error sending datastream', err) } }) }) diff --git a/packages/dd-trace/src/debugger/devtools_client/config.js b/packages/dd-trace/src/debugger/devtools_client/config.js index 838a1a76cca..fa48779f313 100644 --- a/packages/dd-trace/src/debugger/devtools_client/config.js +++ b/packages/dd-trace/src/debugger/devtools_client/config.js @@ -15,7 +15,7 @@ const config = module.exports = { updateUrl(parentConfig) configPort.on('message', updateUrl) -configPort.on('messageerror', (err) => log.error(err)) +configPort.on('messageerror', (err) => log.error('Debugger config messageerror', err)) function updateUrl (updates) { config.url = updates.url || format({ diff --git a/packages/dd-trace/src/debugger/devtools_client/index.js b/packages/dd-trace/src/debugger/devtools_client/index.js index db71e7028e7..116688c2183 100644 --- a/packages/dd-trace/src/debugger/devtools_client/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/index.js @@ -93,7 +93,7 @@ session.on('Debugger.paused', async ({ params }) => { // TODO: Process template (DEBUG-2628) send(probe.template, logger, snapshot, (err) => { - if (err) log.error(err) + if (err) log.error('Debugger error', err) else ackEmitting(probe) }) } diff --git a/packages/dd-trace/src/debugger/devtools_client/remote_config.js b/packages/dd-trace/src/debugger/devtools_client/remote_config.js index 165a68ce503..66d82fae81f 100644 --- a/packages/dd-trace/src/debugger/devtools_client/remote_config.js +++ b/packages/dd-trace/src/debugger/devtools_client/remote_config.js @@ -41,7 +41,7 @@ rcPort.on('message', async ({ action, conf: probe, ackId }) => { ackError(err, probe) } }) -rcPort.on('messageerror', (err) => log.error(err)) +rcPort.on('messageerror', (err) => log.error('Debugger RC message error', err)) async function processMsg (action, probe) { log.debug(`Received request to ${action} ${probe.type} probe (id: ${probe.id}, version: ${probe.version})`) diff --git a/packages/dd-trace/src/debugger/devtools_client/status.js b/packages/dd-trace/src/debugger/devtools_client/status.js index a18480d4037..32e4fb42834 100644 --- a/packages/dd-trace/src/debugger/devtools_client/status.js +++ b/packages/dd-trace/src/debugger/devtools_client/status.js @@ -55,7 +55,7 @@ function ackEmitting ({ id: probeId, version }) { } function ackError (err, { id: probeId, version }) { - log.error(err) + log.error('Debugger ackError', err) onlyUniqueUpdates(STATUSES.ERROR, probeId, version, () => { const payload = statusPayload(probeId, version, STATUSES.ERROR) @@ -87,7 +87,7 @@ function send (payload) { } request(form, options, (err) => { - if (err) log.error(err) + if (err) log.error('Error sending debugger payload', err) }) } diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index ea2a36d4d25..35cfb2630df 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -33,14 +33,14 @@ function start (config, rc) { const ack = rcAckCallbacks.get(ackId) if (ack === undefined) { // This should never happen, but just in case something changes in the future, we should guard against it - log.error(`Received an unknown ackId: ${ackId}`) - if (error) log.error(error) + log.error('Received an unknown ackId: %s', ackId) + if (error) log.error('Error starting Dynamic Instrumentation client', error) return } ack(error) rcAckCallbacks.delete(ackId) }) - rcChannel.port2.on('messageerror', (err) => log.error(err)) + rcChannel.port2.on('messageerror', (err) => log.error('Debugger RC messageerror', err)) worker = new Worker( join(__dirname, 'devtools_client', 'index.js'), @@ -61,13 +61,13 @@ function start (config, rc) { log.debug(`Dynamic Instrumentation worker thread started successfully (thread id: ${worker.threadId})`) }) - worker.on('error', (err) => log.error(err)) - worker.on('messageerror', (err) => log.error(err)) + worker.on('error', (err) => log.error('Debugger worker error', err)) + worker.on('messageerror', (err) => log.error('Debugger worker messageerror', err)) worker.on('exit', (code) => { const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`) - log.error(error) + log.error('Debugger worker exited unexpectedly', error) // Be nice, clean up now that the worker thread encounted an issue and we can't continue rc.removeProductHandler('LIVE_DEBUGGING') diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index ba84de71341..a396c9e98a4 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -71,7 +71,7 @@ class DogStatsDClient { const buffer = Buffer.concat(queue) request(buffer, this._httpOptions, (err) => { if (err) { - log.error('HTTP error from agent: ' + err.stack) + log.error('DogStatsDClient: HTTP error from agent: %s', err.message, err) if (err.status === 404) { // Inside this if-block, we have connectivity to the agent, but // we're not getting a 200 from the proxy endpoint. If it's a 404, @@ -89,7 +89,7 @@ class DogStatsDClient { this._sendUdpFromQueue(queue, this._host, this._family) } else { lookup(this._host, (err, address, family) => { - if (err) return log.error(err) + if (err) return log.error('DogStatsDClient: Host not found', err) this._sendUdpFromQueue(queue, address, family) }) } diff --git a/packages/dd-trace/src/exporters/agent/writer.js b/packages/dd-trace/src/exporters/agent/writer.js index 82a28647778..8fac323e614 100644 --- a/packages/dd-trace/src/exporters/agent/writer.js +++ b/packages/dd-trace/src/exporters/agent/writer.js @@ -41,17 +41,17 @@ class Writer extends BaseWriter { startupLog({ agentError: err }) if (err) { - log.error(err) + log.error('Error sending payload to the agent (status code: %s)', err.status, err) done() return } - log.debug(`Response from the agent: ${res}`) + log.debug('Response from the agent: %s', res) try { this._prioritySampler.update(JSON.parse(res).rate_by_service) } catch (e) { - log.error(e) + log.error('Error updating prioritySampler rates', e) runtimeMetrics.increment(`${METRIC_PREFIX}.errors`, true) runtimeMetrics.increment(`${METRIC_PREFIX}.errors.by.name`, `name:${e.name}`, true) diff --git a/packages/dd-trace/src/exporters/common/request.js b/packages/dd-trace/src/exporters/common/request.js index ab8b697eef6..2ff90236ee8 100644 --- a/packages/dd-trace/src/exporters/common/request.js +++ b/packages/dd-trace/src/exporters/common/request.js @@ -86,7 +86,7 @@ function request (data, options, callback) { if (isGzip) { zlib.gunzip(buffer, (err, result) => { if (err) { - log.error(`Could not gunzip response: ${err.message}`) + log.error('Could not gunzip response: %s', err.message) callback(null, '', res.statusCode) } else { callback(null, result.toString(), res.statusCode) diff --git a/packages/dd-trace/src/exporters/span-stats/writer.js b/packages/dd-trace/src/exporters/span-stats/writer.js index 3ece6d221b4..37cd6c77d5e 100644 --- a/packages/dd-trace/src/exporters/span-stats/writer.js +++ b/packages/dd-trace/src/exporters/span-stats/writer.js @@ -16,7 +16,7 @@ class Writer extends BaseWriter { _sendPayload (data, _, done) { makeRequest(data, this._url, (err, res) => { if (err) { - log.error(err) + log.error('Error sending span stats', err) done() return } diff --git a/packages/dd-trace/src/flare/index.js b/packages/dd-trace/src/flare/index.js index 70ec4ccd75e..4a5166d45e1 100644 --- a/packages/dd-trace/src/flare/index.js +++ b/packages/dd-trace/src/flare/index.js @@ -83,7 +83,7 @@ const flare = { headers: form.getHeaders() }, (err) => { if (err) { - log.error(err) + log.error('Error sending flare payload', err) } }) } diff --git a/packages/dd-trace/src/lambda/runtime/ritm.js b/packages/dd-trace/src/lambda/runtime/ritm.js index 4dd27713a0b..ec50a4a80be 100644 --- a/packages/dd-trace/src/lambda/runtime/ritm.js +++ b/packages/dd-trace/src/lambda/runtime/ritm.js @@ -101,7 +101,7 @@ const registerLambdaHook = () => { try { moduleExports = hook(moduleExports) } catch (e) { - log.error(e) + log.error('Error executing lambda hook', e) } } @@ -120,7 +120,7 @@ const registerLambdaHook = () => { try { moduleExports = hook(moduleExports) } catch (e) { - log.error(e) + log.error('Error executing lambda hook for datadog-lambda-js', e) } } } diff --git a/packages/dd-trace/src/llmobs/writers/base.js b/packages/dd-trace/src/llmobs/writers/base.js index 8a6cdae9c2f..1d33bc653ad 100644 --- a/packages/dd-trace/src/llmobs/writers/base.js +++ b/packages/dd-trace/src/llmobs/writers/base.js @@ -74,11 +74,11 @@ class BaseLLMObsWriter { request(payload, options, (err, resp, code) => { if (err) { logger.error( - `Error sending ${events.length} LLMObs ${this._eventType} events to ${this._url}: ${err.message}` + 'Error sending %d LLMObs %s events to %s: %s', events.length, this._eventType, this._url, err.message, err ) } else if (code >= 300) { logger.error( - `Error sending ${events.length} LLMObs ${this._eventType} events to ${this._url}: ${code}` + 'Error sending %d LLMObs %s events to %s: %s', events.length, this._eventType, this._url, code ) } else { logger.debug(`Sent ${events.length} LLMObs ${this._eventType} events to ${this._url}`) diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index e855e504e17..00fd51da027 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -214,7 +214,7 @@ class DatadogSpan { if (DD_TRACE_EXPERIMENTAL_STATE_TRACKING === 'true') { if (!this._spanContext._tags['service.name']) { - log.error(`Finishing invalid span: ${this}`) + log.error('Finishing invalid span: %s', this) } } diff --git a/packages/dd-trace/src/opentracing/tracer.js b/packages/dd-trace/src/opentracing/tracer.js index 2d854442cc3..4ae30ca93ac 100644 --- a/packages/dd-trace/src/opentracing/tracer.js +++ b/packages/dd-trace/src/opentracing/tracer.js @@ -91,7 +91,7 @@ class DatadogTracer { } this._propagators[format].inject(context, carrier) } catch (e) { - log.error(e) + log.error('Error injecting trace', e) runtimeMetrics.increment('datadog.tracer.node.inject.errors', true) } } @@ -100,7 +100,7 @@ class DatadogTracer { try { return this._propagators[format].extract(carrier) } catch (e) { - log.error(e) + log.error('Error extracting trace', e) runtimeMetrics.increment('datadog.tracer.node.extract.errors', true) return null } diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index dccf518eb1e..a2f8948bf49 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -49,7 +49,7 @@ module.exports = class CiPlugin extends Plugin { } this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => { if (err) { - log.error(`Library configuration could not be fetched. ${err.message}`) + log.error('Library configuration could not be fetched. %s', err.message) } else { this.libraryConfig = libraryConfig } @@ -63,7 +63,7 @@ module.exports = class CiPlugin extends Plugin { } this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => { if (err) { - log.error(`Skippable suites could not be fetched. ${err.message}`) + log.error('Skippable suites could not be fetched. %s', err.message) } else { this.itrCorrelationId = itrCorrelationId } @@ -152,7 +152,7 @@ module.exports = class CiPlugin extends Plugin { } this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => { if (err) { - log.error(`Known tests could not be fetched. ${err.message}`) + log.error('Known tests could not be fetched. %s', err.message) this.libraryConfig.isEarlyFlakeDetectionEnabled = false } onDone({ err, knownTests }) diff --git a/packages/dd-trace/src/plugins/plugin.js b/packages/dd-trace/src/plugins/plugin.js index 78a49b62b14..e8d9c911a69 100644 --- a/packages/dd-trace/src/plugins/plugin.js +++ b/packages/dd-trace/src/plugins/plugin.js @@ -79,7 +79,7 @@ module.exports = class Plugin { return handler.apply(this, arguments) } catch (e) { logger.error('Error in plugin handler:', e) - logger.info('Disabling plugin:', plugin.id) + logger.info('Disabling plugin: %s', plugin.id) plugin.configure(false) } } diff --git a/packages/dd-trace/src/plugins/util/git.js b/packages/dd-trace/src/plugins/util/git.js index 06b9521817f..47707a48679 100644 --- a/packages/dd-trace/src/plugins/util/git.js +++ b/packages/dd-trace/src/plugins/util/git.js @@ -61,7 +61,7 @@ function sanitizedExec ( exitCode: err.status || err.errno }) } - log.error(err) + log.error('Git plugin error executing command', err) return '' } finally { storage.enterWith(store) @@ -144,7 +144,7 @@ function unshallowRepository () { ], { stdio: 'pipe' }) } catch (err) { // If the local HEAD is a commit that has not been pushed to the remote, the above command will fail. - log.error(err) + log.error('Git plugin error executing git command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno } @@ -157,7 +157,7 @@ function unshallowRepository () { ], { stdio: 'pipe' }) } catch (err) { // If the CI is working on a detached HEAD or branch tracking hasn’t been set up, the above command will fail. - log.error(err) + log.error('Git plugin error executing fallback git command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno } @@ -196,7 +196,7 @@ function getLatestCommits () { distributionMetric(TELEMETRY_GIT_COMMAND_MS, { command: 'get_local_commits' }, Date.now() - startTime) return result } catch (err) { - log.error(`Get latest commits failed: ${err.message}`) + log.error('Get latest commits failed: %s', err.message) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_local_commits', errorType: err.status } @@ -229,7 +229,7 @@ function getCommitsRevList (commitsToExclude, commitsToInclude) { .split('\n') .filter(commit => commit) } catch (err) { - log.error(`Get commits to upload failed: ${err.message}`) + log.error('Get commits to upload failed: %s', err.message) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'get_objects', errorType: err.code, exitCode: err.status || err.errno } // err.status might be null @@ -272,7 +272,7 @@ function generatePackFilesForCommits (commitsToUpload) { try { result = execGitPackObjects(temporaryPath) } catch (err) { - log.error(err) + log.error('Git plugin error executing git pack-objects command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code } @@ -292,7 +292,7 @@ function generatePackFilesForCommits (commitsToUpload) { try { result = execGitPackObjects(cwdPath) } catch (err) { - log.error(err) + log.error('Git plugin error executing fallback git pack-objects command', err) incrementCountMetric( TELEMETRY_GIT_COMMAND_ERRORS, { command: 'pack_objects', exitCode: err.status || err.errno, errorType: err.code } diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 8719c916915..633b1f14361 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -218,13 +218,13 @@ function removeInvalidMetadata (metadata) { return Object.keys(metadata).reduce((filteredTags, tag) => { if (tag === GIT_REPOSITORY_URL) { if (!validateGitRepositoryUrl(metadata[GIT_REPOSITORY_URL])) { - log.error(`Repository URL is not a valid repository URL: ${metadata[GIT_REPOSITORY_URL]}.`) + log.error('Repository URL is not a valid repository URL: %s.', metadata[GIT_REPOSITORY_URL]) return filteredTags } } if (tag === GIT_COMMIT_SHA) { if (!validateGitCommitSha(metadata[GIT_COMMIT_SHA])) { - log.error(`Git commit SHA must be a full-length git SHA: ${metadata[GIT_COMMIT_SHA]}.`) + log.error('Git commit SHA must be a full-length git SHA: %s.', metadata[GIT_COMMIT_SHA]) return filteredTags } } diff --git a/packages/dd-trace/src/plugins/util/web.js b/packages/dd-trace/src/plugins/util/web.js index 5bfb1d6fad4..2d92c74ea91 100644 --- a/packages/dd-trace/src/plugins/util/web.js +++ b/packages/dd-trace/src/plugins/util/web.js @@ -546,7 +546,7 @@ function getHeadersToRecord (config) { .map(h => h.split(':')) .map(([key, tag]) => [key.toLowerCase(), tag]) } catch (err) { - log.error(err) + log.error('Web plugin error getting headers', err) } } else if (config.hasOwnProperty('headers')) { log.error('Expected `headers` to be an array of strings.') @@ -595,7 +595,7 @@ function getQsObfuscator (config) { try { return new RegExp(obfuscator, 'gi') } catch (err) { - log.error(err) + log.error('Web plugin error getting qs obfuscator', err) } } diff --git a/packages/dd-trace/src/proxy.js b/packages/dd-trace/src/proxy.js index 81d003eebb7..fd814c9d6e3 100644 --- a/packages/dd-trace/src/proxy.js +++ b/packages/dd-trace/src/proxy.js @@ -187,7 +187,7 @@ class Tracer extends NoopProxy { testVisibilityDynamicInstrumentation.start() } } catch (e) { - log.error(e) + log.error('Error initialising tracer', e) } return this @@ -198,7 +198,7 @@ class Tracer extends NoopProxy { try { return require('./profiler').start(config) } catch (e) { - log.error(e) + log.error('Error starting profiler', e) } } diff --git a/packages/dd-trace/src/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics.js index b2711879a05..49e724eb11c 100644 --- a/packages/dd-trace/src/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics.js @@ -32,7 +32,7 @@ module.exports = { nativeMetrics = require('@datadog/native-metrics') nativeMetrics.start() } catch (e) { - log.error(e) + log.error('Error starting native metrics', e) nativeMetrics = null } diff --git a/packages/dd-trace/src/serverless.js b/packages/dd-trace/src/serverless.js index d352cae899e..415df38fc2c 100644 --- a/packages/dd-trace/src/serverless.js +++ b/packages/dd-trace/src/serverless.js @@ -23,7 +23,7 @@ function maybeStartServerlessMiniAgent (config) { try { require('child_process').spawn(rustBinaryPath, { stdio: 'inherit' }) } catch (err) { - log.error(`Error spawning mini agent process: ${err}`) + log.error('Error spawning mini agent process: %s', err.message) } } diff --git a/packages/dd-trace/src/span_processor.js b/packages/dd-trace/src/span_processor.js index deb92c02f34..46cf51b162b 100644 --- a/packages/dd-trace/src/span_processor.js +++ b/packages/dd-trace/src/span_processor.js @@ -87,22 +87,22 @@ class SpanProcessor { const id = context.toSpanId() if (finished.has(span)) { - log.error(`Span was already finished in the same trace: ${span}`) + log.error('Span was already finished in the same trace: %s', span) } else { finished.add(span) if (finishedIds.has(id)) { - log.error(`Another span with the same ID was already finished in the same trace: ${span}`) + log.error('Another span with the same ID was already finished in the same trace: %s', span) } else { finishedIds.add(id) } if (context._trace !== trace) { - log.error(`A span was finished in the wrong trace: ${span}.`) + log.error('A span was finished in the wrong trace: %s', span) } if (finishedSpans.has(span)) { - log.error(`Span was already finished in a different trace: ${span}`) + log.error('Span was already finished in a different trace: %s', span) } else { finishedSpans.add(span) } @@ -114,35 +114,35 @@ class SpanProcessor { const id = context.toSpanId() if (started.has(span)) { - log.error(`Span was already started in the same trace: ${span}`) + log.error('Span was already started in the same trace: %s', span) } else { started.add(span) if (startedIds.has(id)) { - log.error(`Another span with the same ID was already started in the same trace: ${span}`) + log.error('Another span with the same ID was already started in the same trace: %s', span) } else { startedIds.add(id) } if (context._trace !== trace) { - log.error(`A span was started in the wrong trace: ${span}.`) + log.error('A span was started in the wrong trace: %s', span) } if (startedSpans.has(span)) { - log.error(`Span was already started in a different trace: ${span}`) + log.error('Span was already started in a different trace: %s', span) } else { startedSpans.add(span) } } if (!finished.has(span)) { - log.error(`Span started in one trace but was finished in another trace: ${span}`) + log.error('Span started in one trace but was finished in another trace: %s', span) } } for (const span of trace.finished) { if (!started.has(span)) { - log.error(`Span finished in one trace but was started in another trace: ${span}`) + log.error('Span finished in one trace but was started in another trace: %s', span) } } } diff --git a/packages/dd-trace/src/tagger.js b/packages/dd-trace/src/tagger.js index 41c8616a086..bbd8a187940 100644 --- a/packages/dd-trace/src/tagger.js +++ b/packages/dd-trace/src/tagger.js @@ -44,7 +44,7 @@ function add (carrier, keyValuePairs, parseOtelTags = false) { Object.assign(carrier, keyValuePairs) } } catch (e) { - log.error(e) + log.error('Error adding tags', e) } } diff --git a/packages/dd-trace/src/telemetry/index.js b/packages/dd-trace/src/telemetry/index.js index 5df7d6fcae3..eb1fe376c67 100644 --- a/packages/dd-trace/src/telemetry/index.js +++ b/packages/dd-trace/src/telemetry/index.js @@ -137,6 +137,7 @@ function appClosing () { sendData(config, application, host, reqType, payload) // We flush before shutting down. metricsManager.send(config, application, host) + telemetryLogger.send(config, application, host) } function onBeforeExit () { diff --git a/packages/dd-trace/src/telemetry/logs/index.js b/packages/dd-trace/src/telemetry/logs/index.js index c535acb9cdd..d8fa1969e55 100644 --- a/packages/dd-trace/src/telemetry/logs/index.js +++ b/packages/dd-trace/src/telemetry/logs/index.js @@ -47,8 +47,7 @@ function onErrorLog (msg) { if (cause) { telLog.stack_trace = cause.stack - const errorType = cause.name ?? 'Error' - telLog.message = `${errorType}: ${telLog.message}` + telLog.errorType = cause.constructor.name } onLog(telLog) diff --git a/packages/dd-trace/src/telemetry/logs/log-collector.js b/packages/dd-trace/src/telemetry/logs/log-collector.js index 9103fd1c47d..a15f5ba4b3e 100644 --- a/packages/dd-trace/src/telemetry/logs/log-collector.js +++ b/packages/dd-trace/src/telemetry/logs/log-collector.js @@ -47,8 +47,14 @@ function sanitize (logEntry) { .filter((line, index) => (isDDCode && index < firstIndex) || line.includes(ddBasePath)) .map(line => line.replace(ddBasePath, '')) + if (!isDDCode && logEntry.errorType && stackLines.length) { + stackLines = [`${logEntry.errorType}: redacted`, ...stackLines] + } + + delete logEntry.errorType + logEntry.stack_trace = stackLines.join(EOL) - if (logEntry.stack_trace === '' && !logEntry.message) { + if (logEntry.stack_trace === '' && (!logEntry.message || logEntry.message === 'Generic Error')) { // If entire stack was removed and there is no message we'd rather not log it at all. return null } diff --git a/packages/dd-trace/src/telemetry/send-data.js b/packages/dd-trace/src/telemetry/send-data.js index 813fa427812..81406910c27 100644 --- a/packages/dd-trace/src/telemetry/send-data.js +++ b/packages/dd-trace/src/telemetry/send-data.js @@ -57,7 +57,7 @@ function sendData (config, application, host, reqType, payload = {}, cb = () => try { url = url || new URL(getAgentlessTelemetryEndpoint(config.site)) } catch (err) { - log.error(err) + log.error('Telemetry endpoint url is invalid', err) // No point to do the request if the URL is invalid return cb(err, { payload, reqType }) } @@ -100,7 +100,7 @@ function sendData (config, application, host, reqType, payload = {}, cb = () => path: '/api/v2/apmtelemetry' } if (backendUrl) { - request(data, backendOptions, (error) => { log.error(error) }) + request(data, backendOptions, (error) => { log.error('Error sending telemetry data', error) }) } else { log.error('Invalid Telemetry URL') } diff --git a/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js index 62e10e9753e..61ffee21181 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agentless/coverage-writer.spec.js @@ -111,7 +111,7 @@ describe('CI Visibility Coverage Writer', () => { encoder.makePayload.returns(payload) coverageWriter.flush(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error).to.have.been.calledWith('Error sending CI coverage payload', error) done() }) }) diff --git a/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js b/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js index 85765c6bf3a..29ac58fbd31 100644 --- a/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js +++ b/packages/dd-trace/test/ci-visibility/exporters/agentless/writer.spec.js @@ -113,7 +113,7 @@ describe('CI Visibility Writer', () => { encoder.count.returns(1) writer.flush(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error).to.have.been.calledWith('Error sending CI agentless payload', error) done() }) }) diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 7734708832e..1eb711dbd2c 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -388,7 +388,7 @@ describe('Config', () => { { name: 'telemetry.dependencyCollection', value: true, origin: 'default' }, { name: 'telemetry.enabled', value: true, origin: 'env_var' }, { name: 'telemetry.heartbeatInterval', value: 60000, origin: 'default' }, - { name: 'telemetry.logCollection', value: false, origin: 'default' }, + { name: 'telemetry.logCollection', value: true, origin: 'default' }, { name: 'telemetry.metrics', value: true, origin: 'default' }, { name: 'traceId128BitGenerationEnabled', value: true, origin: 'default' }, { name: 'traceId128BitLoggingEnabled', value: false, origin: 'default' }, @@ -1585,7 +1585,7 @@ describe('Config', () => { expect(config.telemetry).to.not.be.undefined expect(config.telemetry.enabled).to.be.true expect(config.telemetry.heartbeatInterval).to.eq(60000) - expect(config.telemetry.logCollection).to.be.false + expect(config.telemetry.logCollection).to.be.true expect(config.telemetry.debug).to.be.false expect(config.telemetry.metrics).to.be.true }) @@ -1623,7 +1623,7 @@ describe('Config', () => { process.env.DD_TELEMETRY_METRICS_ENABLED = origTelemetryMetricsEnabledValue }) - it('should not set DD_TELEMETRY_LOG_COLLECTION_ENABLED', () => { + it('should disable log collection if DD_TELEMETRY_LOG_COLLECTION_ENABLED is false', () => { const origLogsValue = process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED = 'false' @@ -1634,17 +1634,6 @@ describe('Config', () => { process.env.DD_TELEMETRY_LOG_COLLECTION_ENABLED = origLogsValue }) - it('should set DD_TELEMETRY_LOG_COLLECTION_ENABLED if DD_IAST_ENABLED', () => { - const origIastEnabledValue = process.env.DD_IAST_ENABLED - process.env.DD_IAST_ENABLED = 'true' - - const config = new Config() - - expect(config.telemetry.logCollection).to.be.true - - process.env.DD_IAST_ENABLED = origIastEnabledValue - }) - it('should set DD_TELEMETRY_DEBUG', () => { const origTelemetryDebugValue = process.env.DD_TELEMETRY_DEBUG process.env.DD_TELEMETRY_DEBUG = 'true' @@ -1800,9 +1789,12 @@ describe('Config', () => { }) expect(log.error).to.be.callCount(3) - expect(log.error.firstCall).to.have.been.calledWithExactly(error) - expect(log.error.secondCall).to.have.been.calledWithExactly(error) - expect(log.error.thirdCall).to.have.been.calledWithExactly(error) + expect(log.error.firstCall) + .to.have.been.calledWithExactly('Error reading file %s', 'DOES_NOT_EXIST.json', error) + expect(log.error.secondCall) + .to.have.been.calledWithExactly('Error reading file %s', 'DOES_NOT_EXIST.html', error) + expect(log.error.thirdCall) + .to.have.been.calledWithExactly('Error reading file %s', 'DOES_NOT_EXIST.json', error) expect(config.appsec.enabled).to.be.true expect(config.appsec.rules).to.eq('path/to/rules.json') diff --git a/packages/dd-trace/test/exporters/agent/writer.spec.js b/packages/dd-trace/test/exporters/agent/writer.spec.js index ce7a62d49bf..aad8749ef37 100644 --- a/packages/dd-trace/test/exporters/agent/writer.spec.js +++ b/packages/dd-trace/test/exporters/agent/writer.spec.js @@ -149,6 +149,7 @@ function describeWriter (protocolVersion) { it('should log request errors', done => { const error = new Error('boom') + error.status = 42 request.yields(error) @@ -156,7 +157,8 @@ function describeWriter (protocolVersion) { writer.flush() setTimeout(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error) + .to.have.been.calledWith('Error sending payload to the agent (status code: %s)', error.status, error) done() }) }) diff --git a/packages/dd-trace/test/exporters/common/request.spec.js b/packages/dd-trace/test/exporters/common/request.spec.js index 55bcb603a27..a6efcc45fa6 100644 --- a/packages/dd-trace/test/exporters/common/request.spec.js +++ b/packages/dd-trace/test/exporters/common/request.spec.js @@ -429,7 +429,7 @@ describe('request', function () { 'accept-encoding': 'gzip' } }, (err, res) => { - expect(log.error).to.have.been.calledWith('Could not gunzip response: unexpected end of file') + expect(log.error).to.have.been.calledWith('Could not gunzip response: %s', 'unexpected end of file') expect(res).to.equal('') done(err) }) diff --git a/packages/dd-trace/test/exporters/span-stats/writer.spec.js b/packages/dd-trace/test/exporters/span-stats/writer.spec.js index d65d480409d..f8e65500e04 100644 --- a/packages/dd-trace/test/exporters/span-stats/writer.spec.js +++ b/packages/dd-trace/test/exporters/span-stats/writer.spec.js @@ -106,7 +106,7 @@ describe('span-stats writer', () => { encoder.count.returns(1) writer.flush(() => { - expect(log.error).to.have.been.calledWith(error) + expect(log.error).to.have.been.calledWith('Error sending span stats', error) done() }) }) diff --git a/packages/dd-trace/test/llmobs/writers/base.spec.js b/packages/dd-trace/test/llmobs/writers/base.spec.js index 8b971b2748a..a2880251f4c 100644 --- a/packages/dd-trace/test/llmobs/writers/base.spec.js +++ b/packages/dd-trace/test/llmobs/writers/base.spec.js @@ -138,14 +138,16 @@ describe('BaseLLMObsWriter', () => { writer.append({ foo: 'bar' }) const error = new Error('boom') + let reqUrl request.callsFake((url, options, callback) => { + reqUrl = options.url callback(error) }) writer.flush() expect(logger.error).to.have.been.calledWith( - 'Error sending 1 LLMObs undefined events to https://llmobs-intake.datadoghq.com/api/v2/llmobs: boom' + 'Error sending %d LLMObs %s events to %s: %s', 1, undefined, reqUrl, 'boom', error ) }) diff --git a/packages/dd-trace/test/proxy.spec.js b/packages/dd-trace/test/proxy.spec.js index 4836e99787f..dd145390245 100644 --- a/packages/dd-trace/test/proxy.spec.js +++ b/packages/dd-trace/test/proxy.spec.js @@ -520,8 +520,9 @@ describe('TracerProxy', () => { const profilerImportFailureProxy = new ProfilerImportFailureProxy() profilerImportFailureProxy.init() + sinon.assert.calledOnce(log.error) const expectedErr = sinon.match.instanceOf(Error).and(sinon.match.has('code', 'MODULE_NOT_FOUND')) - sinon.assert.calledWith(log.error, sinon.match(expectedErr)) + sinon.assert.match(log.error.firstCall.lastArg, sinon.match(expectedErr)) }) it('should start telemetry', () => { diff --git a/packages/dd-trace/test/telemetry/logs/index.spec.js b/packages/dd-trace/test/telemetry/logs/index.spec.js index 0d18b6e847b..e865644e960 100644 --- a/packages/dd-trace/test/telemetry/logs/index.spec.js +++ b/packages/dd-trace/test/telemetry/logs/index.spec.js @@ -145,7 +145,12 @@ describe('telemetry logs', () => { errorLog.publish({ cause: error }) expect(logCollectorAdd) - .to.be.calledOnceWith(match({ message: `${error.name}: Generic Error`, level: 'ERROR', stack_trace: stack })) + .to.be.calledOnceWith(match({ + message: 'Generic Error', + level: 'ERROR', + errorType: 'Error', + stack_trace: stack + })) }) it('should be called when an error string is published to datadog:log:error', () => { diff --git a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js index 1cb99cef518..6f4d5bbb9d6 100644 --- a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js +++ b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js @@ -43,9 +43,18 @@ describe('telemetry log collector', () => { expect(logCollector.add({ message: 'Error 1', level: 'DEBUG', stack_trace: `stack 1\n${ddFrame}` })).to.be.true }) + it('should not store logs with empty stack and \'Generic Error\' message', () => { + expect(logCollector.add({ + message: 'Generic Error', + level: 'ERROR', + stack_trace: 'stack 1\n/not/a/dd/frame' + }) + ).to.be.false + }) + it('should include original message and dd frames', () => { const ddFrame = `at T (${ddBasePath}packages/dd-trace/test/telemetry/logs/log_collector.spec.js:29:21)` - const stack = new Error('Error 1') + const stack = new TypeError('Error 1') .stack.replace(`Error 1${EOL}`, `Error 1${EOL}${ddFrame}${EOL}`) const ddFrames = stack @@ -54,28 +63,41 @@ describe('telemetry log collector', () => { .map(line => line.replace(ddBasePath, '')) .join(EOL) - expect(logCollector.add({ message: 'Error 1', level: 'ERROR', stack_trace: stack })).to.be.true + expect(logCollector.add({ + message: 'Error 1', + level: 'ERROR', + stack_trace: stack, + errorType: 'TypeError' + })).to.be.true expect(logCollector.hasEntry({ message: 'Error 1', level: 'ERROR', - stack_trace: `Error: Error 1${EOL}${ddFrames}` + stack_trace: `TypeError: Error 1${EOL}${ddFrames}` })).to.be.true }) - it('should include original message if first frame is not a dd frame', () => { + it('should redact stack message if first frame is not a dd frame', () => { const thirdPartyFrame = `at callFn (/this/is/not/a/dd/frame/runnable.js:366:21) at T (${ddBasePath}packages/dd-trace/test/telemetry/logs/log_collector.spec.js:29:21)` - const stack = new Error('Error 1') + const stack = new TypeError('Error 1') .stack.replace(`Error 1${EOL}`, `Error 1${EOL}${thirdPartyFrame}${EOL}`) - const ddFrames = stack - .split(EOL) - .filter(line => line.includes(ddBasePath)) - .map(line => line.replace(ddBasePath, '')) - .join(EOL) + const ddFrames = [ + 'TypeError: redacted', + ...stack + .split(EOL) + .filter(line => line.includes(ddBasePath)) + .map(line => line.replace(ddBasePath, '')) + ].join(EOL) + + expect(logCollector.add({ + message: 'Error 1', + level: 'ERROR', + stack_trace: stack, + errorType: 'TypeError' + })).to.be.true - expect(logCollector.add({ message: 'Error 1', level: 'ERROR', stack_trace: stack })).to.be.true expect(logCollector.hasEntry({ message: 'Error 1', level: 'ERROR', From de0b516846fb812045367f26773a85717f5fbadd Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Thu, 12 Dec 2024 19:23:59 +0100 Subject: [PATCH 43/61] [DI] Add support for sampling (#4998) --- integration-tests/debugger/basic.spec.js | 39 +++++++++++++++++++ .../debugger/devtools_client/breakpoints.js | 8 ++++ .../src/debugger/devtools_client/defaults.js | 6 +++ .../src/debugger/devtools_client/index.js | 37 +++++++++++++++--- 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 packages/dd-trace/src/debugger/devtools_client/defaults.js diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index f42388396ef..22a8ec98ff1 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -338,6 +338,45 @@ describe('Dynamic Instrumentation', function () { }) }) + describe('sampling', function () { + it('should respect sampling rate for single probe', function (done) { + let start, timer + let payloadsReceived = 0 + const rcConfig = t.generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + + function triggerBreakpointContinuously () { + t.axios.get(t.breakpoint.url).catch(done) + timer = setTimeout(triggerBreakpointContinuously, 10) + } + + t.agent.on('debugger-diagnostics', ({ payload }) => { + if (payload.debugger.diagnostics.status === 'INSTALLED') triggerBreakpointContinuously() + }) + + t.agent.on('debugger-input', () => { + payloadsReceived++ + if (payloadsReceived === 1) { + start = Date.now() + } else if (payloadsReceived === 2) { + const duration = Date.now() - start + clearTimeout(timer) + + // Allow for a variance of -5/+50ms (time will tell if this is enough) + assert.isAbove(duration, 995) + assert.isBelow(duration, 1050) + + // Wait at least a full sampling period, to see if we get any more payloads + timer = setTimeout(done, 1250) + } else { + clearTimeout(timer) + done(new Error('Too many payloads received!')) + } + }) + + t.agent.addRemoteConfig(rcConfig) + }) + }) + describe('race conditions', function () { it('should remove the last breakpoint completely before trying to add a new one', function (done) { const rcConfig2 = t.generateRemoteConfig() diff --git a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js index 5f12f83f11d..480c2479745 100644 --- a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +++ b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js @@ -1,6 +1,7 @@ 'use strict' const session = require('./session') +const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults') const { findScriptFromPartialPath, probes, breakpoints } = require('./state') const log = require('../../log') @@ -21,6 +22,13 @@ async function addBreakpoint (probe) { probe.location = { file, lines: [String(line)] } delete probe.where + // Optimize for fast calculations when probe is hit + const snapshotsPerSecond = probe.sampling.snapshotsPerSecond ?? (probe.captureSnapshot + ? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE + : MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE) + probe.sampling.nsBetweenSampling = BigInt(1 / snapshotsPerSecond * 1e9) + probe.lastCaptureNs = 0n + // TODO: Inbetween `await session.post('Debugger.enable')` and here, the scripts are parsed and cached. // Maybe there's a race condition here or maybe we're guraenteed that `await session.post('Debugger.enable')` will // not continue untill all scripts have been parsed? diff --git a/packages/dd-trace/src/debugger/devtools_client/defaults.js b/packages/dd-trace/src/debugger/devtools_client/defaults.js new file mode 100644 index 00000000000..6acb813ab26 --- /dev/null +++ b/packages/dd-trace/src/debugger/devtools_client/defaults.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = { + MAX_SNAPSHOTS_PER_SECOND_PER_PROBE: 1, + MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE: 5_000 +} diff --git a/packages/dd-trace/src/debugger/devtools_client/index.js b/packages/dd-trace/src/debugger/devtools_client/index.js index 116688c2183..241b931d341 100644 --- a/packages/dd-trace/src/debugger/devtools_client/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/index.js @@ -18,23 +18,47 @@ require('./remote_config') const threadId = parentThreadId === 0 ? `pid:${process.pid}` : `pid:${process.pid};tid:${parentThreadId}` const threadName = parentThreadId === 0 ? 'MainThread' : `WorkerThread:${parentThreadId}` +// WARNING: The code above the line `await session.post('Debugger.resume')` is highly optimized. Please edit with care! session.on('Debugger.paused', async ({ params }) => { const start = process.hrtime.bigint() - const timestamp = Date.now() let captureSnapshotForProbe = null let maxReferenceDepth, maxCollectionSize, maxFieldCount, maxLength - const probes = params.hitBreakpoints.map((id) => { + + // V8 doesn't allow seting more than one breakpoint at a specific location, however, it's possible to set two + // breakpoints just next to eachother that will "snap" to the same logical location, which in turn will be hit at the + // same time. E.g. index.js:1:1 and index.js:1:2. + // TODO: Investigate if it will improve performance to create a fast-path for when there's only a single breakpoint + let sampled = false + const length = params.hitBreakpoints.length + let probes = new Array(length) + for (let i = 0; i < length; i++) { + const id = params.hitBreakpoints[i] const probe = breakpoints.get(id) - if (probe.captureSnapshot) { + + if (start - probe.lastCaptureNs < probe.sampling.nsBetweenSampling) { + continue + } + + sampled = true + probe.lastCaptureNs = start + + if (probe.captureSnapshot === true) { captureSnapshotForProbe = probe maxReferenceDepth = highestOrUndefined(probe.capture.maxReferenceDepth, maxReferenceDepth) maxCollectionSize = highestOrUndefined(probe.capture.maxCollectionSize, maxCollectionSize) maxFieldCount = highestOrUndefined(probe.capture.maxFieldCount, maxFieldCount) maxLength = highestOrUndefined(probe.capture.maxLength, maxLength) } - return probe - }) + + probes[i] = probe + } + + if (sampled === false) { + return session.post('Debugger.resume') + } + + const timestamp = Date.now() let processLocalState if (captureSnapshotForProbe !== null) { @@ -56,6 +80,9 @@ session.on('Debugger.paused', async ({ params }) => { log.debug(`Finished processing breakpoints - main thread paused for: ${Number(diff) / 1000000} ms`) + // Due to the highly optimized algorithm above, the `probes` array might have gaps + probes = probes.filter((probe) => !!probe) + const logger = { // We can safely use `location.file` from the first probe in the array, since all probes hit by `hitBreakpoints` // must exist in the same file since the debugger can only pause the main thread in one location. From 594ca4c4f37b4b1ded0913eb4b7eddc6f66972d7 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 12 Dec 2024 13:34:18 -0500 Subject: [PATCH 44/61] clarify startup benchmark (#3019) --- benchmark/sirun/startup/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/benchmark/sirun/startup/README.md b/benchmark/sirun/startup/README.md index c09d0aed461..69c311d778c 100644 --- a/benchmark/sirun/startup/README.md +++ b/benchmark/sirun/startup/README.md @@ -1,3 +1,7 @@ This is a simple startup test. It tests with an without the tracer, and with and without requiring every dependency and devDependency in the package.json, for a total of four variants. + +While it's unrealistic to load all the tracer's devDependencies, the intention +is to simulate loading a lot of dependencies for an application, and have them +either be intercepted by our loader hooks, or not. From 329bdf9bcfd65d4ec2815a062594ac6186879b11 Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Thu, 12 Dec 2024 13:40:46 -0500 Subject: [PATCH 45/61] remove dependency on msgpack-lite (#4969) * remove dependency on msgpack-lite * remove int64-buffer as well * fix datastreams processor * fix span stats * fix ci visibility * add support for encoding buffers and typedarrays * fix ci visibility agentless encoder * make everything faster and fix negative 53bit ints * remove debug log * remove more usage of dataview * optimize chunk write * stop handling typedarrays in set --- LICENSE-3rdparty.csv | 4 +- package.json | 4 +- .../dd-trace/src/datastreams/processor.js | 10 +- packages/dd-trace/src/datastreams/writer.js | 7 +- packages/dd-trace/src/encode/0.4.js | 101 ++---- .../src/encode/agentless-ci-visibility.js | 32 -- .../src/encode/coverage-ci-visibility.js | 3 +- packages/dd-trace/src/encode/span-stats.js | 30 -- .../dd-trace/src/{encode => msgpack}/chunk.js | 13 +- packages/dd-trace/src/msgpack/encoder.js | 309 ++++++++++++++++++ packages/dd-trace/src/msgpack/index.js | 6 + .../test/datastreams/processor.spec.js | 8 +- .../encode/agentless-ci-visibility.spec.js | 12 +- .../dd-trace/test/msgpack/encoder.spec.js | 88 +++++ 14 files changed, 457 insertions(+), 170 deletions(-) rename packages/dd-trace/src/{encode => msgpack}/chunk.js (85%) create mode 100644 packages/dd-trace/src/msgpack/encoder.js create mode 100644 packages/dd-trace/src/msgpack/index.js create mode 100644 packages/dd-trace/test/msgpack/encoder.spec.js diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index a4f6f0536fa..23c1fcda420 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -13,7 +13,6 @@ require,crypto-randomuuid,MIT,Copyright 2021 Node.js Foundation and contributors require,dc-polyfill,MIT,Copyright 2023 Datadog Inc. require,ignore,MIT,Copyright 2013 Kael Zhang and contributors require,import-in-the-middle,Apache license 2.0,Copyright 2021 Datadog Inc. -require,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki require,istanbul-lib-coverage,BSD-3-Clause,Copyright 2012-2015 Yahoo! Inc. require,jest-docblock,MIT,Copyright Meta Platforms, Inc. and affiliates. require,koalas,MIT,Copyright 2013-2017 Brian Woodward @@ -21,7 +20,6 @@ require,limiter,MIT,Copyright 2011 John Hurliman require,lodash.sortby,MIT,Copyright JS Foundation and other contributors require,lru-cache,ISC,Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors require,module-details-from-path,MIT,Copyright 2016 Thomas Watson Steen -require,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki require,opentracing,MIT,Copyright 2016 Resonance Labs Inc require,path-to-regexp,MIT,Copyright 2014 Blake Embrey require,pprof-format,MIT,Copyright 2022 Stephen Belanger @@ -59,10 +57,12 @@ dev,get-port,MIT,Copyright Sindre Sorhus dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors dev,globals,MIT,Copyright (c) Sindre Sorhus (https://sindresorhus.com) dev,graphql,MIT,Copyright 2015 Facebook Inc. +dev,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors dev,knex,MIT,Copyright (c) 2013-present Tim Griesser dev,mkdirp,MIT,Copyright 2010 James Halliday dev,mocha,MIT,Copyright 2011-2018 JS Foundation and contributors https://js.foundation +dev,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki dev,multer,MIT,Copyright 2014 Hage Yaapa dev,nock,MIT,Copyright 2017 Pedro Teixeira and other contributors dev,nyc,ISC,Copyright 2015 Contributors diff --git a/package.json b/package.json index dd90ee51661..008fd1f17d3 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,6 @@ "dc-polyfill": "^0.1.4", "ignore": "^5.2.4", "import-in-the-middle": "1.11.2", - "int64-buffer": "^0.1.9", "istanbul-lib-coverage": "3.2.0", "jest-docblock": "^29.7.0", "koalas": "^1.0.2", @@ -103,7 +102,6 @@ "lodash.sortby": "^4.7.0", "lru-cache": "^7.14.0", "module-details-from-path": "^1.0.3", - "msgpack-lite": "^0.1.26", "opentracing": ">=0.12.1", "path-to-regexp": "^0.1.12", "pprof-format": "^2.1.0", @@ -143,10 +141,12 @@ "glob": "^7.1.6", "globals": "^15.10.0", "graphql": "0.13.2", + "int64-buffer": "^0.1.9", "jszip": "^3.5.0", "knex": "^2.4.2", "mkdirp": "^3.0.1", "mocha": "^9", + "msgpack-lite": "^0.1.26", "multer": "^1.4.5-lts.1", "nock": "^11.3.3", "nyc": "^15.1.0", diff --git a/packages/dd-trace/src/datastreams/processor.js b/packages/dd-trace/src/datastreams/processor.js index d036af805a7..d997ba098ae 100644 --- a/packages/dd-trace/src/datastreams/processor.js +++ b/packages/dd-trace/src/datastreams/processor.js @@ -1,7 +1,5 @@ const os = require('os') const pkg = require('../../../../package.json') -// Message pack int encoding is done in big endian, but data streams uses little endian -const Uint64 = require('int64-buffer').Uint64BE const { LogCollapsingLowestDenseDDSketch } = require('@datadog/sketches-js') const { DsmPathwayCodec } = require('./pathway') @@ -19,8 +17,8 @@ const HIGH_ACCURACY_DISTRIBUTION = 0.0075 class StatsPoint { constructor (hash, parentHash, edgeTags) { - this.hash = new Uint64(hash) - this.parentHash = new Uint64(parentHash) + this.hash = hash.readBigUInt64BE() + this.parentHash = parentHash.readBigUInt64BE() this.edgeTags = edgeTags this.edgeLatency = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION) this.pathwayLatency = new LogCollapsingLowestDenseDDSketch(HIGH_ACCURACY_DISTRIBUTION) @@ -344,8 +342,8 @@ class DataStreamsProcessor { backlogs.push(backlog.encode()) } serializedBuckets.push({ - Start: new Uint64(timeNs), - Duration: new Uint64(this.bucketSizeNs), + Start: BigInt(timeNs), + Duration: BigInt(this.bucketSizeNs), Stats: points, Backlogs: backlogs }) diff --git a/packages/dd-trace/src/datastreams/writer.js b/packages/dd-trace/src/datastreams/writer.js index 5f789f2e056..220b3dfecf7 100644 --- a/packages/dd-trace/src/datastreams/writer.js +++ b/packages/dd-trace/src/datastreams/writer.js @@ -2,9 +2,10 @@ const pkg = require('../../../../package.json') const log = require('../log') const request = require('../exporters/common/request') const { URL, format } = require('url') -const msgpack = require('msgpack-lite') +const { MsgpackEncoder } = require('../msgpack') const zlib = require('zlib') -const codec = msgpack.createCodec({ int64: true }) + +const msgpack = new MsgpackEncoder() function makeRequest (data, url, cb) { const options = { @@ -41,7 +42,7 @@ class DataStreamsWriter { log.debug(() => `Maximum number of active requests reached. Payload discarded: ${JSON.stringify(payload)}`) return } - const encodedPayload = msgpack.encode(payload, { codec }) + const encodedPayload = msgpack.encode(payload) zlib.gzip(encodedPayload, { level: 1 }, (err, compressedData) => { if (err) { diff --git a/packages/dd-trace/src/encode/0.4.js b/packages/dd-trace/src/encode/0.4.js index 02d96cb8a26..d5c72bdb575 100644 --- a/packages/dd-trace/src/encode/0.4.js +++ b/packages/dd-trace/src/encode/0.4.js @@ -1,26 +1,20 @@ 'use strict' const { truncateSpan, normalizeSpan } = require('./tags-processors') -const Chunk = require('./chunk') +const { Chunk, MsgpackEncoder } = require('../msgpack') const log = require('../log') const { isTrue } = require('../util') const coalesce = require('koalas') const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB -const float64Array = new Float64Array(1) -const uInt8Float64Array = new Uint8Array(float64Array.buffer) - -float64Array[0] = -1 - -const bigEndian = uInt8Float64Array[7] === 0 - function formatSpan (span) { return normalizeSpan(truncateSpan(span, false)) } class AgentEncoder { constructor (writer, limit = SOFT_LIMIT) { + this._msgpack = new MsgpackEncoder() this._limit = limit this._traceBytes = new Chunk() this._stringBytes = new Chunk() @@ -84,11 +78,11 @@ class AgentEncoder { bytes.reserve(1) if (span.type && span.meta_struct) { - bytes.buffer[bytes.length++] = 0x8d + bytes.buffer[bytes.length - 1] = 0x8d } else if (span.type || span.meta_struct) { - bytes.buffer[bytes.length++] = 0x8c + bytes.buffer[bytes.length - 1] = 0x8c } else { - bytes.buffer[bytes.length++] = 0x8b + bytes.buffer[bytes.length - 1] = 0x8b } if (span.type) { @@ -135,43 +129,31 @@ class AgentEncoder { this._cacheString('') } - _encodeArrayPrefix (bytes, value) { - const length = value.length - const offset = bytes.length + _encodeBuffer (bytes, buffer) { + this._msgpack.encodeBin(bytes, buffer) + } - bytes.reserve(5) - bytes.length += 5 + _encodeBool (bytes, value) { + this._msgpack.encodeBoolean(bytes, value) + } - bytes.buffer[offset] = 0xdd - bytes.buffer[offset + 1] = length >> 24 - bytes.buffer[offset + 2] = length >> 16 - bytes.buffer[offset + 3] = length >> 8 - bytes.buffer[offset + 4] = length + _encodeArrayPrefix (bytes, value) { + this._msgpack.encodeArrayPrefix(bytes, value) } _encodeMapPrefix (bytes, keysLength) { - const offset = bytes.length - - bytes.reserve(5) - bytes.length += 5 - bytes.buffer[offset] = 0xdf - bytes.buffer[offset + 1] = keysLength >> 24 - bytes.buffer[offset + 2] = keysLength >> 16 - bytes.buffer[offset + 3] = keysLength >> 8 - bytes.buffer[offset + 4] = keysLength + this._msgpack.encodeMapPrefix(bytes, keysLength) } _encodeByte (bytes, value) { - bytes.reserve(1) - - bytes.buffer[bytes.length++] = value + this._msgpack.encodeByte(bytes, value) } + // TODO: Use BigInt instead. _encodeId (bytes, id) { const offset = bytes.length bytes.reserve(9) - bytes.length += 9 id = id.toArray() @@ -186,36 +168,16 @@ class AgentEncoder { bytes.buffer[offset + 8] = id[7] } - _encodeInteger (bytes, value) { - const offset = bytes.length - - bytes.reserve(5) - bytes.length += 5 + _encodeNumber (bytes, value) { + this._msgpack.encodeNumber(bytes, value) + } - bytes.buffer[offset] = 0xce - bytes.buffer[offset + 1] = value >> 24 - bytes.buffer[offset + 2] = value >> 16 - bytes.buffer[offset + 3] = value >> 8 - bytes.buffer[offset + 4] = value + _encodeInteger (bytes, value) { + this._msgpack.encodeInteger(bytes, value) } _encodeLong (bytes, value) { - const offset = bytes.length - const hi = (value / Math.pow(2, 32)) >> 0 - const lo = value >>> 0 - - bytes.reserve(9) - bytes.length += 9 - - bytes.buffer[offset] = 0xcf - bytes.buffer[offset + 1] = hi >> 24 - bytes.buffer[offset + 2] = hi >> 16 - bytes.buffer[offset + 3] = hi >> 8 - bytes.buffer[offset + 4] = hi - bytes.buffer[offset + 5] = lo >> 24 - bytes.buffer[offset + 6] = lo >> 16 - bytes.buffer[offset + 7] = lo >> 8 - bytes.buffer[offset + 8] = lo + this._msgpack.encodeLong(bytes, value) } _encodeMap (bytes, value) { @@ -252,23 +214,7 @@ class AgentEncoder { } _encodeFloat (bytes, value) { - float64Array[0] = value - - const offset = bytes.length - bytes.reserve(9) - bytes.length += 9 - - bytes.buffer[offset] = 0xcb - - if (bigEndian) { - for (let i = 0; i <= 7; i++) { - bytes.buffer[offset + i + 1] = uInt8Float64Array[i] - } - } else { - for (let i = 7; i >= 0; i--) { - bytes.buffer[bytes.length - i - 1] = uInt8Float64Array[i] - } - } + this._msgpack.encodeFloat(bytes, value) } _encodeMetaStruct (bytes, value) { @@ -294,7 +240,6 @@ class AgentEncoder { const offset = bytes.length bytes.reserve(prefixLength) - bytes.length += prefixLength this._encodeObject(bytes, value) diff --git a/packages/dd-trace/src/encode/agentless-ci-visibility.js b/packages/dd-trace/src/encode/agentless-ci-visibility.js index dea15182323..bc5d9fc42b6 100644 --- a/packages/dd-trace/src/encode/agentless-ci-visibility.js +++ b/packages/dd-trace/src/encode/agentless-ci-visibility.js @@ -251,37 +251,6 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder { } } - _encodeNumber (bytes, value) { - if (Math.floor(value) !== value) { // float 64 - return this._encodeFloat(bytes, value) - } - return this._encodeLong(bytes, value) - } - - _encodeLong (bytes, value) { - const isPositive = value >= 0 - - const hi = isPositive ? (value / Math.pow(2, 32)) >> 0 : Math.floor(value / Math.pow(2, 32)) - const lo = value >>> 0 - const flag = isPositive ? 0xcf : 0xd3 - - const offset = bytes.length - - // int 64 - bytes.reserve(9) - bytes.length += 9 - - bytes.buffer[offset] = flag - bytes.buffer[offset + 1] = hi >> 24 - bytes.buffer[offset + 2] = hi >> 16 - bytes.buffer[offset + 3] = hi >> 8 - bytes.buffer[offset + 4] = hi - bytes.buffer[offset + 5] = lo >> 24 - bytes.buffer[offset + 6] = lo >> 16 - bytes.buffer[offset + 7] = lo >> 8 - bytes.buffer[offset + 8] = lo - } - _encode (bytes, trace) { if (this._isReset) { this._encodePayloadStart(bytes) @@ -380,7 +349,6 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder { // Get offset of the events list to update the length of the array when calling `makePayload` this._eventsOffset = bytes.length bytes.reserve(5) - bytes.length += 5 } reset () { diff --git a/packages/dd-trace/src/encode/coverage-ci-visibility.js b/packages/dd-trace/src/encode/coverage-ci-visibility.js index bdf4b17a3cc..5b31d83cb12 100644 --- a/packages/dd-trace/src/encode/coverage-ci-visibility.js +++ b/packages/dd-trace/src/encode/coverage-ci-visibility.js @@ -1,6 +1,6 @@ 'use strict' const { AgentEncoder } = require('./0.4') -const Chunk = require('./chunk') +const { Chunk } = require('../msgpack') const { distributionMetric, @@ -82,7 +82,6 @@ class CoverageCIVisibilityEncoder extends AgentEncoder { // Get offset of the coverages list to update the length of the array when calling `makePayload` this._coveragesOffset = bytes.length bytes.reserve(5) - bytes.length += 5 } makePayload () { diff --git a/packages/dd-trace/src/encode/span-stats.js b/packages/dd-trace/src/encode/span-stats.js index 15410cec203..43215756c7c 100644 --- a/packages/dd-trace/src/encode/span-stats.js +++ b/packages/dd-trace/src/encode/span-stats.js @@ -22,10 +22,6 @@ function truncate (value, maxLength, suffix = '') { } class SpanStatsEncoder extends AgentEncoder { - _encodeBool (bytes, value) { - this._encodeByte(bytes, value ? 0xc3 : 0xc2) - } - makePayload () { const traceSize = this._traceBytes.length const buffer = Buffer.allocUnsafe(traceSize) @@ -34,32 +30,6 @@ class SpanStatsEncoder extends AgentEncoder { return buffer } - _encodeMapPrefix (bytes, length) { - const offset = bytes.length - - bytes.reserve(1) - bytes.length += 1 - - bytes.buffer[offset] = 0x80 + length - } - - _encodeBuffer (bytes, buffer) { - const length = buffer.length - const offset = bytes.length - - bytes.reserve(5) - bytes.length += 5 - - bytes.buffer[offset] = 0xc6 - bytes.buffer[offset + 1] = length >> 24 - bytes.buffer[offset + 2] = length >> 16 - bytes.buffer[offset + 3] = length >> 8 - bytes.buffer[offset + 4] = length - - buffer.copy(bytes.buffer, offset + 5) - bytes.length += length - } - _encodeStat (bytes, stat) { this._encodeMapPrefix(bytes, 12) diff --git a/packages/dd-trace/src/encode/chunk.js b/packages/dd-trace/src/msgpack/chunk.js similarity index 85% rename from packages/dd-trace/src/encode/chunk.js rename to packages/dd-trace/src/msgpack/chunk.js index 8a17b45f430..02999086c55 100644 --- a/packages/dd-trace/src/encode/chunk.js +++ b/packages/dd-trace/src/msgpack/chunk.js @@ -10,6 +10,7 @@ const DEFAULT_MIN_SIZE = 2 * 1024 * 1024 // 2MB class Chunk { constructor (minSize = DEFAULT_MIN_SIZE) { this.buffer = Buffer.allocUnsafe(minSize) + this.view = new DataView(this.buffer.buffer) this.length = 0 this._minSize = minSize } @@ -20,11 +21,9 @@ class Chunk { if (length < 0x20) { // fixstr this.reserve(length + 1) - this.length += 1 this.buffer[offset] = length | 0xa0 } else if (length < 0x100000000) { // str 32 this.reserve(length + 5) - this.length += 5 this.buffer[offset] = 0xdb this.buffer[offset + 1] = length >> 24 this.buffer[offset + 2] = length >> 16 @@ -32,7 +31,7 @@ class Chunk { this.buffer[offset + 4] = length } - this.length += this.buffer.utf8Write(value, this.length, length) + this.buffer.utf8Write(value, this.length - length, length) return this.length - offset } @@ -42,22 +41,26 @@ class Chunk { } set (array) { + const length = this.length + this.reserve(array.length) - this.buffer.set(array, this.length) - this.length += array.length + this.buffer.set(array, length) } reserve (size) { if (this.length + size > this.buffer.length) { this._resize(this._minSize * Math.ceil((this.length + size) / this._minSize)) } + + this.length += size } _resize (size) { const oldBuffer = this.buffer this.buffer = Buffer.allocUnsafe(size) + this.view = new DataView(this.buffer.buffer) oldBuffer.copy(this.buffer, 0, 0, this.length) } diff --git a/packages/dd-trace/src/msgpack/encoder.js b/packages/dd-trace/src/msgpack/encoder.js new file mode 100644 index 00000000000..6fa39d82148 --- /dev/null +++ b/packages/dd-trace/src/msgpack/encoder.js @@ -0,0 +1,309 @@ +'use strict' + +const Chunk = require('./chunk') + +class MsgpackEncoder { + encode (value) { + const bytes = new Chunk() + + this.encodeValue(bytes, value) + + return bytes.buffer.subarray(0, bytes.length) + } + + encodeValue (bytes, value) { + switch (typeof value) { + case 'bigint': + this.encodeBigInt(bytes, value) + break + case 'boolean': + this.encodeBoolean(bytes, value) + break + case 'number': + this.encodeNumber(bytes, value) + break + case 'object': + if (value === null) { + this.encodeNull(bytes, value) + } else if (Array.isArray(value)) { + this.encodeArray(bytes, value) + } else if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) { + this.encodeBin(bytes, value) + } else { + this.encodeMap(bytes, value) + } + break + case 'string': + this.encodeString(bytes, value) + break + case 'symbol': + this.encodeString(bytes, value.toString()) + break + default: // function, symbol, undefined + this.encodeNull(bytes, value) + break + } + } + + encodeNull (bytes) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = 0xc0 + } + + encodeBoolean (bytes, value) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = value ? 0xc3 : 0xc2 + } + + encodeString (bytes, value) { + bytes.write(value) + } + + encodeFixArray (bytes, size = 0) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = 0x90 + size + } + + encodeArrayPrefix (bytes, value) { + const length = value.length + const offset = bytes.length + + bytes.reserve(5) + bytes.buffer[offset] = 0xdd + bytes.buffer[offset + 1] = length >> 24 + bytes.buffer[offset + 2] = length >> 16 + bytes.buffer[offset + 3] = length >> 8 + bytes.buffer[offset + 4] = length + } + + encodeArray (bytes, value) { + if (value.length < 16) { + this.encodeFixArray(bytes, value.length) + } else { + this.encodeArrayPrefix(bytes, value) + } + + for (const item of value) { + this.encodeValue(bytes, item) + } + } + + encodeFixMap (bytes, size = 0) { + const offset = bytes.length + + bytes.reserve(1) + bytes.buffer[offset] = 0x80 + size + } + + encodeMapPrefix (bytes, keysLength) { + const offset = bytes.length + + bytes.reserve(5) + bytes.buffer[offset] = 0xdf + bytes.buffer[offset + 1] = keysLength >> 24 + bytes.buffer[offset + 2] = keysLength >> 16 + bytes.buffer[offset + 3] = keysLength >> 8 + bytes.buffer[offset + 4] = keysLength + } + + encodeByte (bytes, value) { + bytes.reserve(1) + bytes.buffer[bytes.length - 1] = value + } + + encodeBin (bytes, value) { + const offset = bytes.length + + if (value.byteLength < 256) { + bytes.reserve(2) + bytes.buffer[offset] = 0xc4 + bytes.buffer[offset + 1] = value.byteLength + } else if (value.byteLength < 65536) { + bytes.reserve(3) + bytes.buffer[offset] = 0xc5 + bytes.buffer[offset + 1] = value.byteLength >> 8 + bytes.buffer[offset + 2] = value.byteLength + } else { + bytes.reserve(5) + bytes.buffer[offset] = 0xc6 + bytes.buffer[offset + 1] = value.byteLength >> 24 + bytes.buffer[offset + 2] = value.byteLength >> 16 + bytes.buffer[offset + 3] = value.byteLength >> 8 + bytes.buffer[offset + 4] = value.byteLength + } + + bytes.set(value) + } + + encodeInteger (bytes, value) { + const offset = bytes.length + + bytes.reserve(5) + bytes.buffer[offset] = 0xce + bytes.buffer[offset + 1] = value >> 24 + bytes.buffer[offset + 2] = value >> 16 + bytes.buffer[offset + 3] = value >> 8 + bytes.buffer[offset + 4] = value + } + + encodeShort (bytes, value) { + const offset = bytes.length + + bytes.reserve(3) + bytes.buffer[offset] = 0xcd + bytes.buffer[offset + 1] = value >> 8 + bytes.buffer[offset + 2] = value + } + + encodeLong (bytes, value) { + const offset = bytes.length + const hi = (value / Math.pow(2, 32)) >> 0 + const lo = value >>> 0 + + bytes.reserve(9) + bytes.buffer[offset] = 0xcf + bytes.buffer[offset + 1] = hi >> 24 + bytes.buffer[offset + 2] = hi >> 16 + bytes.buffer[offset + 3] = hi >> 8 + bytes.buffer[offset + 4] = hi + bytes.buffer[offset + 5] = lo >> 24 + bytes.buffer[offset + 6] = lo >> 16 + bytes.buffer[offset + 7] = lo >> 8 + bytes.buffer[offset + 8] = lo + } + + encodeNumber (bytes, value) { + if (Number.isNaN(value)) { + value = 0 + } + if (Number.isInteger(value)) { + if (value >= 0) { + this.encodeUnsigned(bytes, value) + } else { + this.encodeSigned(bytes, value) + } + } else { + this.encodeFloat(bytes, value) + } + } + + encodeSigned (bytes, value) { + const offset = bytes.length + + if (value >= -0x20) { + bytes.reserve(1) + bytes.buffer[offset] = value + } else if (value >= -0x80) { + bytes.reserve(2) + bytes.buffer[offset] = 0xd0 + bytes.buffer[offset + 1] = value + } else if (value >= -0x8000) { + bytes.reserve(3) + bytes.buffer[offset] = 0xd1 + bytes.buffer[offset + 1] = value >> 8 + bytes.buffer[offset + 2] = value + } else if (value >= -0x80000000) { + bytes.reserve(5) + bytes.buffer[offset] = 0xd2 + bytes.buffer[offset + 1] = value >> 24 + bytes.buffer[offset + 2] = value >> 16 + bytes.buffer[offset + 3] = value >> 8 + bytes.buffer[offset + 4] = value + } else { + const hi = Math.floor(value / Math.pow(2, 32)) + const lo = value >>> 0 + + bytes.reserve(9) + bytes.buffer[offset] = 0xd3 + bytes.buffer[offset + 1] = hi >> 24 + bytes.buffer[offset + 2] = hi >> 16 + bytes.buffer[offset + 3] = hi >> 8 + bytes.buffer[offset + 4] = hi + bytes.buffer[offset + 5] = lo >> 24 + bytes.buffer[offset + 6] = lo >> 16 + bytes.buffer[offset + 7] = lo >> 8 + bytes.buffer[offset + 8] = lo + } + } + + encodeUnsigned (bytes, value) { + const offset = bytes.length + + if (value <= 0x7f) { + bytes.reserve(1) + bytes.buffer[offset] = value + } else if (value <= 0xff) { + bytes.reserve(2) + bytes.buffer[offset] = 0xcc + bytes.buffer[offset + 1] = value + } else if (value <= 0xffff) { + bytes.reserve(3) + bytes.buffer[offset] = 0xcd + bytes.buffer[offset + 1] = value >> 8 + bytes.buffer[offset + 2] = value + } else if (value <= 0xffffffff) { + bytes.reserve(5) + bytes.buffer[offset] = 0xce + bytes.buffer[offset + 1] = value >> 24 + bytes.buffer[offset + 2] = value >> 16 + bytes.buffer[offset + 3] = value >> 8 + bytes.buffer[offset + 4] = value + } else { + const hi = (value / Math.pow(2, 32)) >> 0 + const lo = value >>> 0 + + bytes.reserve(9) + bytes.buffer[offset] = 0xcf + bytes.buffer[offset + 1] = hi >> 24 + bytes.buffer[offset + 2] = hi >> 16 + bytes.buffer[offset + 3] = hi >> 8 + bytes.buffer[offset + 4] = hi + bytes.buffer[offset + 5] = lo >> 24 + bytes.buffer[offset + 6] = lo >> 16 + bytes.buffer[offset + 7] = lo >> 8 + bytes.buffer[offset + 8] = lo + } + } + + // TODO: Support BigInt larger than 64bit. + encodeBigInt (bytes, value) { + const offset = bytes.length + + bytes.reserve(9) + + if (value >= 0n) { + bytes.buffer[offset] = 0xcf + bytes.view.setBigUint64(offset + 1, value) + } else { + bytes.buffer[offset] = 0xd3 + bytes.view.setBigInt64(offset + 1, value) + } + } + + encodeMap (bytes, value) { + const keys = Object.keys(value) + + this.encodeMapPrefix(bytes, keys.length) + + for (const key of keys) { + this.encodeValue(bytes, key) + this.encodeValue(bytes, value[key]) + } + } + + encodeFloat (bytes, value) { + const offset = bytes.length + + bytes.reserve(9) + bytes.buffer[offset] = 0xcb + bytes.view.setFloat64(offset + 1, value) + } +} + +module.exports = { MsgpackEncoder } diff --git a/packages/dd-trace/src/msgpack/index.js b/packages/dd-trace/src/msgpack/index.js new file mode 100644 index 00000000000..03228d27044 --- /dev/null +++ b/packages/dd-trace/src/msgpack/index.js @@ -0,0 +1,6 @@ +'use strict' + +const Chunk = require('./chunk') +const { MsgpackEncoder } = require('./encoder') + +module.exports = { Chunk, MsgpackEncoder } diff --git a/packages/dd-trace/test/datastreams/processor.spec.js b/packages/dd-trace/test/datastreams/processor.spec.js index 0c30bc77947..110d9ff6c35 100644 --- a/packages/dd-trace/test/datastreams/processor.spec.js +++ b/packages/dd-trace/test/datastreams/processor.spec.js @@ -294,11 +294,11 @@ describe('DataStreamsProcessor', () => { Service: 'service1', Version: 'v1', Stats: [{ - Start: new Uint64(1680000000000), - Duration: new Uint64(10000000000), + Start: 1680000000000n, + Duration: 10000000000n, Stats: [{ - Hash: new Uint64(DEFAULT_CURRENT_HASH), - ParentHash: new Uint64(DEFAULT_PARENT_HASH), + Hash: DEFAULT_CURRENT_HASH.readBigUInt64BE(), + ParentHash: DEFAULT_PARENT_HASH.readBigUInt64BE(), EdgeTags: mockCheckpoint.edgeTags, EdgeLatency: edgeLatency.toProto(), PathwayLatency: pathwayLatency.toProto(), diff --git a/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js b/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js index 54ddab1a2a6..259ff78df2e 100644 --- a/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js +++ b/packages/dd-trace/test/encode/agentless-ci-visibility.spec.js @@ -67,14 +67,14 @@ describe('agentless-ci-visibility-encode', () => { const buffer = encoder.makePayload() const decodedTrace = msgpack.decode(buffer, { codec }) - expect(decodedTrace.version.toNumber()).to.equal(1) + expect(decodedTrace.version).to.equal(1) expect(decodedTrace.metadata['*']).to.contain({ language: 'javascript', library_version: ddTraceVersion }) const spanEvent = decodedTrace.events[0] expect(spanEvent.type).to.equal('span') - expect(spanEvent.version.toNumber()).to.equal(1) + expect(spanEvent.version).to.equal(1) expect(spanEvent.content.trace_id.toString(10)).to.equal(trace[0].trace_id.toString(10)) expect(spanEvent.content.span_id.toString(10)).to.equal(trace[0].span_id.toString(10)) expect(spanEvent.content.parent_id.toString(10)).to.equal(trace[0].parent_id.toString(10)) @@ -84,9 +84,9 @@ describe('agentless-ci-visibility-encode', () => { service: 'test-s', type: 'foo' }) - expect(spanEvent.content.error.toNumber()).to.equal(0) - expect(spanEvent.content.start.toNumber()).to.equal(123) - expect(spanEvent.content.duration.toNumber()).to.equal(456) + expect(spanEvent.content.error).to.equal(0) + expect(spanEvent.content.start).to.equal(123) + expect(spanEvent.content.duration).to.equal(456) expect(spanEvent.content.meta).to.eql({ bar: 'baz' @@ -276,6 +276,6 @@ describe('agentless-ci-visibility-encode', () => { const decodedTrace = msgpack.decode(buffer, { codec }) const spanEvent = decodedTrace.events[0] expect(spanEvent.type).to.equal('span') - expect(spanEvent.version.toNumber()).to.equal(1) + expect(spanEvent.version).to.equal(1) }) }) diff --git a/packages/dd-trace/test/msgpack/encoder.spec.js b/packages/dd-trace/test/msgpack/encoder.spec.js new file mode 100644 index 00000000000..cfda0a9e7d7 --- /dev/null +++ b/packages/dd-trace/test/msgpack/encoder.spec.js @@ -0,0 +1,88 @@ +'use strict' + +require('../setup/tap') + +const { expect } = require('chai') +const msgpack = require('msgpack-lite') +const codec = msgpack.createCodec({ int64: true }) +const { MsgpackEncoder } = require('../../src/msgpack/encoder') + +function randString (length) { + return Array.from({ length }, () => { + return String.fromCharCode(Math.floor(Math.random() * 256)) + }).join('') +} + +describe('msgpack/encoder', () => { + let encoder + + beforeEach(() => { + encoder = new MsgpackEncoder() + }) + + it('should encode to msgpack', () => { + const data = [ + { first: 'test' }, + { + fixstr: 'foo', + str: randString(1000), + fixuint: 127, + fixint: -31, + uint8: 255, + uint16: 65535, + uint32: 4294967295, + uint53: 9007199254740991, + int8: -15, + int16: -32767, + int32: -2147483647, + int53: -9007199254740991, + float: 12345.6789, + biguint: BigInt('9223372036854775807'), + bigint: BigInt('-9223372036854775807'), + buffer: Buffer.from('test'), + uint8array: new Uint8Array([1, 2, 3, 4]), + uint32array: new Uint32Array([1, 2]) + } + ] + + const buffer = encoder.encode(data) + const decoded = msgpack.decode(buffer, { codec }) + + expect(decoded).to.be.an('array') + expect(decoded[0]).to.be.an('object') + expect(decoded[0]).to.have.property('first', 'test') + expect(decoded[1]).to.be.an('object') + expect(decoded[1]).to.have.property('fixstr', 'foo') + expect(decoded[1]).to.have.property('str') + expect(decoded[1].str).to.have.length(1000) + expect(decoded[1]).to.have.property('fixuint', 127) + expect(decoded[1]).to.have.property('fixint', -31) + expect(decoded[1]).to.have.property('uint8', 255) + expect(decoded[1]).to.have.property('uint16', 65535) + expect(decoded[1]).to.have.property('uint32', 4294967295) + expect(decoded[1]).to.have.property('uint53') + expect(decoded[1].uint53.toString()).to.equal('9007199254740991') + expect(decoded[1]).to.have.property('int8', -15) + expect(decoded[1]).to.have.property('int16', -32767) + expect(decoded[1]).to.have.property('int32', -2147483647) + expect(decoded[1]).to.have.property('int53') + expect(decoded[1].int53.toString()).to.equal('-9007199254740991') + expect(decoded[1]).to.have.property('float', 12345.6789) + expect(decoded[1]).to.have.property('biguint') + expect(decoded[1].biguint.toString()).to.equal('9223372036854775807') + expect(decoded[1]).to.have.property('bigint') + expect(decoded[1].bigint.toString()).to.equal('-9223372036854775807') + expect(decoded[1]).to.have.property('buffer') + expect(decoded[1].buffer.toString('utf8')).to.equal('test') + expect(decoded[1]).to.have.property('buffer') + expect(decoded[1].buffer.toString('utf8')).to.equal('test') + expect(decoded[1]).to.have.property('uint8array') + expect(decoded[1].uint8array[0]).to.equal(1) + expect(decoded[1].uint8array[1]).to.equal(2) + expect(decoded[1].uint8array[2]).to.equal(3) + expect(decoded[1].uint8array[3]).to.equal(4) + expect(decoded[1]).to.have.property('uint32array') + expect(decoded[1].uint32array[0]).to.equal(1) + expect(decoded[1].uint32array[4]).to.equal(2) + }) +}) From e6ad5b3b6fa45203e51f9b4017e8557d8e683b3d Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 12 Dec 2024 13:48:18 -0500 Subject: [PATCH 46/61] speed up shimmer by about 50x (#4633) The `copyProperties` function was doing an `Object.setPrototypeOf`, which is rather costly. We don't actually need this because all it was doing was acting as a stopgap in case we don't get all the properties from the original function. We're using `Reflect.ownKeys()` to get those properties, so this can't happen, therefore there's no need to set the prototype. On a benchmark I whipped up separately for another project, I had noticed that shimmer + our instrumentations adds a significant amount of overhead. On a run with 100 iterations, I found that overhead to be about 10000%. When I ran the same benchmark after this change, I saw that overhead to be about 200%. --- packages/datadog-shimmer/src/shimmer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/datadog-shimmer/src/shimmer.js b/packages/datadog-shimmer/src/shimmer.js index 0285c5e5083..52abb665345 100644 --- a/packages/datadog-shimmer/src/shimmer.js +++ b/packages/datadog-shimmer/src/shimmer.js @@ -6,8 +6,6 @@ const log = require('../../dd-trace/src/log') const unwrappers = new WeakMap() function copyProperties (original, wrapped) { - Object.setPrototypeOf(wrapped, original) - const props = Object.getOwnPropertyDescriptors(original) const keys = Reflect.ownKeys(props) From d0ba71d4a65e7ff8af83b72177e60c2f482f147f Mon Sep 17 00:00:00 2001 From: Thomas Hunter II Date: Thu, 12 Dec 2024 11:11:53 -0800 Subject: [PATCH 47/61] telemetry: increment .count when deduping telemetry logs (#5001) --- .../src/telemetry/logs/log-collector.js | 19 +++++++++++++++++-- packages/dd-trace/src/util.js | 2 ++ .../test/telemetry/logs/log-collector.spec.js | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/dd-trace/src/telemetry/logs/log-collector.js b/packages/dd-trace/src/telemetry/logs/log-collector.js index a15f5ba4b3e..c43e25c8dc4 100644 --- a/packages/dd-trace/src/telemetry/logs/log-collector.js +++ b/packages/dd-trace/src/telemetry/logs/log-collector.js @@ -3,7 +3,7 @@ const log = require('../../log') const { calculateDDBasePath } = require('../../util') -const logs = new Map() +const logs = new Map() // hash -> log // NOTE: Is this a reasonable number? let maxEntries = 10000 @@ -79,8 +79,10 @@ const logCollector = { } const hash = createHash(logEntry) if (!logs.has(hash)) { - logs.set(hash, logEntry) + logs.set(hash, errorCopy(logEntry)) return true + } else { + logs.get(hash).count++ } } catch (e) { log.error('Unable to add log to logCollector: %s', e.message) @@ -120,6 +122,19 @@ const logCollector = { } } +// clone an Error object to later serialize and transmit +// { ...error } doesn't work +// also users can add arbitrary fields to an error +function errorCopy (error) { + const keys = Object.getOwnPropertyNames(error) + const obj = {} + for (const key of keys) { + obj[key] = error[key] + } + obj.count = 1 + return obj +} + logCollector.reset() module.exports = logCollector diff --git a/packages/dd-trace/src/util.js b/packages/dd-trace/src/util.js index 5259a43ed60..8cfa3d6f58c 100644 --- a/packages/dd-trace/src/util.js +++ b/packages/dd-trace/src/util.js @@ -66,6 +66,8 @@ function globMatch (pattern, subject) { return true } +// TODO: this adds stack traces relative to packages/ +// shouldn't paths be relative to the root of dd-trace? function calculateDDBasePath (dirname) { const dirSteps = dirname.split(path.sep) const packagesIndex = dirSteps.lastIndexOf('packages') diff --git a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js index 6f4d5bbb9d6..e3d4126c4c9 100644 --- a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js +++ b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js @@ -126,5 +126,24 @@ describe('telemetry log collector', () => { expect(logs.length).to.be.equal(4) expect(logs[3]).to.deep.eq({ message: 'Omitted 2 entries due to overflowing', level: 'ERROR' }) }) + + it('duplicated errors should send incremented count values', () => { + const err1 = new Error('oh no') + err1.level = 'ERROR' + + const err2 = new Error('foo buzz') + err2.level = 'ERROR' + + logCollector.add(err1) + logCollector.add(err2) + logCollector.add(err1) + logCollector.add(err2) + logCollector.add(err1) + + const drainedErrors = logCollector.drain() + expect(drainedErrors.length).to.be.equal(2) + expect(drainedErrors[0].count).to.be.equal(3) + expect(drainedErrors[1].count).to.be.equal(2) + }) }) }) From 43046841de989cdb98b475ea4dc7ef02c3736484 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 12 Dec 2024 15:27:37 -0500 Subject: [PATCH 48/61] copy prototypes in shimmer where necessary (#5009) --- packages/datadog-shimmer/src/shimmer.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/datadog-shimmer/src/shimmer.js b/packages/datadog-shimmer/src/shimmer.js index 52abb665345..e5f2f189381 100644 --- a/packages/datadog-shimmer/src/shimmer.js +++ b/packages/datadog-shimmer/src/shimmer.js @@ -6,6 +6,12 @@ const log = require('../../dd-trace/src/log') const unwrappers = new WeakMap() function copyProperties (original, wrapped) { + // TODO getPrototypeOf is not fast. Should we instead do this in specific + // instrumentations where needed? + const proto = Object.getPrototypeOf(original) + if (proto !== Function.prototype) { + Object.setPrototypeOf(wrapped, proto) + } const props = Object.getOwnPropertyDescriptors(original) const keys = Reflect.ownKeys(props) From 25d46fc785d1787c8c2e135a878b67d48f5207ab Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 13 Dec 2024 14:56:12 +0100 Subject: [PATCH 49/61] [DI] Clean up all logs emitted by the debugger (#5008) --- .../src/debugger/devtools_client/breakpoints.js | 5 ++++- .../src/debugger/devtools_client/config.js | 4 +++- .../src/debugger/devtools_client/index.js | 5 ++++- .../debugger/devtools_client/remote_config.js | 7 +++++-- .../src/debugger/devtools_client/status.js | 4 ++-- packages/dd-trace/src/debugger/index.js | 16 ++++++++-------- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js index 480c2479745..dd44e9bfde0 100644 --- a/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +++ b/packages/dd-trace/src/debugger/devtools_client/breakpoints.js @@ -36,7 +36,10 @@ async function addBreakpoint (probe) { if (!script) throw new Error(`No loaded script found for ${file} (probe: ${probe.id}, version: ${probe.version})`) const [path, scriptId] = script - log.debug(`Adding breakpoint at ${path}:${line} (probe: ${probe.id}, version: ${probe.version})`) + log.debug( + '[debugger:devtools_client] Adding breakpoint at %s:%d (probe: %s, version: %d)', + path, line, probe.id, probe.version + ) const { breakpointId } = await session.post('Debugger.setBreakpoint', { location: { diff --git a/packages/dd-trace/src/debugger/devtools_client/config.js b/packages/dd-trace/src/debugger/devtools_client/config.js index fa48779f313..7783bc84d75 100644 --- a/packages/dd-trace/src/debugger/devtools_client/config.js +++ b/packages/dd-trace/src/debugger/devtools_client/config.js @@ -15,7 +15,9 @@ const config = module.exports = { updateUrl(parentConfig) configPort.on('message', updateUrl) -configPort.on('messageerror', (err) => log.error('Debugger config messageerror', err)) +configPort.on('messageerror', (err) => + log.error('[debugger:devtools_client] received "messageerror" on config port', err) +) function updateUrl (updates) { config.url = updates.url || format({ diff --git a/packages/dd-trace/src/debugger/devtools_client/index.js b/packages/dd-trace/src/debugger/devtools_client/index.js index 241b931d341..7ca828786ac 100644 --- a/packages/dd-trace/src/debugger/devtools_client/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/index.js @@ -78,7 +78,10 @@ session.on('Debugger.paused', async ({ params }) => { await session.post('Debugger.resume') const diff = process.hrtime.bigint() - start // TODO: Recored as telemetry (DEBUG-2858) - log.debug(`Finished processing breakpoints - main thread paused for: ${Number(diff) / 1000000} ms`) + log.debug( + '[debugger:devtools_client] Finished processing breakpoints - main thread paused for: %d ms', + Number(diff) / 1000000 + ) // Due to the highly optimized algorithm above, the `probes` array might have gaps probes = probes.filter((probe) => !!probe) diff --git a/packages/dd-trace/src/debugger/devtools_client/remote_config.js b/packages/dd-trace/src/debugger/devtools_client/remote_config.js index 66d82fae81f..8e56fdd7aa0 100644 --- a/packages/dd-trace/src/debugger/devtools_client/remote_config.js +++ b/packages/dd-trace/src/debugger/devtools_client/remote_config.js @@ -41,10 +41,13 @@ rcPort.on('message', async ({ action, conf: probe, ackId }) => { ackError(err, probe) } }) -rcPort.on('messageerror', (err) => log.error('Debugger RC message error', err)) +rcPort.on('messageerror', (err) => log.error('[debugger:devtools_client] received "messageerror" on RC port', err)) async function processMsg (action, probe) { - log.debug(`Received request to ${action} ${probe.type} probe (id: ${probe.id}, version: ${probe.version})`) + log.debug( + '[debugger:devtools_client] Received request to %s %s probe (id: %s, version: %d)', + action, probe.type, probe.id, probe.version + ) if (action !== 'unapply') ackReceived(probe) diff --git a/packages/dd-trace/src/debugger/devtools_client/status.js b/packages/dd-trace/src/debugger/devtools_client/status.js index 32e4fb42834..b228d7e50b7 100644 --- a/packages/dd-trace/src/debugger/devtools_client/status.js +++ b/packages/dd-trace/src/debugger/devtools_client/status.js @@ -55,7 +55,7 @@ function ackEmitting ({ id: probeId, version }) { } function ackError (err, { id: probeId, version }) { - log.error('Debugger ackError', err) + log.error('[debugger:devtools_client] ackError', err) onlyUniqueUpdates(STATUSES.ERROR, probeId, version, () => { const payload = statusPayload(probeId, version, STATUSES.ERROR) @@ -87,7 +87,7 @@ function send (payload) { } request(form, options, (err) => { - if (err) log.error('Error sending debugger payload', err) + if (err) log.error('[debugger:devtools_client] Error sending debugger payload', err) }) } diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index 35cfb2630df..fee514f32f1 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -18,7 +18,7 @@ module.exports = { function start (config, rc) { if (worker !== null) return - log.debug('Starting Dynamic Instrumentation client...') + log.debug('[debugger] Starting Dynamic Instrumentation client...') const rcAckCallbacks = new Map() const rcChannel = new MessageChannel() @@ -33,14 +33,14 @@ function start (config, rc) { const ack = rcAckCallbacks.get(ackId) if (ack === undefined) { // This should never happen, but just in case something changes in the future, we should guard against it - log.error('Received an unknown ackId: %s', ackId) - if (error) log.error('Error starting Dynamic Instrumentation client', error) + log.error('[debugger] Received an unknown ackId: %s', ackId) + if (error) log.error('[debugger] Error starting Dynamic Instrumentation client', error) return } ack(error) rcAckCallbacks.delete(ackId) }) - rcChannel.port2.on('messageerror', (err) => log.error('Debugger RC messageerror', err)) + rcChannel.port2.on('messageerror', (err) => log.error('[debugger] received "messageerror" on RC port', err)) worker = new Worker( join(__dirname, 'devtools_client', 'index.js'), @@ -58,16 +58,16 @@ function start (config, rc) { ) worker.on('online', () => { - log.debug(`Dynamic Instrumentation worker thread started successfully (thread id: ${worker.threadId})`) + log.debug('[debugger] Dynamic Instrumentation worker thread started successfully (thread id: %d)', worker.threadId) }) - worker.on('error', (err) => log.error('Debugger worker error', err)) - worker.on('messageerror', (err) => log.error('Debugger worker messageerror', err)) + worker.on('error', (err) => log.error('[debugger] worker thread error', err)) + worker.on('messageerror', (err) => log.error('[debugger] received "messageerror" from worker', err)) worker.on('exit', (code) => { const error = new Error(`Dynamic Instrumentation worker thread exited unexpectedly with code ${code}`) - log.error('Debugger worker exited unexpectedly', error) + log.error('[debugger] worker thread exited unexpectedly', error) // Be nice, clean up now that the worker thread encounted an issue and we can't continue rc.removeProductHandler('LIVE_DEBUGGING') From 83c69285e1b88054c1da520271d45028d9089a4e Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Fri, 13 Dec 2024 17:12:34 +0100 Subject: [PATCH 50/61] Fix flaky dns and net timeline event tests (#5011) --- integration-tests/profiler/profiler.spec.js | 30 ++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/integration-tests/profiler/profiler.spec.js b/integration-tests/profiler/profiler.spec.js index 9a963202934..80be4c8fd36 100644 --- a/integration-tests/profiler/profiler.spec.js +++ b/integration-tests/profiler/profiler.spec.js @@ -133,6 +133,7 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args const events = [] for (const sample of profile.sample) { let ts, event, host, address, port, name, spanId, localRootSpanId + const unexpectedLabels = [] for (const label of sample.label) { switch (label.key) { case tsKey: ts = label.num; break @@ -143,23 +144,28 @@ async function gatherNetworkTimelineEvents (cwd, scriptFilePath, eventType, args case portKey: port = label.num; break case spanIdKey: spanId = label.str; break case localRootSpanIdKey: localRootSpanId = label.str; break - default: assert.fail(`Unexpected label key ${label.key} ${strings.strings[label.key]} ${encoded}`) + default: unexpectedLabels.push(label.key) } } - // Timestamp must be defined and be between process start and end time - assert.isDefined(ts, encoded) - assert.isTrue(ts <= procEnd, encoded) - assert.isTrue(ts >= procStart, encoded) - if (process.platform !== 'win32') { - assert.isDefined(spanId, encoded) - assert.isDefined(localRootSpanId, encoded) - } else { - assert.isUndefined(spanId, encoded) - assert.isUndefined(localRootSpanId, encoded) - } // Gather only DNS events; ignore sporadic GC events if (event === eventValue) { + // Timestamp must be defined and be between process start and end time + assert.isDefined(ts, encoded) + assert.isTrue(ts <= procEnd, encoded) + assert.isTrue(ts >= procStart, encoded) + if (process.platform !== 'win32') { + assert.isDefined(spanId, encoded) + assert.isDefined(localRootSpanId, encoded) + } else { + assert.isUndefined(spanId, encoded) + assert.isUndefined(localRootSpanId, encoded) + } assert.isDefined(name, encoded) + if (unexpectedLabels.length > 0) { + const labelsStr = JSON.stringify(unexpectedLabels) + const labelsStrStr = unexpectedLabels.map(k => strings.strings[k]).join(',') + assert.fail(`Unexpected labels: ${labelsStr}\n${labelsStrStr}\n${encoded}`) + } // Exactly one of these is defined assert.isTrue(!!address !== !!host, encoded) const ev = { name: strings.strings[name] } From 7b5ccb2ab49e6cf0f039628ee28abc469a9f35f9 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Fri, 13 Dec 2024 20:18:17 +0100 Subject: [PATCH 51/61] [DI] Improve sampling tests (#4999) To test that multiple probes doesn't interfere with each others sample rate, this commit also adds support for multiple breakpoints in a single file. --- integration-tests/debugger/basic.spec.js | 63 +++++++++++++++++-- .../debugger/target-app/basic.js | 8 ++- integration-tests/debugger/utils.js | 48 ++++++++++---- 3 files changed, 100 insertions(+), 19 deletions(-) diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 22a8ec98ff1..57c0c4a67a8 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -14,7 +14,7 @@ describe('Dynamic Instrumentation', function () { it('base case: target app should work as expected if no test probe has been added', async function () { const response = await t.axios.get(t.breakpoint.url) assert.strictEqual(response.status, 200) - assert.deepStrictEqual(response.data, { hello: 'foo' }) + assert.deepStrictEqual(response.data, { hello: 'bar' }) }) describe('diagnostics messages', function () { @@ -54,7 +54,7 @@ describe('Dynamic Instrumentation', function () { t.axios.get(t.breakpoint.url) .then((response) => { assert.strictEqual(response.status, 200) - assert.deepStrictEqual(response.data, { hello: 'foo' }) + assert.deepStrictEqual(response.data, { hello: 'bar' }) }) .catch(done) } else { @@ -245,7 +245,7 @@ describe('Dynamic Instrumentation', function () { message: 'Hello World!', logger: { name: t.breakpoint.file, - method: 'handler', + method: 'fooHandler', version, thread_name: 'MainThread' }, @@ -279,7 +279,7 @@ describe('Dynamic Instrumentation', function () { const topFrame = payload['debugger.snapshot'].stack[0] // path seems to be prefeixed with `/private` on Mac assert.match(topFrame.fileName, new RegExp(`${t.appFile}$`)) - assert.strictEqual(topFrame.function, 'handler') + assert.strictEqual(topFrame.function, 'fooHandler') assert.strictEqual(topFrame.lineNumber, t.breakpoint.line) assert.strictEqual(topFrame.columnNumber, 3) @@ -375,6 +375,61 @@ describe('Dynamic Instrumentation', function () { t.agent.addRemoteConfig(rcConfig) }) + + it('should adhere to individual probes sample rate', function (done) { + const rcConfig1 = t.breakpoints[0].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + const rcConfig2 = t.breakpoints[1].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } }) + const state = { + [rcConfig1.config.id]: { + payloadsReceived: 0, + tiggerBreakpointContinuously () { + t.axios.get(t.breakpoints[0].url).catch(done) + this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) + } + }, + [rcConfig2.config.id]: { + payloadsReceived: 0, + tiggerBreakpointContinuously () { + t.axios.get(t.breakpoints[1].url).catch(done) + this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10) + } + } + } + + t.agent.on('debugger-diagnostics', ({ payload }) => { + const { probeId, status } = payload.debugger.diagnostics + if (status === 'INSTALLED') state[probeId].tiggerBreakpointContinuously() + }) + + t.agent.on('debugger-input', ({ payload }) => { + const _state = state[payload['debugger.snapshot'].probe.id] + _state.payloadsReceived++ + if (_state.payloadsReceived === 1) { + _state.start = Date.now() + } else if (_state.payloadsReceived === 2) { + const duration = Date.now() - _state.start + clearTimeout(_state.timer) + + // Allow for a variance of -5/+50ms (time will tell if this is enough) + assert.isAbove(duration, 995) + assert.isBelow(duration, 1050) + + // Wait at least a full sampling period, to see if we get any more payloads + _state.timer = setTimeout(doneWhenCalledTwice, 1250) + } else { + clearTimeout(_state.timer) + done(new Error('Too many payloads received!')) + } + }) + + t.agent.addRemoteConfig(rcConfig1) + t.agent.addRemoteConfig(rcConfig2) + + function doneWhenCalledTwice () { + if (doneWhenCalledTwice.calledOnce) return done() + doneWhenCalledTwice.calledOnce = true + } + }) }) describe('race conditions', function () { diff --git a/integration-tests/debugger/target-app/basic.js b/integration-tests/debugger/target-app/basic.js index 2fa9c16d221..d9d1e0e9185 100644 --- a/integration-tests/debugger/target-app/basic.js +++ b/integration-tests/debugger/target-app/basic.js @@ -5,8 +5,12 @@ const Fastify = require('fastify') const fastify = Fastify() -fastify.get('/:name', function handler (request) { - return { hello: request.params.name } // BREAKPOINT: /foo +fastify.get('/foo/:name', function fooHandler (request) { + return { hello: request.params.name } // BREAKPOINT: /foo/bar +}) + +fastify.get('/bar/:name', function barHandler (request) { + return { hello: request.params.name } // BREAKPOINT: /bar/baz }) fastify.listen({ port: process.env.APP_PORT }, (err) => { diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index 1ea6cb9b54c..bca970dea87 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -20,28 +20,43 @@ module.exports = { function setup () { let sandbox, cwd, appPort - const breakpoint = getBreakpointInfo(1) // `1` to disregard the `setup` function + const breakpoints = getBreakpointInfo(1) // `1` to disregard the `setup` function const t = { - breakpoint, + breakpoint: breakpoints[0], + breakpoints, + axios: null, appFile: null, agent: null, + + // Default to the first breakpoint in the file (normally there's only one) rcConfig: null, - triggerBreakpoint, - generateRemoteConfig, - generateProbeConfig + triggerBreakpoint: triggerBreakpoint.bind(null, breakpoints[0].url), + generateRemoteConfig: generateRemoteConfig.bind(null, breakpoints[0]), + generateProbeConfig: generateProbeConfig.bind(null, breakpoints[0]) } - function triggerBreakpoint () { + // Allow specific access to each breakpoint + for (let i = 0; i < breakpoints.length; i++) { + t.breakpoints[i] = { + rcConfig: null, + triggerBreakpoint: triggerBreakpoint.bind(null, breakpoints[i].url), + generateRemoteConfig: generateRemoteConfig.bind(null, breakpoints[i]), + generateProbeConfig: generateProbeConfig.bind(null, breakpoints[i]), + ...breakpoints[i] + } + } + + function triggerBreakpoint (url) { // Trigger the breakpoint once probe is successfully installed t.agent.on('debugger-diagnostics', ({ payload }) => { if (payload.debugger.diagnostics.status === 'INSTALLED') { - t.axios.get(breakpoint.url) + t.axios.get(url) } }) } - function generateRemoteConfig (overrides = {}) { + function generateRemoteConfig (breakpoint, overrides = {}) { overrides.id = overrides.id || randomUUID() return { product: 'LIVE_DEBUGGING', @@ -54,7 +69,7 @@ function setup () { sandbox = await createSandbox(['fastify']) // TODO: Make this dynamic cwd = sandbox.folder // The sandbox uses the `integration-tests` folder as its root - t.appFile = join(cwd, 'debugger', breakpoint.file) + t.appFile = join(cwd, 'debugger', breakpoints[0].file) }) after(async function () { @@ -62,7 +77,11 @@ function setup () { }) beforeEach(async function () { - t.rcConfig = generateRemoteConfig(breakpoint) + // Default to the first breakpoint in the file (normally there's only one) + t.rcConfig = generateRemoteConfig(breakpoints[0]) + // Allow specific access to each breakpoint + t.breakpoints.forEach((breakpoint) => { breakpoint.rcConfig = generateRemoteConfig(breakpoint) }) + appPort = await getPort() t.agent = await new FakeAgent().start() t.proc = await spawnProc(t.appFile, { @@ -96,16 +115,19 @@ function getBreakpointInfo (stackIndex = 0) { .slice(0, -1) .split(':')[0] - // Then, find the corresponding file in which the breakpoint exists + // Then, find the corresponding file in which the breakpoint(s) exists const file = join('target-app', basename(testFile).replace('.spec', '')) - // Finally, find the line number of the breakpoint + // Finally, find the line number(s) of the breakpoint(s) const lines = readFileSync(join(__dirname, file), 'utf8').split('\n') + const result = [] for (let i = 0; i < lines.length; i++) { const index = lines[i].indexOf(BREAKPOINT_TOKEN) if (index !== -1) { const url = lines[i].slice(index + BREAKPOINT_TOKEN.length + 1).trim() - return { file, line: i + 1, url } + result.push({ file, line: i + 1, url }) } } + + return result } From 880f15ae979493054cb3b3c74c639ac47641e408 Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Fri, 13 Dec 2024 14:25:43 -0500 Subject: [PATCH 52/61] run benchmarks also on node 20 and 22 (#4975) --- .gitlab/benchmarks.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitlab/benchmarks.yml b/.gitlab/benchmarks.yml index 57eba976441..7461f88b98c 100644 --- a/.gitlab/benchmarks.yml +++ b/.gitlab/benchmarks.yml @@ -65,6 +65,18 @@ benchmark: GROUP: 2 - MAJOR_VERSION: 18 GROUP: 3 + - MAJOR_VERSION: 20 + GROUP: 1 + - MAJOR_VERSION: 20 + GROUP: 2 + - MAJOR_VERSION: 20 + GROUP: 3 + - MAJOR_VERSION: 22 + GROUP: 1 + - MAJOR_VERSION: 22 + GROUP: 2 + - MAJOR_VERSION: 22 + GROUP: 3 variables: SPLITS: 3 From 749b9a8949d6aba2f23b74af15f041e11a31791a Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Fri, 13 Dec 2024 14:36:54 -0500 Subject: [PATCH 53/61] use gc observer for gc runtime metrics when available (#4961) --- packages/dd-trace/src/runtime_metrics.js | 109 ++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/packages/dd-trace/src/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics.js index 49e724eb11c..a9036612a67 100644 --- a/packages/dd-trace/src/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics.js @@ -7,11 +7,19 @@ const os = require('os') const { DogStatsDClient } = require('./dogstatsd') const log = require('./log') const Histogram = require('./histogram') -const { performance } = require('perf_hooks') +const { performance, PerformanceObserver } = require('perf_hooks') +const { NODE_MAJOR, NODE_MINOR } = require('../../../version') const INTERVAL = 10 * 1000 +// Node >=16 has PerformanceObserver with `gc` type, but <16.7 had a critical bug. +// See: https://github.com/nodejs/node/issues/39548 +const hasGCObserver = NODE_MAJOR >= 18 || (NODE_MAJOR === 16 && NODE_MINOR >= 7) +const hasGCProfiler = NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 15) + let nativeMetrics = null +let gcObserver = null +let gcProfiler = null let interval let client @@ -24,13 +32,18 @@ let elu reset() -module.exports = { +const runtimeMetrics = module.exports = { start (config) { const clientConfig = DogStatsDClient.generateClientConfig(config) try { nativeMetrics = require('@datadog/native-metrics') - nativeMetrics.start() + + if (hasGCObserver) { + nativeMetrics.start('loop') // Only add event loop watcher and not GC. + } else { + nativeMetrics.start() + } } catch (e) { log.error('Error starting native metrics', e) nativeMetrics = null @@ -40,6 +53,9 @@ module.exports = { time = process.hrtime() + startGCObserver() + startGCProfiler() + if (nativeMetrics) { interval = setInterval(() => { captureCommonMetrics() @@ -138,6 +154,10 @@ function reset () { counters = {} histograms = {} nativeMetrics = null + gcObserver && gcObserver.disconnect() + gcObserver = null + gcProfiler && gcProfiler.stop() + gcProfiler = null } function captureCpuUsage () { @@ -202,6 +222,29 @@ function captureHeapSpace () { client.gauge('runtime.node.heap.physical_size.by.space', stats[i].physical_space_size, tags) } } +function captureGCMetrics () { + if (!gcProfiler) return + + const profile = gcProfiler.stop() + const pauseAll = new Histogram() + const pause = {} + + for (const stat of profile.statistics) { + const type = stat.gcType.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase() + + pause[type] = pause[type] || new Histogram() + pause[type].record(stat.cost) + pauseAll.record(stat.cost) + } + + histogram('runtime.node.gc.pause', pauseAll) + + for (const type in pause) { + histogram('runtime.node.gc.pause.by.type', pause[type], [`gc_type:${type}`]) + } + + gcProfiler.start() +} function captureGauges () { Object.keys(gauges).forEach(name => { @@ -256,6 +299,7 @@ function captureCommonMetrics () { captureCounters() captureHistograms() captureELU() + captureGCMetrics() } function captureNativeMetrics () { @@ -297,6 +341,11 @@ function captureNativeMetrics () { function histogram (name, stats, tags) { tags = [].concat(tags) + // Stats can contain garbage data when a value was never recorded. + if (stats.count === 0) { + stats = { max: 0, min: 0, sum: 0, avg: 0, median: 0, p95: 0, count: 0 } + } + client.gauge(`${name}.min`, stats.min, tags) client.gauge(`${name}.max`, stats.max, tags) client.increment(`${name}.sum`, stats.sum, tags) @@ -306,3 +355,57 @@ function histogram (name, stats, tags) { client.gauge(`${name}.median`, stats.median, tags) client.gauge(`${name}.95percentile`, stats.p95, tags) } + +function startGCObserver () { + if (gcObserver || hasGCProfiler || !hasGCObserver) return + + gcObserver = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + const type = gcType(entry.kind) + + runtimeMetrics.histogram('runtime.node.gc.pause.by.type', entry.duration, `gc_type:${type}`) + runtimeMetrics.histogram('runtime.node.gc.pause', entry.duration) + } + }) + + gcObserver.observe({ type: 'gc' }) +} + +function startGCProfiler () { + if (gcProfiler || !hasGCProfiler) return + + gcProfiler = new v8.GCProfiler() + gcProfiler.start() +} + +function gcType (kind) { + if (NODE_MAJOR >= 22) { + switch (kind) { + case 1: return 'scavenge' + case 2: return 'minor_mark_sweep' + case 4: return 'mark_sweep_compact' // Deprecated, might be removed soon. + case 8: return 'incremental_marking' + case 16: return 'process_weak_callbacks' + case 31: return 'all' + } + } else if (NODE_MAJOR >= 18) { + switch (kind) { + case 1: return 'scavenge' + case 2: return 'minor_mark_compact' + case 4: return 'mark_sweep_compact' + case 8: return 'incremental_marking' + case 16: return 'process_weak_callbacks' + case 31: return 'all' + } + } else { + switch (kind) { + case 1: return 'scavenge' + case 2: return 'mark_sweep_compact' + case 4: return 'incremental_marking' + case 8: return 'process_weak_callbacks' + case 15: return 'all' + } + } + + return 'unknown' +} From 69b27b3c3d8488033dbfee79167674762bc69177 Mon Sep 17 00:00:00 2001 From: Thomas Hunter II Date: Fri, 13 Dec 2024 11:51:17 -0800 Subject: [PATCH 54/61] telemetry: make count logic faster (#5013) --- packages/dd-trace/src/telemetry/logs/index.js | 1 + .../dd-trace/src/telemetry/logs/log-collector.js | 15 +-------------- .../test/telemetry/logs/log-collector.spec.js | 6 ++---- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/dd-trace/src/telemetry/logs/index.js b/packages/dd-trace/src/telemetry/logs/index.js index d8fa1969e55..199b5fb7943 100644 --- a/packages/dd-trace/src/telemetry/logs/index.js +++ b/packages/dd-trace/src/telemetry/logs/index.js @@ -40,6 +40,7 @@ function onErrorLog (msg) { const telLog = { level: 'ERROR', + count: 1, // existing log.error(err) without message will be reported as 'Generic Error' message: message ?? 'Generic Error' diff --git a/packages/dd-trace/src/telemetry/logs/log-collector.js b/packages/dd-trace/src/telemetry/logs/log-collector.js index c43e25c8dc4..a2ee9d06f4a 100644 --- a/packages/dd-trace/src/telemetry/logs/log-collector.js +++ b/packages/dd-trace/src/telemetry/logs/log-collector.js @@ -79,7 +79,7 @@ const logCollector = { } const hash = createHash(logEntry) if (!logs.has(hash)) { - logs.set(hash, errorCopy(logEntry)) + logs.set(hash, logEntry) return true } else { logs.get(hash).count++ @@ -122,19 +122,6 @@ const logCollector = { } } -// clone an Error object to later serialize and transmit -// { ...error } doesn't work -// also users can add arbitrary fields to an error -function errorCopy (error) { - const keys = Object.getOwnPropertyNames(error) - const obj = {} - for (const key of keys) { - obj[key] = error[key] - } - obj.count = 1 - return obj -} - logCollector.reset() module.exports = logCollector diff --git a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js index e3d4126c4c9..57600dcb441 100644 --- a/packages/dd-trace/test/telemetry/logs/log-collector.spec.js +++ b/packages/dd-trace/test/telemetry/logs/log-collector.spec.js @@ -128,11 +128,9 @@ describe('telemetry log collector', () => { }) it('duplicated errors should send incremented count values', () => { - const err1 = new Error('oh no') - err1.level = 'ERROR' + const err1 = { message: 'oh no', level: 'ERROR', count: 1 } - const err2 = new Error('foo buzz') - err2.level = 'ERROR' + const err2 = { message: 'foo buzz', level: 'ERROR', count: 1 } logCollector.add(err1) logCollector.add(err2) From baf22d9f4f68d74ddb7947fee3039e039dfe18e8 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Fri, 13 Dec 2024 16:17:42 -0500 Subject: [PATCH 55/61] Verify yaml (#4639) * add script to verify plugin yaml * add github actions job to verify yaml * fix instrumentations * fix up aerospike * better version ranges for aerospike * fix ci script * make it pass hopefully * update license 3rdparty * fix it no longer assuming nodejs versions * fix aerospike * since node version is now ignored, run on only one version of node --- .github/workflows/appsec.yml | 2 +- .github/workflows/plugins.yml | 75 +++------ .github/workflows/project.yml | 7 + LICENSE-3rdparty.csv | 1 + package.json | 3 +- .../datadog-instrumentations/src/aerospike.js | 2 +- packages/datadog-instrumentations/src/next.js | 11 +- scripts/verify-ci-config.js | 156 ++++++++++++++++++ yarn.lock | 5 + 9 files changed, 200 insertions(+), 62 deletions(-) create mode 100644 scripts/verify-ci-config.js diff --git a/.github/workflows/appsec.yml b/.github/workflows/appsec.yml index 45edbde6ebc..17a4e66f15c 100644 --- a/.github/workflows/appsec.yml +++ b/.github/workflows/appsec.yml @@ -210,7 +210,7 @@ jobs: version: - 18 - latest - range: ['9.5.0', '11.1.4', '13.2.0', '>=14.0.0 <=14.2.6', '>=14.2.7 <15', '>=15.0.0'] + range: ['>=10.2.0 <11', '>=11.0.0 <13', '11.1.4', '>=13.0.0 <14', '13.2.0', '>=14.0.0 <=14.2.6', '>=14.2.7 <15', '>=15.0.0'] runs-on: ubuntu-latest env: PLUGINS: next diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index d25535e2aab..2a76db58145 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -15,54 +15,30 @@ concurrency: jobs: - aerospike-node-16: - runs-on: ubuntu-latest - services: - aerospike: - image: aerospike:ce-5.7.0.15 - ports: - - "127.0.0.1:3000-3002:3000-3002" - env: - PLUGINS: aerospike - SERVICES: aerospike - PACKAGE_VERSION_RANGE: '>=4.0.0 <5.2.0' - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/testagent/start - - uses: ./.github/actions/node/setup - - id: pkg - run: | - content=`cat ./package.json | tr '\n' ' '` - echo "json=$content" >> $GITHUB_OUTPUT - - id: extract - run: | - version="${{fromJson(steps.pkg.outputs.json).version}}" - majorVersion=$(echo "$version" | cut -d '.' -f 1) - echo "Major Version: $majorVersion" - echo "MAJOR_VERSION=$majorVersion" >> $GITHUB_ENV - - uses: ./.github/actions/node/oldest - - name: Install dependencies - if: env.MAJOR_VERSION == '4' - uses: ./.github/actions/install - - name: Run tests - if: env.MAJOR_VERSION == '4' - run: yarn test:plugins:ci - - if: always() - uses: ./.github/actions/testagent/logs - - uses: codecov/codecov-action@v3 - - aerospike-node-18-20: + aerospike: strategy: matrix: - node-version: [18] - range: ['5.2.0 - 5.7.0'] + node-version: [16] + range: ['>=4.0.0 <5.2.0'] + aerospike-image: [ce-5.7.0.15] + test-image: [ubuntu-latest] include: + - node-version: 18 + range: '>=5.2.0' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest - node-version: 20 - range: '>=5.8.0' - runs-on: ubuntu-latest + range: '>=5.5.0' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest + - node-version: 22 + range: '>=5.12.1' + aerospike-image: ce-6.4.0.3 + test-image: ubuntu-latest + runs-on: ${{ matrix.test-image }} services: aerospike: - image: aerospike:ce-6.4.0.3 + image: aerospike:${{ matrix.aerospike-image }} ports: - "127.0.0.1:3000-3002:3000-3002" env: @@ -73,24 +49,13 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/testagent/start - uses: ./.github/actions/node/setup - - id: pkg - run: | - content=`cat ./package.json | tr '\n' ' '` - echo "json=$content" >> $GITHUB_OUTPUT - - id: extract - run: | - version="${{fromJson(steps.pkg.outputs.json).version}}" - majorVersion=$(echo "$version" | cut -d '.' -f 1) - echo "Major Version: $majorVersion" - echo "MAJOR_VERSION=$majorVersion" >> $GITHUB_ENV - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + - run: yarn config set ignore-engines true - name: Install dependencies - if: env.MAJOR_VERSION == '5' uses: ./.github/actions/install - name: Run tests - if: env.MAJOR_VERSION == '5' run: yarn test:plugins:ci - if: always() uses: ./.github/actions/testagent/logs @@ -759,7 +724,7 @@ jobs: version: - 18 - latest - range: ['9.5.0', '11.1.4', '13.2.0', '>=14.0.0 <=14.2.6', '>=14.2.7 <15', '>=15.0.0'] + range: ['>=10.2.0 <11', '>=11.0.0 <13', '11.1.4', '>=13.0.0 <14', '13.2.0', '>=14.0.0 <=14.2.6', '>=14.2.7 <15', '>=15.0.0'] runs-on: ubuntu-latest env: PLUGINS: next diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index f7839ac941e..3dd8475811e 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -162,3 +162,10 @@ jobs: - run: yarn type:test - run: yarn type:doc + verify-yaml: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/node/setup + - uses: ./.github/actions/install + - run: node scripts/verify-ci-config.js diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 23c1fcda420..4ba4775b73c 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -72,6 +72,7 @@ dev,sinon,BSD-3-Clause,Copyright 2010-2017 Christian Johansen dev,sinon-chai,WTFPL and BSD-2-Clause,Copyright 2004 Sam Hocevar 2012–2017 Domenic Denicola dev,tap,ISC,Copyright 2011-2022 Isaac Z. Schlueter and Contributors dev,tiktoken,MIT,Copyright (c) 2022 OpenAI, Shantanu Jain +dev,yaml,ISC,Copyright Eemeli Aro file,aws-lambda-nodejs-runtime-interface-client,Apache 2.0,Copyright 2019 Amazon.com Inc. or its affiliates. All Rights Reserved. file,profile.proto,Apache license 2.0,Copyright 2016 Google Inc. file,is-git-url,MIT,Copyright (c) 2017 Jon Schlinkert. diff --git a/package.json b/package.json index 008fd1f17d3..04d5c78e320 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "sinon": "^16.1.3", "sinon-chai": "^3.7.0", "tap": "^16.3.7", - "tiktoken": "^1.0.15" + "tiktoken": "^1.0.15", + "yaml": "^2.5.0" } } diff --git a/packages/datadog-instrumentations/src/aerospike.js b/packages/datadog-instrumentations/src/aerospike.js index 724c518e050..497a64aaf80 100644 --- a/packages/datadog-instrumentations/src/aerospike.js +++ b/packages/datadog-instrumentations/src/aerospike.js @@ -40,7 +40,7 @@ function wrapProcess (process) { addHook({ name: 'aerospike', file: 'lib/commands/command.js', - versions: ['^3.16.2', '4', '5'] + versions: ['4', '5'] }, commandFactory => { return shimmer.wrapFunction(commandFactory, f => wrapCreateCommand(f)) diff --git a/packages/datadog-instrumentations/src/next.js b/packages/datadog-instrumentations/src/next.js index 56ce695fe76..770d340d567 100644 --- a/packages/datadog-instrumentations/src/next.js +++ b/packages/datadog-instrumentations/src/next.js @@ -2,7 +2,6 @@ const { channel, addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') -const { DD_MAJOR } = require('../../../version') const startChannel = channel('apm:next:request:start') const finishChannel = channel('apm:next:request:finish') @@ -221,7 +220,7 @@ addHook({ addHook({ name: 'next', - versions: DD_MAJOR >= 4 ? ['>=10.2 <11.1'] : ['>=9.5 <11.1'], + versions: ['>=10.2 <11.1'], file: 'dist/next-server/server/serve-static.js' }, serveStatic => shimmer.wrap(serveStatic, 'serveStatic', wrapServeStatic)) @@ -248,7 +247,11 @@ addHook({ name: 'next', versions: ['>=13.2'], file: 'dist/server/next-server.js' return nextServer }) -addHook({ name: 'next', versions: ['>=11.1 <13.2'], file: 'dist/server/next-server.js' }, nextServer => { +addHook({ + name: 'next', + versions: ['>=11.1 <13.2'], + file: 'dist/server/next-server.js' +}, nextServer => { const Server = nextServer.default shimmer.wrap(Server.prototype, 'handleApiRequest', wrapHandleApiRequest) return nextServer @@ -256,7 +259,7 @@ addHook({ name: 'next', versions: ['>=11.1 <13.2'], file: 'dist/server/next-serv addHook({ name: 'next', - versions: DD_MAJOR >= 4 ? ['>=10.2 <11.1'] : ['>=9.5 <11.1'], + versions: ['>=10.2 <11.1'], file: 'dist/next-server/server/next-server.js' }, nextServer => { const Server = nextServer.default diff --git a/scripts/verify-ci-config.js b/scripts/verify-ci-config.js new file mode 100644 index 00000000000..7a917132688 --- /dev/null +++ b/scripts/verify-ci-config.js @@ -0,0 +1,156 @@ +'use strict' +/* eslint-disable no-console */ + +const fs = require('fs') +const path = require('path') +const util = require('util') +const proxyquire = require('proxyquire') +const yaml = require('yaml') +const semver = require('semver') +const { execSync } = require('child_process') +const Module = require('module') +if (!Module.isBuiltin) { + Module.isBuiltin = mod => Module.builtinModules.includes(mod) +} + +const nodeMajor = Number(process.versions.node.split('.')[0]) + +const names = fs.readdirSync(path.join(__dirname, '..', 'packages', 'datadog-instrumentations', 'src')) + .filter(file => file.endsWith('.js')) + .map(file => file.slice(0, -3)) + +const instrumentations = names.reduce((acc, key) => { + let instrumentations = [] + const name = key + + try { + loadInstFile(`${name}/server.js`, instrumentations) + loadInstFile(`${name}/client.js`, instrumentations) + } catch (e) { + loadInstFile(`${name}.js`, instrumentations) + } + + instrumentations = instrumentations.filter(i => i.versions) + if (instrumentations.length) { + acc[key] = instrumentations + } + + return acc +}, {}) + +const versions = {} + +function checkYaml (yamlPath) { + const yamlContent = yaml.parse(fs.readFileSync(yamlPath, 'utf8')) + + const rangesPerPluginFromYaml = {} + const rangesPerPluginFromInst = {} + for (const jobName in yamlContent.jobs) { + const job = yamlContent.jobs[jobName] + if (!job.env || !job.env.PLUGINS) continue + + const pluginName = job.env.PLUGINS + if (Module.isBuiltin(pluginName)) continue + const rangesFromYaml = getRangesFromYaml(job) + if (rangesFromYaml) { + if (!rangesPerPluginFromYaml[pluginName]) { + rangesPerPluginFromYaml[pluginName] = new Set() + } + rangesFromYaml.forEach(range => rangesPerPluginFromYaml[pluginName].add(range)) + const plugin = instrumentations[pluginName] + const allRangesForPlugin = new Set(plugin.map(x => x.versions).flat()) + rangesPerPluginFromInst[pluginName] = allRangesForPlugin + } + } + for (const pluginName in rangesPerPluginFromYaml) { + const yamlRanges = Array.from(rangesPerPluginFromYaml[pluginName]) + const instRanges = Array.from(rangesPerPluginFromInst[pluginName]) + const yamlVersions = getMatchingVersions(pluginName, yamlRanges) + const instVersions = getMatchingVersions(pluginName, instRanges) + if (!util.isDeepStrictEqual(yamlVersions, instVersions)) { + const opts = { colors: true } + const colors = x => util.inspect(x, opts) + errorMsg(pluginName, 'Mismatch', ` +Valid version ranges from YAML: ${colors(yamlRanges)} +Valid version ranges from INST: ${colors(instRanges)} +${mismatching(yamlVersions, instVersions)} +Note that versions may be dependent on Node.js version. This is Node.js v${colors(nodeMajor)} + +> These don't match the same sets of versions in npm. +> +> Please check ${yamlPath} and the instrumentations +> for ${pluginName} to see that the version ranges match.`.trim()) + } + } +} + +function loadInstFile (file, instrumentations) { + const instrument = { + addHook (instrumentation) { + instrumentations.push(instrumentation) + } + } + + const instPath = path.join(__dirname, `../packages/datadog-instrumentations/src/${file}`) + + proxyquire.noPreserveCache()(instPath, { + './helpers/instrument': instrument, + '../helpers/instrument': instrument + }) +} + +function getRangesFromYaml (job) { + // eslint-disable-next-line no-template-curly-in-string + if (job.env && job.env.PACKAGE_VERSION_RANGE && job.env.PACKAGE_VERSION_RANGE !== '${{ matrix.range }}') { + errorMsg(job.env.PLUGINS, 'ERROR in YAML', 'You must use matrix.range instead of env.PACKAGE_VERSION_RANGE') + process.exitCode = 1 + } + if (job.strategy && job.strategy.matrix && job.strategy.matrix.range) { + const possibilities = [job.strategy.matrix] + if (job.strategy.matrix.include) { + possibilities.push(...job.strategy.matrix.include) + } + return possibilities.map(possibility => { + if (possibility.range) { + return [possibility.range].flat() + } else { + return undefined + } + }).flat() + } + + return null +} + +function getMatchingVersions (name, ranges) { + if (!versions[name]) { + versions[name] = JSON.parse(execSync('npm show ' + name + ' versions --json').toString()) + } + return versions[name].filter(version => ranges.some(range => semver.satisfies(version, range))) +} + +checkYaml(path.join(__dirname, '..', '.github', 'workflows', 'plugins.yml')) +checkYaml(path.join(__dirname, '..', '.github', 'workflows', 'appsec.yml')) + +function mismatching (yamlVersions, instVersions) { + const yamlSet = new Set(yamlVersions) + const instSet = new Set(instVersions) + + const onlyInYaml = yamlVersions.filter(v => !instSet.has(v)) + const onlyInInst = instVersions.filter(v => !yamlSet.has(v)) + + const opts = { colors: true } + return [ + `Versions only in YAML: ${util.inspect(onlyInYaml, opts)}`, + `Versions only in INST: ${util.inspect(onlyInInst, opts)}` + ].join('\n') +} + +function errorMsg (pluginName, title, message) { + console.log('===========================================') + console.log(title + ' for ' + pluginName) + console.log('-------------------------------------------') + console.log(message) + console.log('\n') + process.exitCode = 1 +} diff --git a/yarn.lock b/yarn.lock index 54222f765ba..107dfd70f1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5252,6 +5252,11 @@ yaml@^1.10.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" + integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" From 75865b468568d2b54b2e959edeeb2e1e6b41077b Mon Sep 17 00:00:00 2001 From: Ugaitz Urien Date: Mon, 16 Dec 2024 11:35:20 +0100 Subject: [PATCH 56/61] Test aerospike node 16 with ubuntu-22.04 (#5017) --- .github/workflows/plugins.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 2a76db58145..4822539ecab 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -21,7 +21,7 @@ jobs: node-version: [16] range: ['>=4.0.0 <5.2.0'] aerospike-image: [ce-5.7.0.15] - test-image: [ubuntu-latest] + test-image: [ubuntu-22.04] include: - node-version: 18 range: '>=5.2.0' From 23720bb6ef8ff900927ffe6a64f5f7c183ce18ec Mon Sep 17 00:00:00 2001 From: Igor Unanua Date: Mon, 16 Dec 2024 12:02:07 +0100 Subject: [PATCH 57/61] Upgrade iast rewriter version to 2.6.1 (#5010) * Upgrade iast rewriter version to 2.6.1 * fix nanoid version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 04d5c78e320..28c20dde6ed 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "dependencies": { "@datadog/libdatadog": "^0.2.2", "@datadog/native-appsec": "8.3.0", - "@datadog/native-iast-rewriter": "2.6.0", + "@datadog/native-iast-rewriter": "2.6.1", "@datadog/native-iast-taint-tracking": "3.2.0", "@datadog/native-metrics": "^3.0.1", "@datadog/pprof": "5.4.1", diff --git a/yarn.lock b/yarn.lock index 107dfd70f1d..ebbc6922e3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -413,10 +413,10 @@ dependencies: node-gyp-build "^3.9.0" -"@datadog/native-iast-rewriter@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.6.0.tgz#745148ac630cace48372fb3b3aaa50e32460b693" - integrity sha512-TCRe3QNm7hGWlfvW1RnE959sV/kBqDiSEGAHS+HlQYaIwG2y0WcxA5TjLxhcIJJsfmgou5ycIlknAvNkbaoDDQ== +"@datadog/native-iast-rewriter@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.6.1.tgz#5e5393628c73c57dcf08256299c0e8cf71deb14f" + integrity sha512-zv7cr/MzHg560jhAnHcO7f9pLi4qaYrBEcB+Gla0xkVouYSDsp8cGXIGG4fiGdAMHdt7SpDNS6+NcEAqD/v8Ig== dependencies: lru-cache "^7.14.0" node-gyp-build "^4.5.0" From 02fba54df8cf8affdb088b4119d0b16bf61db8c0 Mon Sep 17 00:00:00 2001 From: Igor Unanua Date: Mon, 16 Dec 2024 12:09:46 +0100 Subject: [PATCH 58/61] Add some checks to avoid runtime errors (#4945) * Add some checks to avoid runtime errors * check span * linter --- packages/datadog-plugin-avsc/src/schema_iterator.js | 11 ++++++++--- packages/datadog-plugin-grpc/src/client.js | 4 ++-- .../dd-trace/src/opentracing/propagation/text_map.js | 2 +- packages/dd-trace/src/plugins/tracing.js | 2 +- .../src/profiling/profilers/event_plugins/event.js | 10 ++++++++-- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/datadog-plugin-avsc/src/schema_iterator.js b/packages/datadog-plugin-avsc/src/schema_iterator.js index 44fce95a765..0b4874ceea8 100644 --- a/packages/datadog-plugin-avsc/src/schema_iterator.js +++ b/packages/datadog-plugin-avsc/src/schema_iterator.js @@ -108,10 +108,15 @@ class SchemaExtractor { if (!builder.shouldExtractSchema(schemaName, depth)) { return false } - for (const field of schema.fields) { - if (!this.extractProperty(field, schemaName, field.name, builder, depth)) { - log.warn('DSM: Unable to extract field with name: %s from Avro schema with name: %s', field.name, schemaName) + if (schema.fields?.[Symbol.iterator]) { + for (const field of schema.fields) { + if (!this.extractProperty(field, schemaName, field.name, builder, depth)) { + log.warn('DSM: Unable to extract field with name: %s from Avro schema with name: %s', field.name, + schemaName) + } } + } else { + log.warn('DSM: schema.fields is not iterable from Avro schema with name: %s', schemaName) } } return true diff --git a/packages/datadog-plugin-grpc/src/client.js b/packages/datadog-plugin-grpc/src/client.js index 1b130a1f93e..db8dd89b9bf 100644 --- a/packages/datadog-plugin-grpc/src/client.js +++ b/packages/datadog-plugin-grpc/src/client.js @@ -62,7 +62,7 @@ class GrpcClientPlugin extends ClientPlugin { return parentStore } - error ({ span, error }) { + error ({ span = this.activeSpan, error }) { this.addCode(span, error.code) if (error.code && !this._tracerConfig.grpc.client.error.statuses.includes(error.code)) { return @@ -108,7 +108,7 @@ class GrpcClientPlugin extends ClientPlugin { } addCode (span, code) { - if (code !== undefined) { + if (code !== undefined && span) { span.setTag('grpc.status.code', code) } } diff --git a/packages/dd-trace/src/opentracing/propagation/text_map.js b/packages/dd-trace/src/opentracing/propagation/text_map.js index dcf8fb3fcc6..82bc9f2b30f 100644 --- a/packages/dd-trace/src/opentracing/propagation/text_map.js +++ b/packages/dd-trace/src/opentracing/propagation/text_map.js @@ -499,7 +499,7 @@ class TextMapPropagator { } _extractGenericContext (carrier, traceKey, spanKey, radix) { - if (carrier[traceKey] && carrier[spanKey]) { + if (carrier && carrier[traceKey] && carrier[spanKey]) { if (invalidSegment.test(carrier[traceKey])) return null return new DatadogSpanContext({ diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index 6f11b9bde6a..e384b8cb7a7 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -94,7 +94,7 @@ class TracingPlugin extends Plugin { } addError (error, span = this.activeSpan) { - if (!span._spanContext._tags.error) { + if (span && !span._spanContext._tags.error) { // Errors may be wrapped in a context. error = (error && error.error) || error span.setTag('error', error || 1) diff --git a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js index 5d81e1d8a3f..48e430ba607 100644 --- a/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +++ b/packages/dd-trace/src/profiling/profilers/event_plugins/event.js @@ -21,11 +21,17 @@ class EventPlugin extends TracingPlugin { } error () { - this.store.getStore().error = true + const store = this.store.getStore() + if (store) { + store.error = true + } } finish () { - const { startEvent, startTime, error } = this.store.getStore() + const store = this.store.getStore() + if (!store) return + + const { startEvent, startTime, error } = store if (error) { return // don't emit perf events for failed operations } From 048868e2f778423d75210528afe4226e3b10f954 Mon Sep 17 00:00:00 2001 From: simon-id Date: Mon, 16 Dec 2024 17:14:31 +0100 Subject: [PATCH 59/61] New automatic user event collection (#4674) --- docs/test.ts | 2 +- index.d.ts | 22 +- .../src/passport-http.js | 16 +- .../src/passport-local.js | 16 +- .../src/passport-utils.js | 62 +- .../test/passport-http.spec.js | 115 ++- .../test/passport-local.spec.js | 132 ++-- .../test/passport-utils.spec.js | 36 - packages/dd-trace/src/appsec/addresses.js | 3 + packages/dd-trace/src/appsec/index.js | 15 +- packages/dd-trace/src/appsec/passport.js | 110 --- .../src/appsec/remote_config/capabilities.js | 1 + .../src/appsec/remote_config/index.js | 26 +- .../dd-trace/src/appsec/sdk/track_event.js | 51 +- packages/dd-trace/src/appsec/telemetry.js | 10 + packages/dd-trace/src/appsec/user_tracking.js | 168 +++++ packages/dd-trace/src/config.js | 20 +- packages/dd-trace/test/appsec/index.spec.js | 154 ++-- .../dd-trace/test/appsec/passport.spec.js | 245 ------ .../test/appsec/remote_config/index.spec.js | 91 ++- .../test/appsec/sdk/track_event.spec.js | 145 ++-- .../dd-trace/test/appsec/telemetry.spec.js | 11 + .../test/appsec/user_tracking.spec.js | 696 ++++++++++++++++++ packages/dd-trace/test/config.spec.js | 26 +- 24 files changed, 1485 insertions(+), 688 deletions(-) delete mode 100644 packages/datadog-instrumentations/test/passport-utils.spec.js delete mode 100644 packages/dd-trace/src/appsec/passport.js create mode 100644 packages/dd-trace/src/appsec/user_tracking.js delete mode 100644 packages/dd-trace/test/appsec/passport.spec.js create mode 100644 packages/dd-trace/test/appsec/user_tracking.spec.js diff --git a/docs/test.ts b/docs/test.ts index 479b4620b4d..ce34a23d62b 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -111,7 +111,7 @@ tracer.init({ blockedTemplateJson: './blocked.json', blockedTemplateGraphql: './blockedgraphql.json', eventTracking: { - mode: 'safe' + mode: 'anon' }, apiSecurity: { enabled: true, diff --git a/index.d.ts b/index.d.ts index 9b4becec957..a41b4aee410 100644 --- a/index.d.ts +++ b/index.d.ts @@ -655,12 +655,24 @@ declare namespace tracer { */ eventTracking?: { /** - * Controls the automated user event tracking mode. Possible values are disabled, safe and extended. - * On safe mode, any detected Personally Identifiable Information (PII) about the user will be redacted from the event. - * On extended mode, no redaction will take place. - * @default 'safe' + * Controls the automated user tracking mode for user IDs and logins collections. Possible values: + * * 'anonymous': will hash user IDs and user logins before collecting them + * * 'anon': alias for 'anonymous' + * * 'safe': deprecated alias for 'anonymous' + * + * * 'identification': will collect user IDs and logins without redaction + * * 'ident': alias for 'identification' + * * 'extended': deprecated alias for 'identification' + * + * * 'disabled': will not collect user IDs and logins + * + * Unknown values will be considered as 'disabled' + * @default 'identification' */ - mode?: 'safe' | 'extended' | 'disabled' + mode?: + 'anonymous' | 'anon' | 'safe' | + 'identification' | 'ident' | 'extended' | + 'disabled' }, /** * Configuration for Api Security diff --git a/packages/datadog-instrumentations/src/passport-http.js b/packages/datadog-instrumentations/src/passport-http.js index 0969d2d3fc9..3b930a1a1cc 100644 --- a/packages/datadog-instrumentations/src/passport-http.js +++ b/packages/datadog-instrumentations/src/passport-http.js @@ -1,22 +1,10 @@ 'use strict' -const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const { wrapVerify } = require('./passport-utils') +const { strategyHook } = require('./passport-utils') addHook({ name: 'passport-http', file: 'lib/passport-http/strategies/basic.js', versions: ['>=0.3.0'] -}, BasicStrategy => { - return shimmer.wrapFunction(BasicStrategy, BasicStrategy => function () { - const type = 'http' - - if (typeof arguments[0] === 'function') { - arguments[0] = wrapVerify(arguments[0], false, type) - } else { - arguments[1] = wrapVerify(arguments[1], (arguments[0] && arguments[0].passReqToCallback), type) - } - return BasicStrategy.apply(this, arguments) - }) -}) +}, strategyHook) diff --git a/packages/datadog-instrumentations/src/passport-local.js b/packages/datadog-instrumentations/src/passport-local.js index dab74eb470e..c6dcec9a48d 100644 --- a/packages/datadog-instrumentations/src/passport-local.js +++ b/packages/datadog-instrumentations/src/passport-local.js @@ -1,22 +1,10 @@ 'use strict' -const shimmer = require('../../datadog-shimmer') const { addHook } = require('./helpers/instrument') -const { wrapVerify } = require('./passport-utils') +const { strategyHook } = require('./passport-utils') addHook({ name: 'passport-local', file: 'lib/strategy.js', versions: ['>=1.0.0'] -}, Strategy => { - return shimmer.wrapFunction(Strategy, Strategy => function () { - const type = 'local' - - if (typeof arguments[0] === 'function') { - arguments[0] = wrapVerify(arguments[0], false, type) - } else { - arguments[1] = wrapVerify(arguments[1], (arguments[0] && arguments[0].passReqToCallback), type) - } - return Strategy.apply(this, arguments) - }) -}) +}, strategyHook) diff --git a/packages/datadog-instrumentations/src/passport-utils.js b/packages/datadog-instrumentations/src/passport-utils.js index 7969ab486b4..de1cd090a71 100644 --- a/packages/datadog-instrumentations/src/passport-utils.js +++ b/packages/datadog-instrumentations/src/passport-utils.js @@ -5,33 +5,57 @@ const { channel } = require('./helpers/instrument') const passportVerifyChannel = channel('datadog:passport:verify:finish') -function wrapVerifiedAndPublish (username, password, verified, type) { - if (!passportVerifyChannel.hasSubscribers) { - return verified - } +function wrapVerifiedAndPublish (framework, username, verified) { + return shimmer.wrapFunction(verified, function wrapVerified (verified) { + return function wrappedVerified (err, user) { + // if there is an error, it's neither an auth success nor a failure + if (!err) { + const abortController = new AbortController() + + passportVerifyChannel.publish({ framework, login: username, user, success: !!user, abortController }) + + if (abortController.signal.aborted) return + } - // eslint-disable-next-line n/handle-callback-err - return shimmer.wrapFunction(verified, verified => function (err, user, info) { - const credentials = { type, username } - passportVerifyChannel.publish({ credentials, user }) - return verified.apply(this, arguments) + return verified.apply(this, arguments) + } }) } -function wrapVerify (verify, passReq, type) { - if (passReq) { - return function (req, username, password, verified) { - arguments[3] = wrapVerifiedAndPublish(username, password, verified, type) - return verify.apply(this, arguments) +function wrapVerify (verify) { + return function wrappedVerify (req, username, password, verified) { + if (passportVerifyChannel.hasSubscribers) { + const framework = `passport-${this.name}` + + // replace the callback with our own wrapper to get the result + if (this._passReqToCallback) { + arguments[3] = wrapVerifiedAndPublish(framework, arguments[1], arguments[3]) + } else { + arguments[2] = wrapVerifiedAndPublish(framework, arguments[0], arguments[2]) + } } - } else { - return function (username, password, verified) { - arguments[2] = wrapVerifiedAndPublish(username, password, verified, type) - return verify.apply(this, arguments) + + return verify.apply(this, arguments) + } +} + +function wrapStrategy (Strategy) { + return function wrappedStrategy () { + // verify function can be either the first or second argument + if (typeof arguments[0] === 'function') { + arguments[0] = wrapVerify(arguments[0]) + } else { + arguments[1] = wrapVerify(arguments[1]) } + + return Strategy.apply(this, arguments) } } +function strategyHook (Strategy) { + return shimmer.wrapFunction(Strategy, wrapStrategy) +} + module.exports = { - wrapVerify + strategyHook } diff --git a/packages/datadog-instrumentations/test/passport-http.spec.js b/packages/datadog-instrumentations/test/passport-http.spec.js index 2918c935e20..5cb0282ec2f 100644 --- a/packages/datadog-instrumentations/test/passport-http.spec.js +++ b/packages/datadog-instrumentations/test/passport-http.spec.js @@ -1,8 +1,9 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') -const axios = require('axios') +const axios = require('axios').create({ validateStatus: null }) const dc = require('dc-polyfill') +const { storage } = require('../../datadog-core') withVersions('passport-http', 'passport-http', version => { describe('passport-http instrumentation', () => { @@ -10,7 +11,7 @@ withVersions('passport-http', 'passport-http', version => { let port, server, subscriberStub before(() => { - return agent.load(['express', 'passport', 'passport-http'], { client: false }) + return agent.load(['http', 'express', 'passport', 'passport-http'], { client: false }) }) before((done) => { @@ -19,7 +20,17 @@ withVersions('passport-http', 'passport-http', version => { const BasicStrategy = require(`../../../versions/passport-http@${version}`).get().BasicStrategy const app = express() - passport.use(new BasicStrategy((username, password, done) => { + function validateUser (req, username, password, done) { + // support with or without passReqToCallback + if (typeof done !== 'function') { + done = password + password = username + username = req + } + + // simulate db error + if (username === 'error') return done('error') + const users = [{ _id: 1, username: 'test', @@ -35,7 +46,18 @@ withVersions('passport-http', 'passport-http', version => { return done(null, user) } } - )) + + passport.use('basic', new BasicStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: false + }, validateUser)) + + passport.use('basic-withreq', new BasicStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true + }, validateUser)) app.use(passport.initialize()) app.use(express.json()) @@ -44,16 +66,14 @@ withVersions('passport-http', 'passport-http', version => { passport.authenticate('basic', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: false, session: false }) ) - app.post('/req', - passport.authenticate('basic', { + app.get('/req', + passport.authenticate('basic-withreq', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: true, session: false }) ) @@ -66,9 +86,7 @@ withVersions('passport-http', 'passport-http', version => { res.send('Denied') }) - passportVerifyChannel.subscribe(function ({ credentials, user, err, info }) { - subscriberStub(arguments[0]) - }) + passportVerifyChannel.subscribe((data) => subscriberStub(data)) server = app.listen(0, () => { port = server.address().port @@ -85,6 +103,18 @@ withVersions('passport-http', 'passport-http', version => { return agent.close({ ritmReset: false }) }) + it('should not call subscriber when an error occurs', async () => { + const res = await axios.get(`http://localhost:${port}/`, { + headers: { + // error:1234 + Authorization: 'Basic ZXJyb3I6MTIzNA==' + } + }) + + expect(res.status).to.equal(500) + expect(subscriberStub).to.not.be.called + }) + it('should call subscriber with proper arguments on success', async () => { const res = await axios.get(`http://localhost:${port}/`, { headers: { @@ -95,16 +125,17 @@ withVersions('passport-http', 'passport-http', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'http', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on success with passReqToCallback set to true', async () => { - const res = await axios.get(`http://localhost:${port}/`, { + const res = await axios.get(`http://localhost:${port}/req`, { headers: { // test:1234 Authorization: 'Basic dGVzdDoxMjM0' @@ -113,12 +144,13 @@ withVersions('passport-http', 'passport-http', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'http', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on failure', async () => { @@ -131,12 +163,37 @@ withVersions('passport-http', 'passport-http', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Denied') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'http', username: 'test' }, - user: false + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: false, + success: false, + abortController: new AbortController() + }) + }) + + it('should block when subscriber aborts', async () => { + subscriberStub = sinon.spy(({ abortController }) => { + storage.getStore().req.res.writeHead(403).end('Blocked') + abortController.abort() + }) + + const res = await axios.get(`http://localhost:${port}/`, { + headers: { + // test:1234 + Authorization: 'Basic dGVzdDoxMjM0' } - ) + }) + + expect(res.status).to.equal(403) + expect(res.data).to.equal('Blocked') + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-basic', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) }) }) diff --git a/packages/datadog-instrumentations/test/passport-local.spec.js b/packages/datadog-instrumentations/test/passport-local.spec.js index d54f02b289f..bcfc2e56dc9 100644 --- a/packages/datadog-instrumentations/test/passport-local.spec.js +++ b/packages/datadog-instrumentations/test/passport-local.spec.js @@ -1,8 +1,9 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') -const axios = require('axios') +const axios = require('axios').create({ validateStatus: null }) const dc = require('dc-polyfill') +const { storage } = require('../../datadog-core') withVersions('passport-local', 'passport-local', version => { describe('passport-local instrumentation', () => { @@ -10,7 +11,7 @@ withVersions('passport-local', 'passport-local', version => { let port, server, subscriberStub before(() => { - return agent.load(['express', 'passport', 'passport-local'], { client: false }) + return agent.load(['http', 'express', 'passport', 'passport-local'], { client: false }) }) before((done) => { @@ -19,24 +20,44 @@ withVersions('passport-local', 'passport-local', version => { const LocalStrategy = require(`../../../versions/passport-local@${version}`).get().Strategy const app = express() - passport.use(new LocalStrategy({ usernameField: 'username', passwordField: 'password' }, - (username, password, done) => { - const users = [{ - _id: 1, - username: 'test', - password: '1234', - email: 'testuser@ddog.com' - }] - - const user = users.find(user => (user.username === username) && (user.password === password)) - - if (!user) { - return done(null, false) - } else { - return done(null, user) - } + function validateUser (req, username, password, done) { + // support with or without passReqToCallback + if (typeof done !== 'function') { + done = password + password = username + username = req } - )) + + // simulate db error + if (username === 'error') return done('error') + + const users = [{ + _id: 1, + username: 'test', + password: '1234', + email: 'testuser@ddog.com' + }] + + const user = users.find(user => (user.username === username) && (user.password === password)) + + if (!user) { + return done(null, false) + } else { + return done(null, user) + } + } + + passport.use('local', new LocalStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: false + }, validateUser)) + + passport.use('local-withreq', new LocalStrategy({ + usernameField: 'username', + passwordField: 'password', + passReqToCallback: true + }, validateUser)) app.use(passport.initialize()) app.use(express.json()) @@ -45,16 +66,14 @@ withVersions('passport-local', 'passport-local', version => { passport.authenticate('local', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: false, session: false }) ) app.post('/req', - passport.authenticate('local', { + passport.authenticate('local-withreq', { successRedirect: '/grant', failureRedirect: '/deny', - passReqToCallback: true, session: false }) ) @@ -67,9 +86,7 @@ withVersions('passport-local', 'passport-local', version => { res.send('Denied') }) - passportVerifyChannel.subscribe(function ({ credentials, user, err, info }) { - subscriberStub(arguments[0]) - }) + passportVerifyChannel.subscribe((data) => subscriberStub(data)) server = app.listen(0, () => { port = server.address().port @@ -86,17 +103,25 @@ withVersions('passport-local', 'passport-local', version => { return agent.close({ ritmReset: false }) }) + it('should not call subscriber when an error occurs', async () => { + const res = await axios.post(`http://localhost:${port}/`, { username: 'error', password: '1234' }) + + expect(res.status).to.equal(500) + expect(subscriberStub).to.not.be.called + }) + it('should call subscriber with proper arguments on success', async () => { const res = await axios.post(`http://localhost:${port}/`, { username: 'test', password: '1234' }) expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'local', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on success with passReqToCallback set to true', async () => { @@ -104,12 +129,13 @@ withVersions('passport-local', 'passport-local', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Granted') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'local', username: 'test' }, - user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' } - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) it('should call subscriber with proper arguments on failure', async () => { @@ -117,12 +143,32 @@ withVersions('passport-local', 'passport-local', version => { expect(res.status).to.equal(200) expect(res.data).to.equal('Denied') - expect(subscriberStub).to.be.calledOnceWithExactly( - { - credentials: { type: 'local', username: 'test' }, - user: false - } - ) + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: false, + success: false, + abortController: new AbortController() + }) + }) + + it('should block when subscriber aborts', async () => { + subscriberStub = sinon.spy(({ abortController }) => { + storage.getStore().req.res.writeHead(403).end('Blocked') + abortController.abort() + }) + + const res = await axios.post(`http://localhost:${port}/`, { username: 'test', password: '1234' }) + + expect(res.status).to.equal(403) + expect(res.data).to.equal('Blocked') + expect(subscriberStub).to.be.calledOnceWithExactly({ + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234', email: 'testuser@ddog.com' }, + success: true, + abortController: new AbortController() + }) }) }) }) diff --git a/packages/datadog-instrumentations/test/passport-utils.spec.js b/packages/datadog-instrumentations/test/passport-utils.spec.js deleted file mode 100644 index 3cf6a64a60a..00000000000 --- a/packages/datadog-instrumentations/test/passport-utils.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' - -const proxyquire = require('proxyquire') -const { channel } = require('../src/helpers/instrument') - -const passportVerifyChannel = channel('datadog:passport:verify:finish') - -describe('passport-utils', () => { - const shimmer = { - wrap: sinon.stub() - } - - let passportUtils - - beforeEach(() => { - passportUtils = proxyquire('../src/passport-utils', { - '../../datadog-shimmer': shimmer - }) - }) - - it('should not call wrap when there is no subscribers', () => { - const wrap = passportUtils.wrapVerify(() => {}, false, 'type') - - wrap() - expect(shimmer.wrap).not.to.have.been.called - }) - - it('should call wrap when there is subscribers', () => { - const wrap = passportUtils.wrapVerify(() => {}, false, 'type') - - passportVerifyChannel.subscribe(() => {}) - - wrap() - expect(shimmer.wrap).to.have.been.called - }) -}) diff --git a/packages/dd-trace/src/appsec/addresses.js b/packages/dd-trace/src/appsec/addresses.js index cb540bc4e6f..a492a5e454f 100644 --- a/packages/dd-trace/src/appsec/addresses.js +++ b/packages/dd-trace/src/appsec/addresses.js @@ -1,5 +1,6 @@ 'use strict' +// TODO: reorder all this, it's a mess module.exports = { HTTP_INCOMING_BODY: 'server.request.body', HTTP_INCOMING_QUERY: 'server.request.query', @@ -20,6 +21,8 @@ module.exports = { HTTP_CLIENT_IP: 'http.client_ip', USER_ID: 'usr.id', + USER_LOGIN: 'usr.login', + WAF_CONTEXT_PROCESSOR: 'waf.context.processor', HTTP_OUTGOING_URL: 'server.io.net.url', diff --git a/packages/dd-trace/src/appsec/index.js b/packages/dd-trace/src/appsec/index.js index d4f4adc6554..db089a61dca 100644 --- a/packages/dd-trace/src/appsec/index.js +++ b/packages/dd-trace/src/appsec/index.js @@ -28,7 +28,7 @@ const web = require('../plugins/util/web') const { extractIp } = require('../plugins/util/ip_extractor') const { HTTP_CLIENT_IP } = require('../../../../ext/tags') const { isBlocked, block, setTemplates, getBlockingAction } = require('./blocking') -const { passportTrackEvent } = require('./passport') +const UserTracking = require('./user_tracking') const { storage } = require('../../../datadog-core') const graphql = require('./graphql') const rasp = require('./rasp') @@ -59,11 +59,14 @@ function enable (_config) { apiSecuritySampler.configure(_config.appsec) + UserTracking.setCollectionMode(_config.appsec.eventTracking.mode, false) + bodyParser.subscribe(onRequestBodyParsed) multerParser.subscribe(onRequestBodyParsed) cookieParser.subscribe(onRequestCookieParser) incomingHttpRequestStart.subscribe(incomingHttpStartTranslator) incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator) + passportVerify.subscribe(onPassportVerify) // possible optimization: only subscribe if collection mode is enabled queryParser.subscribe(onRequestQueryParsed) nextBodyParsed.subscribe(onRequestBodyParsed) nextQueryParsed.subscribe(onRequestQueryParsed) @@ -73,10 +76,6 @@ function enable (_config) { responseWriteHead.subscribe(onResponseWriteHead) responseSetHeader.subscribe(onResponseSetHeader) - if (_config.appsec.eventTracking.enabled) { - passportVerify.subscribe(onPassportVerify) - } - isEnabled = true config = _config } catch (err) { @@ -184,7 +183,7 @@ function incomingHttpEndTranslator ({ req, res }) { Reporter.finishRequest(req, res) } -function onPassportVerify ({ credentials, user }) { +function onPassportVerify ({ framework, login, user, success, abortController }) { const store = storage.getStore() const rootSpan = store?.req && web.root(store.req) @@ -193,7 +192,9 @@ function onPassportVerify ({ credentials, user }) { return } - passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode) + const results = UserTracking.trackLogin(framework, login, user, success, rootSpan) + + handleResults(results, store.req, store.req.res, rootSpan, abortController) } function onRequestQueryParsed ({ req, res, query, abortController }) { diff --git a/packages/dd-trace/src/appsec/passport.js b/packages/dd-trace/src/appsec/passport.js deleted file mode 100644 index 2093b7b1fdc..00000000000 --- a/packages/dd-trace/src/appsec/passport.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -const log = require('../log') -const { trackEvent } = require('./sdk/track_event') -const { setUserTags } = require('./sdk/set_user') - -const UUID_PATTERN = '^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$' -const regexUsername = new RegExp(UUID_PATTERN, 'i') - -const SDK_USER_EVENT_PATTERN = '^_dd\\.appsec\\.events\\.users\\.[\\W\\w+]+\\.sdk$' -const regexSdkEvent = new RegExp(SDK_USER_EVENT_PATTERN, 'i') - -function isSdkCalled (tags) { - let called = false - - if (tags !== null && typeof tags === 'object') { - called = Object.entries(tags).some(([key, value]) => regexSdkEvent.test(key) && value === 'true') - } - - return called -} - -// delete this function later if we know it's always credential.username -function getLogin (credentials) { - const type = credentials && credentials.type - let login - if (type === 'local' || type === 'http') { - login = credentials.username - } - - return login -} - -function parseUser (login, passportUser, mode) { - const user = { - 'usr.id': login - } - - if (!user['usr.id']) { - return user - } - - if (passportUser) { - // Guess id - if (passportUser.id) { - user['usr.id'] = passportUser.id - } else if (passportUser._id) { - user['usr.id'] = passportUser._id - } - - if (mode === 'extended') { - if (login) { - user['usr.login'] = login - } - - if (passportUser.email) { - user['usr.email'] = passportUser.email - } - - // Guess username - if (passportUser.username) { - user['usr.username'] = passportUser.username - } else if (passportUser.name) { - user['usr.username'] = passportUser.name - } - } - } - - if (mode === 'safe') { - // Remove PII in safe mode - if (!regexUsername.test(user['usr.id'])) { - user['usr.id'] = '' - } - } - - return user -} - -function passportTrackEvent (credentials, passportUser, rootSpan, mode) { - const tags = rootSpan && rootSpan.context() && rootSpan.context()._tags - - if (isSdkCalled(tags)) { - // Don't overwrite tags set by SDK callings - return - } - const user = parseUser(getLogin(credentials), passportUser, mode) - - if (user['usr.id'] === undefined) { - log.warn('No user ID found in authentication instrumentation') - return - } - - if (passportUser) { - // If a passportUser object is published then the login succeded - const userTags = {} - Object.entries(user).forEach(([k, v]) => { - const attr = k.split('.', 2)[1] - userTags[attr] = v - }) - - setUserTags(userTags, rootSpan) - trackEvent('users.login.success', null, 'passportTrackEvent', rootSpan, mode) - } else { - trackEvent('users.login.failure', user, 'passportTrackEvent', rootSpan, mode) - } -} - -module.exports = { - passportTrackEvent -} diff --git a/packages/dd-trace/src/appsec/remote_config/capabilities.js b/packages/dd-trace/src/appsec/remote_config/capabilities.js index bd729cc39cc..16034f5f9ee 100644 --- a/packages/dd-trace/src/appsec/remote_config/capabilities.js +++ b/packages/dd-trace/src/appsec/remote_config/capabilities.js @@ -22,6 +22,7 @@ module.exports = { ASM_RASP_SSRF: 1n << 23n, ASM_RASP_SHI: 1n << 24n, APM_TRACING_SAMPLE_RULES: 1n << 29n, + ASM_AUTO_USER_INSTRUM_MODE: 1n << 31n, ASM_ENDPOINT_FINGERPRINT: 1n << 32n, ASM_NETWORK_FINGERPRINT: 1n << 34n, ASM_HEADER_FINGERPRINT: 1n << 35n diff --git a/packages/dd-trace/src/appsec/remote_config/index.js b/packages/dd-trace/src/appsec/remote_config/index.js index 90cda5c6f61..7884175abb0 100644 --- a/packages/dd-trace/src/appsec/remote_config/index.js +++ b/packages/dd-trace/src/appsec/remote_config/index.js @@ -4,6 +4,8 @@ const Activation = require('../activation') const RemoteConfigManager = require('./manager') const RemoteConfigCapabilities = require('./capabilities') +const { setCollectionMode } = require('../user_tracking') +const log = require('../../log') let rc @@ -23,9 +25,31 @@ function enable (config, appsec) { rc.updateCapabilities(RemoteConfigCapabilities.ASM_ACTIVATION, true) } - rc.setProductHandler('ASM_FEATURES', (action, rcConfig) => { + rc.updateCapabilities(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) + + let autoUserInstrumModeId + + rc.setProductHandler('ASM_FEATURES', (action, rcConfig, configId) => { if (!rcConfig) return + // this is put before other handlers because it can reject the config + if (typeof rcConfig.auto_user_instrum?.mode === 'string') { + if (action === 'apply' || action === 'modify') { + // check if there is already a config applied with this field + if (autoUserInstrumModeId && configId !== autoUserInstrumModeId) { + log.error('[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config') + // eslint-disable-next-line no-throw-literal + throw 'Multiple auto_user_instrum.mode received in ASM_FEATURES' + } + + setCollectionMode(rcConfig.auto_user_instrum.mode) + autoUserInstrumModeId = configId + } else if (configId === autoUserInstrumModeId) { + setCollectionMode(config.appsec.eventTracking.mode) + autoUserInstrumModeId = null + } + } + if (activation === Activation.ONECLICK) { enableOrDisableAppsec(action, rcConfig, config, appsec) } diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index 0c1ef9c2bd9..a04f596bbc3 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -7,6 +7,7 @@ const standalone = require('../standalone') const waf = require('../waf') const { SAMPLING_MECHANISM_APPSEC } = require('../../constants') const { keepTrace } = require('../../priority_sampler') +const addresses = require('../addresses') function trackUserLoginSuccessEvent (tracer, user, metadata) { // TODO: better user check here and in _setUser() ? @@ -23,7 +24,13 @@ function trackUserLoginSuccessEvent (tracer, user, metadata) { setUserTags(user, rootSpan) - trackEvent('users.login.success', metadata, 'trackUserLoginSuccessEvent', rootSpan, 'sdk') + const login = user.login ?? user.id + + metadata = { 'usr.login': login, ...metadata } + + trackEvent('users.login.success', metadata, 'trackUserLoginSuccessEvent', rootSpan) + + runWaf('users.login.success', { id: user.id, login }) } function trackUserLoginFailureEvent (tracer, userId, exists, metadata) { @@ -34,11 +41,14 @@ function trackUserLoginFailureEvent (tracer, userId, exists, metadata) { const fields = { 'usr.id': userId, + 'usr.login': userId, 'usr.exists': exists ? 'true' : 'false', ...metadata } - trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer), 'sdk') + trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer)) + + runWaf('users.login.failure', { login: userId }) } function trackCustomEvent (tracer, eventName, metadata) { @@ -47,27 +57,18 @@ function trackCustomEvent (tracer, eventName, metadata) { return } - trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer), 'sdk') + trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer)) } -function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) { +function trackEvent (eventName, fields, sdkMethodName, rootSpan) { if (!rootSpan) { log.warn('[ASM] Root span not available in %s', sdkMethodName) return } - keepTrace(rootSpan, SAMPLING_MECHANISM_APPSEC) - const tags = { - [`appsec.events.${eventName}.track`]: 'true' - } - - if (mode === 'sdk') { - tags[`_dd.appsec.events.${eventName}.sdk`] = 'true' - } - - if (mode === 'safe' || mode === 'extended') { - tags[`_dd.appsec.events.${eventName}.auto.mode`] = mode + [`appsec.events.${eventName}.track`]: 'true', + [`_dd.appsec.events.${eventName}.sdk`]: 'true' } if (fields) { @@ -78,16 +79,28 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode) { rootSpan.addTags(tags) + keepTrace(rootSpan, SAMPLING_MECHANISM_APPSEC) standalone.sample(rootSpan) +} + +function runWaf (eventName, user) { + const persistent = { + [`server.business_logic.${eventName}`]: null + } + + if (user.id) { + persistent[addresses.USER_ID] = '' + user.id + } - if (['users.login.success', 'users.login.failure'].includes(eventName)) { - waf.run({ persistent: { [`server.business_logic.${eventName}`]: null } }) + if (user.login) { + persistent[addresses.USER_LOGIN] = '' + user.login } + + waf.run({ persistent }) } module.exports = { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, - trackCustomEvent, - trackEvent + trackCustomEvent } diff --git a/packages/dd-trace/src/appsec/telemetry.js b/packages/dd-trace/src/appsec/telemetry.js index d96ca77601f..8e9a2518f80 100644 --- a/packages/dd-trace/src/appsec/telemetry.js +++ b/packages/dd-trace/src/appsec/telemetry.js @@ -172,6 +172,15 @@ function addRaspRequestMetrics (store, { duration, durationExt }) { store[DD_TELEMETRY_REQUEST_METRICS].raspEvalCount++ } +function incrementMissingUserLoginMetric (framework, eventType) { + if (!enabled) return + + appsecMetrics.count('instrum.user_auth.missing_user_login', { + framework, + event_type: eventType + }).inc() +} + function getRequestMetrics (req) { if (req) { const store = getStore(req) @@ -188,6 +197,7 @@ module.exports = { incrementWafInitMetric, incrementWafUpdatesMetric, incrementWafRequestsMetric, + incrementMissingUserLoginMetric, getRequestMetrics } diff --git a/packages/dd-trace/src/appsec/user_tracking.js b/packages/dd-trace/src/appsec/user_tracking.js new file mode 100644 index 00000000000..5b92f80d642 --- /dev/null +++ b/packages/dd-trace/src/appsec/user_tracking.js @@ -0,0 +1,168 @@ +'use strict' + +const crypto = require('crypto') +const log = require('../log') +const telemetry = require('./telemetry') +const addresses = require('./addresses') +const { keepTrace } = require('../priority_sampler') +const { SAMPLING_MECHANISM_APPSEC } = require('../constants') +const standalone = require('./standalone') +const waf = require('./waf') + +// the RFC doesn't include '_id', but it's common in MongoDB +const USER_ID_FIELDS = ['id', '_id', 'email', 'username', 'login', 'user'] + +let collectionMode + +function setCollectionMode (mode, overwrite = true) { + // don't overwrite if already set, only used in appsec/index.js to not overwrite RC values + if (!overwrite && collectionMode) return + + /* eslint-disable no-fallthrough */ + switch (mode) { + case 'safe': + log.warn('[ASM] Using deprecated value "safe" in config.appsec.eventTracking.mode') + case 'anon': + case 'anonymization': + collectionMode = 'anonymization' + break + + case 'extended': + log.warn('[ASM] Using deprecated value "extended" in config.appsec.eventTracking.mode') + case 'ident': + case 'identification': + collectionMode = 'identification' + break + + default: + collectionMode = 'disabled' + } + /* eslint-enable no-fallthrough */ +} + +function obfuscateIfNeeded (str) { + if (collectionMode === 'anonymization') { + // get first 16 bytes of sha256 hash in lowercase hex + return 'anon_' + crypto.createHash('sha256').update(str).digest().toString('hex', 0, 16).toLowerCase() + } else { + return str + } +} + +// TODO: should we find other ways to get the user ID ? +function getUserId (user) { + if (!user) return + + for (const field of USER_ID_FIELDS) { + let id = user[field] + + // try to find a field that can be stringified + if (id && typeof id.toString === 'function') { + id = id.toString() + + if (typeof id !== 'string' || id.startsWith('[object ')) { + // probably not a usable ID ? + continue + } + + return obfuscateIfNeeded(id) + } + } +} + +function trackLogin (framework, login, user, success, rootSpan) { + if (!collectionMode || collectionMode === 'disabled') return + + if (!rootSpan) { + log.error('[ASM] No rootSpan found in AppSec trackLogin') + return + } + + if (typeof login !== 'string') { + log.error('[ASM] Invalid login provided to AppSec trackLogin') + + telemetry.incrementMissingUserLoginMetric(framework, success ? 'login_success' : 'login_failure') + // note: + // if we start supporting using userId if login is missing, we need to only give up if both are missing, and + // implement 'appsec.instrum.user_auth.missing_user_id' telemetry too + return + } + + login = obfuscateIfNeeded(login) + const userId = getUserId(user) + + let newTags + + const persistent = { + [addresses.USER_LOGIN]: login + } + + const currentTags = rootSpan.context()._tags + const isSdkCalled = currentTags[`_dd.appsec.events.users.login.${success ? 'success' : 'failure'}.sdk`] === 'true' + + // used to not overwrite tags set by SDK + function shouldSetTag (tag) { + return !(isSdkCalled && currentTags[tag]) + } + + if (success) { + newTags = { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': collectionMode, + '_dd.appsec.usr.login': login + } + + if (shouldSetTag('appsec.events.users.login.success.usr.login')) { + newTags['appsec.events.users.login.success.usr.login'] = login + } + + if (userId) { + newTags['_dd.appsec.usr.id'] = userId + + if (shouldSetTag('usr.id')) { + newTags['usr.id'] = userId + persistent[addresses.USER_ID] = userId + } + } + + persistent[addresses.LOGIN_SUCCESS] = null + } else { + newTags = { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': collectionMode, + '_dd.appsec.usr.login': login + } + + if (shouldSetTag('appsec.events.users.login.failure.usr.login')) { + newTags['appsec.events.users.login.failure.usr.login'] = login + } + + if (userId) { + newTags['_dd.appsec.usr.id'] = userId + + if (shouldSetTag('appsec.events.users.login.failure.usr.id')) { + newTags['appsec.events.users.login.failure.usr.id'] = userId + } + } + + /* TODO: if one day we have this info + if (exists != null && shouldSetTag('appsec.events.users.login.failure.usr.exists')) { + newTags['appsec.events.users.login.failure.usr.exists'] = exists + } + */ + + persistent[addresses.LOGIN_FAILURE] = null + } + + keepTrace(rootSpan, SAMPLING_MECHANISM_APPSEC) + standalone.sample(rootSpan) + + rootSpan.addTags(newTags) + + return waf.run({ persistent }) +} + +module.exports = { + setCollectionMode, + trackLogin +} diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 808704bd7e4..6f630212799 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -449,8 +449,7 @@ class Config { this._setValue(defaults, 'appsec.blockedTemplateHtml', undefined) this._setValue(defaults, 'appsec.blockedTemplateJson', undefined) this._setValue(defaults, 'appsec.enabled', undefined) - this._setValue(defaults, 'appsec.eventTracking.enabled', true) - this._setValue(defaults, 'appsec.eventTracking.mode', 'safe') + this._setValue(defaults, 'appsec.eventTracking.mode', 'identification') this._setValue(defaults, 'appsec.obfuscatorKeyRegex', defaultWafObfuscatorKeyRegex) this._setValue(defaults, 'appsec.obfuscatorValueRegex', defaultWafObfuscatorValueRegex) this._setValue(defaults, 'appsec.rasp.enabled', true) @@ -574,6 +573,7 @@ class Config { DD_AGENT_HOST, DD_API_SECURITY_ENABLED, DD_API_SECURITY_SAMPLE_DELAY, + DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE, DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING, DD_APPSEC_ENABLED, DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON, @@ -712,11 +712,10 @@ class Config { this._setValue(env, 'appsec.blockedTemplateJson', maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON)) this._envUnprocessed['appsec.blockedTemplateJson'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON this._setBoolean(env, 'appsec.enabled', DD_APPSEC_ENABLED) - if (DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING) { - this._setValue(env, 'appsec.eventTracking.enabled', - ['extended', 'safe'].includes(DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING.toLowerCase())) - this._setValue(env, 'appsec.eventTracking.mode', DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING.toLowerCase()) - } + this._setString(env, 'appsec.eventTracking.mode', coalesce( + DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE, + DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING // TODO: remove in next major + )) this._setString(env, 'appsec.obfuscatorKeyRegex', DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP) this._setString(env, 'appsec.obfuscatorValueRegex', DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP) this._setBoolean(env, 'appsec.rasp.enabled', DD_APPSEC_RASP_ENABLED) @@ -895,12 +894,7 @@ class Config { this._setValue(opts, 'appsec.blockedTemplateJson', maybeFile(options.appsec.blockedTemplateJson)) this._optsUnprocessed['appsec.blockedTemplateJson'] = options.appsec.blockedTemplateJson this._setBoolean(opts, 'appsec.enabled', options.appsec.enabled) - let eventTracking = options.appsec.eventTracking?.mode - if (eventTracking) { - eventTracking = eventTracking.toLowerCase() - this._setValue(opts, 'appsec.eventTracking.enabled', ['extended', 'safe'].includes(eventTracking)) - this._setValue(opts, 'appsec.eventTracking.mode', eventTracking) - } + this._setString(opts, 'appsec.eventTracking.mode', options.appsec.eventTracking?.mode) this._setString(opts, 'appsec.obfuscatorKeyRegex', options.appsec.obfuscatorKeyRegex) this._setString(opts, 'appsec.obfuscatorValueRegex', options.appsec.obfuscatorValueRegex) this._setBoolean(opts, 'appsec.rasp.enabled', options.appsec.rasp?.enabled) diff --git a/packages/dd-trace/test/appsec/index.spec.js b/packages/dd-trace/test/appsec/index.spec.js index 4ec92f7b0e6..7ca54e9241b 100644 --- a/packages/dd-trace/test/appsec/index.spec.js +++ b/packages/dd-trace/test/appsec/index.spec.js @@ -44,7 +44,7 @@ describe('AppSec Index', function () { let AppSec let web let blocking - let passport + let UserTracking let log let appsecTelemetry let graphql @@ -65,8 +65,7 @@ describe('AppSec Index', function () { blockedTemplateHtml: blockedTemplate.html, blockedTemplateJson: blockedTemplate.json, eventTracking: { - enabled: true, - mode: 'safe' + mode: 'anon' }, apiSecurity: { enabled: false, @@ -90,8 +89,9 @@ describe('AppSec Index', function () { setTemplates: sinon.stub() } - passport = { - passportTrackEvent: sinon.stub() + UserTracking = { + setCollectionMode: sinon.stub(), + trackLogin: sinon.stub() } log = { @@ -124,7 +124,7 @@ describe('AppSec Index', function () { '../log': log, '../plugins/util/web': web, './blocking': blocking, - './passport': passport, + './user_tracking': UserTracking, './telemetry': appsecTelemetry, './graphql': graphql, './api_security_sampler': apiSecuritySampler, @@ -152,6 +152,7 @@ describe('AppSec Index', function () { expect(blocking.setTemplates).to.have.been.calledOnceWithExactly(config) expect(RuleManager.loadRules).to.have.been.calledOnceWithExactly(config.appsec) expect(Reporter.setRateLimit).to.have.been.calledOnceWithExactly(42) + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly('anon', false) expect(incomingHttpRequestStart.subscribe) .to.have.been.calledOnceWithExactly(AppSec.incomingHttpStartTranslator) expect(incomingHttpRequestEnd.subscribe).to.have.been.calledOnceWithExactly(AppSec.incomingHttpEndTranslator) @@ -197,13 +198,13 @@ describe('AppSec Index', function () { expect(responseSetHeader.hasSubscribers).to.be.true }) - it('should not subscribe to passportVerify if eventTracking is disabled', () => { - config.appsec.eventTracking.enabled = false + it('should still subscribe to passportVerify if eventTracking is disabled', () => { + config.appsec.eventTracking.mode = 'disabled' AppSec.disable() AppSec.enable(config) - expect(passportVerify.hasSubscribers).to.be.false + expect(passportVerify.hasSubscribers).to.be.true }) it('should call appsec telemetry enable', () => { @@ -365,7 +366,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -403,7 +404,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -449,7 +450,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -515,7 +516,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -561,7 +562,7 @@ describe('AppSec Index', function () { const res = { getHeaders: () => ({ 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 }), statusCode: 201 } @@ -649,6 +650,17 @@ describe('AppSec Index', function () { abortController = { abort: sinon.stub() } + res = { + getHeaders: () => ({ + 'content-type': 'application/json', + 'content-length': 42 + }), + writeHead: sinon.stub(), + end: sinon.stub(), + getHeaderNames: sinon.stub().returns([]) + } + res.writeHead.returns(res) + req = { url: '/path', headers: { @@ -659,18 +671,9 @@ describe('AppSec Index', function () { socket: { remoteAddress: '127.0.0.1', remotePort: 8080 - } - } - res = { - getHeaders: () => ({ - 'content-type': 'application/json', - 'content-lenght': 42 - }), - writeHead: sinon.stub(), - end: sinon.stub(), - getHeaderNames: sinon.stub().returns([]) + }, + res } - res.writeHead.returns(res) AppSec.enable(config) AppSec.incomingHttpStartTranslator({ req, res }) @@ -807,31 +810,84 @@ describe('AppSec Index', function () { }) describe('onPassportVerify', () => { - it('Should call passportTrackEvent', () => { - const credentials = { type: 'local', username: 'test' } - const user = { id: '1234', username: 'Test' } + beforeEach(() => { + web.root.resetHistory() + sinon.stub(storage, 'getStore').returns({ req }) + }) - sinon.stub(storage, 'getStore').returns({ req: {} }) + it('should block when UserTracking.login() returns action', () => { + UserTracking.trackLogin.returns(resultActions) - passportVerify.publish({ credentials, user }) + const abortController = new AbortController() + const payload = { + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234' }, + success: true, + abortController + } + + passportVerify.publish(payload) + + expect(storage.getStore).to.have.been.calledOnce + expect(web.root).to.have.been.calledOnceWithExactly(req) + expect(UserTracking.trackLogin).to.have.been.calledOnceWithExactly( + payload.framework, + payload.login, + payload.user, + payload.success, + rootSpan + ) + expect(abortController.signal.aborted).to.be.true + expect(res.end).to.have.been.called + }) - expect(passport.passportTrackEvent).to.have.been.calledOnceWithExactly( - credentials, - user, - rootSpan, - config.appsec.eventTracking.mode) + it('should not block when UserTracking.login() returns nothing', () => { + UserTracking.trackLogin.returns(undefined) + + const abortController = new AbortController() + const payload = { + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234' }, + success: true, + abortController + } + + passportVerify.publish(payload) + + expect(storage.getStore).to.have.been.calledOnce + expect(web.root).to.have.been.calledOnceWithExactly(req) + expect(UserTracking.trackLogin).to.have.been.calledOnceWithExactly( + payload.framework, + payload.login, + payload.user, + payload.success, + rootSpan + ) + expect(abortController.signal.aborted).to.be.false + expect(res.end).to.not.have.been.called }) - it('Should call log if no rootSpan is found', () => { - const credentials = { type: 'local', username: 'test' } - const user = { id: '1234', username: 'Test' } + it('should not block and call log if no rootSpan is found', () => { + storage.getStore.returns(undefined) - sinon.stub(storage, 'getStore').returns(undefined) + const abortController = new AbortController() + const payload = { + framework: 'passport-local', + login: 'test', + user: { _id: 1, username: 'test', password: '1234' }, + success: true, + abortController + } - passportVerify.publish({ credentials, user }) + passportVerify.publish(payload) + expect(storage.getStore).to.have.been.calledOnce expect(log.warn).to.have.been.calledOnceWithExactly('[ASM] No rootSpan found in onPassportVerify') - expect(passport.passportTrackEvent).not.to.have.been.called + expect(UserTracking.trackLogin).to.not.have.been.called + expect(abortController.signal.aborted).to.be.false + expect(res.end).to.not.have.been.called }) }) @@ -841,7 +897,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -852,7 +908,7 @@ describe('AppSec Index', function () { 'server.response.status': '404', 'server.response.headers.no_cookies': { 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 } } }, req) @@ -873,7 +929,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -884,7 +940,7 @@ describe('AppSec Index', function () { 'server.response.status': '404', 'server.response.headers.no_cookies': { 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 } } }, req) @@ -904,7 +960,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -920,7 +976,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } @@ -931,7 +987,7 @@ describe('AppSec Index', function () { 'server.response.status': '404', 'server.response.headers.no_cookies': { 'content-type': 'application/json', - 'content-lenght': 42 + 'content-length': 42 } } }, req) @@ -947,7 +1003,7 @@ describe('AppSec Index', function () { const responseHeaders = { 'content-type': 'application/json', - 'content-lenght': 42, + 'content-length': 42, 'set-cookie': 'a=1;b=2' } responseWriteHead.publish({ req, res, abortController, statusCode: 404, responseHeaders }) diff --git a/packages/dd-trace/test/appsec/passport.spec.js b/packages/dd-trace/test/appsec/passport.spec.js deleted file mode 100644 index 7a3db36798c..00000000000 --- a/packages/dd-trace/test/appsec/passport.spec.js +++ /dev/null @@ -1,245 +0,0 @@ -'use strict' - -const proxyquire = require('proxyquire') - -describe('Passport', () => { - const rootSpan = { - context: () => { return {} } - } - const loginLocal = { type: 'local', username: 'test' } - const userUuid = { - id: '591dc126-8431-4d0f-9509-b23318d3dce4', - email: 'testUser@test.com', - username: 'Test User' - } - - let passportModule, log, events, setUser - - beforeEach(() => { - rootSpan.context = () => { return {} } - - log = { - warn: sinon.stub() - } - - events = { - trackEvent: sinon.stub() - } - - setUser = { - setUserTags: sinon.stub() - } - - passportModule = proxyquire('../../src/appsec/passport', { - '../log': log, - './sdk/track_event': events, - './sdk/set_user': setUser - }) - }) - - describe('passportTrackEvent', () => { - it('should call log when credentials is undefined', () => { - passportModule.passportTrackEvent(undefined, undefined, undefined, 'safe') - - expect(log.warn).to.have.been.calledOnceWithExactly('No user ID found in authentication instrumentation') - }) - - it('should call log when type is not known', () => { - const credentials = { type: 'unknown', username: 'test' } - - passportModule.passportTrackEvent(credentials, undefined, undefined, 'safe') - - expect(log.warn).to.have.been.calledOnceWithExactly('No user ID found in authentication instrumentation') - }) - - it('should call log when type is known but username not present', () => { - const credentials = { type: 'http' } - - passportModule.passportTrackEvent(credentials, undefined, undefined, 'safe') - - expect(log.warn).to.have.been.calledOnceWithExactly('No user ID found in authentication instrumentation') - }) - - it('should report login failure when passportUser is not present', () => { - passportModule.passportTrackEvent(loginLocal, undefined, undefined, 'safe') - - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.failure', - { 'usr.id': '' }, - 'passportTrackEvent', - undefined, - 'safe' - ) - }) - - it('should report login success when passportUser is present', () => { - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'safe') - - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly({ id: userUuid.id }, rootSpan) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'safe' - ) - }) - - it('should report login success and blank id in safe mode when id is not a uuid', () => { - const user = { - id: 'publicName', - email: 'testUser@test.com', - username: 'Test User' - } - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'safe') - - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly({ id: '' }, rootSpan) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'safe' - ) - }) - - it('should report login success and the extended fields in extended mode', () => { - const user = { - id: 'publicName', - email: 'testUser@test.com', - username: 'Test User' - } - - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'extended') - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly( - { - id: 'publicName', - login: 'test', - email: 'testUser@test.com', - username: 'Test User' - }, - rootSpan - ) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - - it('should not call trackEvent in safe mode if sdk user event functions are already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.users.login.success.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'safe') - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).not.to.have.been.called - }) - - it('should not call trackEvent in extended mode if trackUserLoginSuccessEvent is already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.users.login.success.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'extended') - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).not.to.have.been.called - }) - - it('should call trackEvent in extended mode if trackCustomEvent function is already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.custom.event.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'extended') - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - - it('should not call trackEvent in extended mode if trackUserLoginFailureEvent is already called', () => { - rootSpan.context = () => { - return { - _tags: { - '_dd.appsec.events.users.login.failure.sdk': 'true' - } - } - } - - passportModule.passportTrackEvent(loginLocal, userUuid, rootSpan, 'extended') - expect(setUser.setUserTags).not.to.have.been.called - expect(events.trackEvent).not.to.have.been.called - }) - - it('should report login success with the _id field', () => { - const user = { - _id: '591dc126-8431-4d0f-9509-b23318d3dce4', - email: 'testUser@test.com', - username: 'Test User' - } - - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'extended') - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly( - { - id: '591dc126-8431-4d0f-9509-b23318d3dce4', - login: 'test', - email: 'testUser@test.com', - username: 'Test User' - }, - rootSpan - ) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - - it('should report login success with the username field passport name', () => { - const user = { - email: 'testUser@test.com', - name: 'Test User' - } - - rootSpan.context = () => { return {} } - - passportModule.passportTrackEvent(loginLocal, user, rootSpan, 'extended') - expect(setUser.setUserTags).to.have.been.calledOnceWithExactly( - { - id: 'test', - login: 'test', - email: 'testUser@test.com', - username: 'Test User' - }, rootSpan) - expect(events.trackEvent).to.have.been.calledOnceWithExactly( - 'users.login.success', - null, - 'passportTrackEvent', - rootSpan, - 'extended' - ) - }) - }) -}) diff --git a/packages/dd-trace/test/appsec/remote_config/index.spec.js b/packages/dd-trace/test/appsec/remote_config/index.spec.js index 67447cf7a69..f3cc6a32dac 100644 --- a/packages/dd-trace/test/appsec/remote_config/index.spec.js +++ b/packages/dd-trace/test/appsec/remote_config/index.spec.js @@ -7,6 +7,8 @@ let config let rc let RemoteConfigManager let RuleManager +let UserTracking +let log let appsec let remoteConfig @@ -14,7 +16,10 @@ describe('Remote Config index', () => { beforeEach(() => { config = { appsec: { - enabled: undefined + enabled: undefined, + eventTracking: { + mode: 'identification' + } } } @@ -32,6 +37,14 @@ describe('Remote Config index', () => { updateWafFromRC: sinon.stub() } + UserTracking = { + setCollectionMode: sinon.stub() + } + + log = { + error: sinon.stub() + } + appsec = { enable: sinon.spy(), disable: sinon.spy() @@ -40,40 +53,48 @@ describe('Remote Config index', () => { remoteConfig = proxyquire('../src/appsec/remote_config', { './manager': RemoteConfigManager, '../rule_manager': RuleManager, + '../user_tracking': UserTracking, + '../../log': log, '..': appsec }) }) describe('enable', () => { it('should listen to remote config when appsec is not explicitly configured', () => { - config.appsec = { enabled: undefined } + config.appsec.enabled = undefined remoteConfig.enable(config) expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) expect(rc.updateCapabilities).to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_ACTIVATION, true) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) expect(rc.setProductHandler).to.have.been.calledWith('ASM_FEATURES') expect(rc.setProductHandler.firstCall.args[1]).to.be.a('function') }) it('should listen to remote config when appsec is explicitly configured as enabled=true', () => { - config.appsec = { enabled: true } + config.appsec.enabled = true remoteConfig.enable(config) expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) - expect(rc.updateCapabilities).to.not.have.been.calledWith('ASM_ACTIVATION') + expect(rc.updateCapabilities).to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_ACTIVATION) + expect(rc.updateCapabilities) + .to.have.been.calledWithExactly(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) expect(rc.setProductHandler).to.have.been.calledOnceWith('ASM_FEATURES') expect(rc.setProductHandler.firstCall.args[1]).to.be.a('function') }) it('should not listen to remote config when appsec is explicitly configured as enabled=false', () => { - config.appsec = { enabled: false } + config.appsec.enabled = false remoteConfig.enable(config) expect(RemoteConfigManager).to.have.been.calledOnceWithExactly(config) expect(rc.updateCapabilities).to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_ACTIVATION, true) + expect(rc.updateCapabilities) + .to.not.have.been.calledWith(RemoteConfigCapabilities.ASM_AUTO_USER_INSTRUM_MODE, true) expect(rc.setProductHandler).to.not.have.been.called }) @@ -81,8 +102,6 @@ describe('Remote Config index', () => { let listener beforeEach(() => { - config.appsec = { enabled: undefined } - remoteConfig.enable(config, appsec) listener = rc.setProductHandler.firstCall.args[1] @@ -100,8 +119,8 @@ describe('Remote Config index', () => { expect(appsec.enable).to.have.been.called }) - it('should disable appsec when listener is called with unnaply and enabled', () => { - listener('unnaply', { asm: { enabled: true } }) + it('should disable appsec when listener is called with unapply and enabled', () => { + listener('unapply', { asm: { enabled: true } }) expect(appsec.disable).to.have.been.calledOnce }) @@ -112,6 +131,60 @@ describe('Remote Config index', () => { expect(appsec.enable).to.not.have.been.called expect(appsec.disable).to.not.have.been.called }) + + describe('auto_user_instrum', () => { + const rcConfig = { auto_user_instrum: { mode: 'anonymous' } } + const configId = 'collectionModeId' + + afterEach(() => { + listener('unapply', rcConfig, configId) + }) + + it('should not update collection mode when not a string', () => { + listener('apply', { auto_user_instrum: { mode: 123 } }, configId) + + expect(UserTracking.setCollectionMode).to.not.have.been.called + }) + + it('should throw when called two times with different config ids', () => { + listener('apply', rcConfig, configId) + + expect(() => listener('apply', rcConfig, 'anotherId')).to.throw() + expect(log.error).to.have.been.calledOnceWithExactly( + '[RC] Multiple auto_user_instrum received in ASM_FEATURES. Discarding config' + ) + }) + + it('should update collection mode when called with apply', () => { + listener('apply', rcConfig, configId) + + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly(rcConfig.auto_user_instrum.mode) + }) + + it('should update collection mode when called with modify', () => { + listener('modify', rcConfig, configId) + + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly(rcConfig.auto_user_instrum.mode) + }) + + it('should revert collection mode when called with unapply', () => { + listener('apply', rcConfig, configId) + UserTracking.setCollectionMode.resetHistory() + + listener('unapply', rcConfig, configId) + + expect(UserTracking.setCollectionMode).to.have.been.calledOnceWithExactly(config.appsec.eventTracking.mode) + }) + + it('should not revert collection mode when called with unapply and unknown id', () => { + listener('apply', rcConfig, configId) + UserTracking.setCollectionMode.resetHistory() + + listener('unapply', rcConfig, 'unknownId') + + expect(UserTracking.setCollectionMode).to.not.have.been.called + }) + }) }) }) diff --git a/packages/dd-trace/test/appsec/sdk/track_event.spec.js b/packages/dd-trace/test/appsec/sdk/track_event.spec.js index 97f1ac07bd7..8e3c1a177bd 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -4,7 +4,7 @@ const proxyquire = require('proxyquire') const agent = require('../../plugins/agent') const axios = require('axios') const tracer = require('../../../../../index') -const { LOGIN_SUCCESS, LOGIN_FAILURE } = require('../../../src/appsec/addresses') +const { LOGIN_SUCCESS, LOGIN_FAILURE, USER_ID, USER_LOGIN } = require('../../../src/appsec/addresses') const { SAMPLING_MECHANISM_APPSEC } = require('../../../src/constants') const { USER_KEEP } = require('../../../../../ext/priority') @@ -12,13 +12,13 @@ describe('track_event', () => { describe('Internal API', () => { const tracer = {} let log + let prioritySampler let rootSpan let getRootSpan let setUserTags - let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent, trackEvent let sample let waf - let prioritySampler + let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent beforeEach(() => { log = { @@ -62,11 +62,6 @@ describe('track_event', () => { trackUserLoginSuccessEvent = trackEvents.trackUserLoginSuccessEvent trackUserLoginFailureEvent = trackEvents.trackUserLoginFailureEvent trackCustomEvent = trackEvents.trackCustomEvent - trackEvent = trackEvents.trackEvent - }) - - afterEach(() => { - sinon.restore() }) describe('trackUserLoginSuccessEvent', () => { @@ -108,12 +103,21 @@ describe('track_event', () => { { 'appsec.events.users.login.success.track': 'true', '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'user_id', 'appsec.events.users.login.success.metakey1': 'metaValue1', 'appsec.events.users.login.success.metakey2': 'metaValue2', 'appsec.events.users.login.success.metakey3': 'metaValue3' }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_SUCCESS]: null, + [USER_ID]: 'user_id', + [USER_LOGIN]: 'user_id' + } + }) }) it('should call setUser and addTags without metadata', () => { @@ -125,27 +129,50 @@ describe('track_event', () => { expect(setUserTags).to.have.been.calledOnceWithExactly(user, rootSpan) expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ 'appsec.events.users.login.success.track': 'true', - '_dd.appsec.events.users.login.success.sdk': 'true' + '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'user_id' }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_SUCCESS]: null, + [USER_ID]: 'user_id', + [USER_LOGIN]: 'user_id' + } + }) }) - it('should call waf run with login success address', () => { - const user = { id: 'user_id' } + it('should call waf with user login', () => { + const user = { id: 'user_id', login: 'user_login' } trackUserLoginSuccessEvent(tracer, user) - sinon.assert.calledOnceWithExactly( - waf.run, - { persistent: { [LOGIN_SUCCESS]: null } } - ) + + expect(log.warn).to.not.have.been.called + expect(setUserTags).to.have.been.calledOnceWithExactly(user, rootSpan) + expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'user_login' + }) + expect(prioritySampler.setPriority) + .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_SUCCESS]: null, + [USER_ID]: 'user_id', + [USER_LOGIN]: 'user_login' + } + }) }) }) describe('trackUserLoginFailureEvent', () => { it('should log warning when passed invalid userId', () => { - trackUserLoginFailureEvent(tracer, null, false) - trackUserLoginFailureEvent(tracer, [], false) + trackUserLoginFailureEvent(tracer, null, false, { key: 'value' }) + trackUserLoginFailureEvent(tracer, [], false, { key: 'value' }) expect(log.warn).to.have.been.calledTwice expect(log.warn.firstCall) @@ -159,7 +186,7 @@ describe('track_event', () => { it('should log warning when root span is not available', () => { rootSpan = undefined - trackUserLoginFailureEvent(tracer, 'user_id', false) + trackUserLoginFailureEvent(tracer, 'user_id', false, { key: 'value' }) expect(log.warn) .to.have.been.calledOnceWithExactly('[ASM] Root span not available in %s', 'trackUserLoginFailureEvent') @@ -168,7 +195,9 @@ describe('track_event', () => { it('should call addTags with metadata', () => { trackUserLoginFailureEvent(tracer, 'user_id', true, { - metakey1: 'metaValue1', metakey2: 'metaValue2', metakey3: 'metaValue3' + metakey1: 'metaValue1', + metakey2: 'metaValue2', + metakey3: 'metaValue3' }) expect(log.warn).to.not.have.been.called @@ -177,6 +206,7 @@ describe('track_event', () => { 'appsec.events.users.login.failure.track': 'true', '_dd.appsec.events.users.login.failure.sdk': 'true', 'appsec.events.users.login.failure.usr.id': 'user_id', + 'appsec.events.users.login.failure.usr.login': 'user_id', 'appsec.events.users.login.failure.usr.exists': 'true', 'appsec.events.users.login.failure.metakey1': 'metaValue1', 'appsec.events.users.login.failure.metakey2': 'metaValue2', @@ -184,11 +214,20 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_FAILURE]: null, + [USER_LOGIN]: 'user_id' + } + }) }) it('should send false `usr.exists` property when the user does not exist', () => { trackUserLoginFailureEvent(tracer, 'user_id', false, { - metakey1: 'metaValue1', metakey2: 'metaValue2', metakey3: 'metaValue3' + metakey1: 'metaValue1', + metakey2: 'metaValue2', + metakey3: 'metaValue3' }) expect(log.warn).to.not.have.been.called @@ -197,6 +236,7 @@ describe('track_event', () => { 'appsec.events.users.login.failure.track': 'true', '_dd.appsec.events.users.login.failure.sdk': 'true', 'appsec.events.users.login.failure.usr.id': 'user_id', + 'appsec.events.users.login.failure.usr.login': 'user_id', 'appsec.events.users.login.failure.usr.exists': 'false', 'appsec.events.users.login.failure.metakey1': 'metaValue1', 'appsec.events.users.login.failure.metakey2': 'metaValue2', @@ -204,6 +244,13 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_FAILURE]: null, + [USER_LOGIN]: 'user_id' + } + }) }) it('should call addTags without metadata', () => { @@ -215,18 +262,18 @@ describe('track_event', () => { 'appsec.events.users.login.failure.track': 'true', '_dd.appsec.events.users.login.failure.sdk': 'true', 'appsec.events.users.login.failure.usr.id': 'user_id', + 'appsec.events.users.login.failure.usr.login': 'user_id', 'appsec.events.users.login.failure.usr.exists': 'true' }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - - it('should call waf run with login failure address', () => { - trackUserLoginFailureEvent(tracer, 'user_id') - sinon.assert.calledOnceWithExactly( - waf.run, - { persistent: { [LOGIN_FAILURE]: null } } - ) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) + expect(waf.run).to.have.been.calledOnceWithExactly({ + persistent: { + [LOGIN_FAILURE]: null, + [USER_LOGIN]: 'user_id' + } + }) }) }) @@ -255,7 +302,10 @@ describe('track_event', () => { }) it('should call addTags with metadata', () => { - trackCustomEvent(tracer, 'custom_event', { metaKey1: 'metaValue1', metakey2: 'metaValue2' }) + trackCustomEvent(tracer, 'custom_event', { + metaKey1: 'metaValue1', + metakey2: 'metaValue2' + }) expect(log.warn).to.not.have.been.called expect(setUserTags).to.not.have.been.called @@ -267,6 +317,7 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + expect(sample).to.have.been.calledOnceWithExactly(rootSpan) }) it('should call addTags without metadata', () => { @@ -280,42 +331,6 @@ describe('track_event', () => { }) expect(prioritySampler.setPriority) .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - }) - - describe('trackEvent', () => { - it('should call addTags with safe mode', () => { - trackEvent('event', { metaKey1: 'metaValue1', metakey2: 'metaValue2' }, 'trackEvent', rootSpan, 'safe') - expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ - 'appsec.events.event.track': 'true', - '_dd.appsec.events.event.auto.mode': 'safe', - 'appsec.events.event.metaKey1': 'metaValue1', - 'appsec.events.event.metakey2': 'metaValue2' - }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - - it('should call addTags with extended mode', () => { - trackEvent('event', { metaKey1: 'metaValue1', metakey2: 'metaValue2' }, 'trackEvent', rootSpan, 'extended') - expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ - 'appsec.events.event.track': 'true', - '_dd.appsec.events.event.auto.mode': 'extended', - 'appsec.events.event.metaKey1': 'metaValue1', - 'appsec.events.event.metakey2': 'metaValue2' - }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) - }) - - it('should call standalone sample', () => { - trackEvent('event', undefined, 'trackEvent', rootSpan, undefined) - - expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ - 'appsec.events.event.track': 'true' - }) - expect(prioritySampler.setPriority) - .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) expect(sample).to.have.been.calledOnceWithExactly(rootSpan) }) }) diff --git a/packages/dd-trace/test/appsec/telemetry.spec.js b/packages/dd-trace/test/appsec/telemetry.spec.js index a297ede3280..3eb3b8521b4 100644 --- a/packages/dd-trace/test/appsec/telemetry.spec.js +++ b/packages/dd-trace/test/appsec/telemetry.spec.js @@ -339,6 +339,17 @@ describe('Appsec Telemetry metrics', () => { expect(count).to.not.have.been.called }) }) + + describe('incrementMissingUserLoginMetric', () => { + it('should increment instrum.user_auth.missing_user_login metric', () => { + appsecTelemetry.incrementMissingUserLoginMetric('passport-local', 'login_success') + + expect(count).to.have.been.calledOnceWithExactly('instrum.user_auth.missing_user_login', { + framework: 'passport-local', + event_type: 'login_success' + }) + }) + }) }) describe('if disabled', () => { diff --git a/packages/dd-trace/test/appsec/user_tracking.spec.js b/packages/dd-trace/test/appsec/user_tracking.spec.js new file mode 100644 index 00000000000..651048d5515 --- /dev/null +++ b/packages/dd-trace/test/appsec/user_tracking.spec.js @@ -0,0 +1,696 @@ +'use strict' + +const assert = require('assert') + +const log = require('../../src/log') +const telemetry = require('../../src/appsec/telemetry') +const { SAMPLING_MECHANISM_APPSEC } = require('../../src/constants') +const standalone = require('../../src/appsec/standalone') +const waf = require('../../src/appsec/waf') + +describe('User Tracking', () => { + let currentTags + let rootSpan + let keepTrace + + let setCollectionMode + let trackLogin + + beforeEach(() => { + sinon.stub(log, 'warn') + sinon.stub(log, 'error') + sinon.stub(telemetry, 'incrementMissingUserLoginMetric') + sinon.stub(standalone, 'sample') + sinon.stub(waf, 'run').returns(['action1']) + + currentTags = {} + + rootSpan = { + context: () => ({ _tags: currentTags }), + addTags: sinon.stub() + } + + keepTrace = sinon.stub() + + const UserTracking = proxyquire('../src/appsec/user_tracking', { + '../priority_sampler': { keepTrace } + }) + + setCollectionMode = UserTracking.setCollectionMode + trackLogin = UserTracking.trackLogin + }) + + afterEach(() => { + sinon.restore() + }) + + describe('getUserId', () => { + beforeEach(() => { + setCollectionMode('identification') + }) + + it('should find an id field in user object', () => { + const user = { + notId: 'no', + id: '123', + email: 'a@b.c' + } + + const results = trackLogin('passport-local', 'login', user, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should find an id-like field in user object when no id field is present', () => { + const user = { + notId: 'no', + email: 'a@b.c', + username: 'azerty' + } + + const results = trackLogin('passport-local', 'login', user, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': 'a@b.c', + 'usr.id': 'a@b.c' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': 'a@b.c', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should find a stringifiable id in user object', () => { + const stringifiableObject = { + a: 1, + toString: () => '123' + } + + const user = { + notId: 'no', + id: { a: 1 }, + _id: stringifiableObject, + email: 'a@b.c' + } + + const results = trackLogin('passport-local', 'login', user, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + }) + + describe('trackLogin', () => { + it('should not do anything if collectionMode is empty or disabled', () => { + setCollectionMode('disabled') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + it('should log error when rootSpan is not found', () => { + setCollectionMode('identification') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.calledOnceWithExactly(log.error, '[ASM] No rootSpan found in AppSec trackLogin') + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + it('should log error and send telemetry when login success is not a string', () => { + setCollectionMode('identification') + + const results = trackLogin('passport-local', {}, { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.calledOnceWithExactly(log.error, '[ASM] Invalid login provided to AppSec trackLogin') + sinon.assert.calledOnceWithExactly(telemetry.incrementMissingUserLoginMetric, 'passport-local', 'login_success') + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + it('should log error and send telemetry when login failure is not a string', () => { + setCollectionMode('identification') + + const results = trackLogin('passport-local', {}, { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.calledOnceWithExactly(log.error, '[ASM] Invalid login provided to AppSec trackLogin') + sinon.assert.calledOnceWithExactly(telemetry.incrementMissingUserLoginMetric, 'passport-local', 'login_failure') + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + + describe('when collectionMode is indentification', () => { + beforeEach(() => { + setCollectionMode('identification') + }) + + it('should write tags and call waf when success is true', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf when success is false', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.failure.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'appsec.events.users.login.failure.usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should not overwrite tags set by SDK when success is true', () => { + currentTags = { + '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'sdk_login', + 'usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + '_dd.appsec.usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should not overwwrite tags set by SDK when success is false', () => { + currentTags = { + '_dd.appsec.events.users.login.failure.sdk': 'true', + 'appsec.events.users.login.failure.usr.login': 'sdk_login', + 'appsec.events.users.login.failure.usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + '_dd.appsec.usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should write tags and call waf without user object when success is true', () => { + const results = trackLogin('passport-local', 'login', null, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf without user object when success is false', () => { + const results = trackLogin('passport-local', 'login', null, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.failure.usr.login': 'login' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'server.business_logic.users.login.failure': null + } + }) + }) + }) + + describe('when collectionMode is anonymization', () => { + beforeEach(() => { + setCollectionMode('anonymization') + }) + + it('should write tags and call waf when success is true', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf when success is false', () => { + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.failure.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'appsec.events.users.login.failure.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should not overwrite tags set by SDK when success is true', () => { + currentTags = { + '_dd.appsec.events.users.login.success.sdk': 'true', + 'appsec.events.users.login.success.usr.login': 'sdk_login', + 'usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should not overwwrite tags set by SDK when success is false', () => { + currentTags = { + '_dd.appsec.events.users.login.failure.sdk': 'true', + 'appsec.events.users.login.failure.usr.login': 'sdk_login', + 'appsec.events.users.login.failure.usr.id': 'sdk_id' + } + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.failure': null + } + }) + }) + + it('should write tags and call waf without user object when success is true', () => { + const results = trackLogin('passport-local', 'login', null, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should write tags and call waf without user object when success is false', () => { + const results = trackLogin('passport-local', 'login', null, false, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.failure.track': 'true', + '_dd.appsec.events.users.login.failure.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.failure.usr.login': 'anon_428821350e9691491f616b754cd8315f' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'server.business_logic.users.login.failure': null + } + }) + }) + }) + + describe('collectionMode aliases', () => { + it('should log warning and use anonymization mode when collectionMode is safe', () => { + setCollectionMode('safe') + + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Using deprecated value "safe" in config.appsec.eventTracking.mode' + ) + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should use anonymization mode when collectionMode is anon', () => { + setCollectionMode('anon') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'anonymization', + '_dd.appsec.usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'appsec.events.users.login.success.usr.login': 'anon_428821350e9691491f616b754cd8315f', + '_dd.appsec.usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'anon_428821350e9691491f616b754cd8315f', + 'usr.id': 'anon_a665a45920422f9d417e4867efdc4fb8', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should log warning and use identification mode when collectionMode is extended', () => { + setCollectionMode('extended') + + sinon.assert.calledOnceWithExactly( + log.warn, + '[ASM] Using deprecated value "extended" in config.appsec.eventTracking.mode' + ) + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should use identification mode when collectionMode is ident', () => { + setCollectionMode('ident') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, ['action1']) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + + sinon.assert.calledOnceWithExactly(keepTrace, rootSpan, SAMPLING_MECHANISM_APPSEC) + sinon.assert.calledOnceWithExactly(standalone.sample, rootSpan) + sinon.assert.calledOnceWithExactly(rootSpan.addTags, { + 'appsec.events.users.login.success.track': 'true', + '_dd.appsec.events.users.login.success.auto.mode': 'identification', + '_dd.appsec.usr.login': 'login', + 'appsec.events.users.login.success.usr.login': 'login', + '_dd.appsec.usr.id': '123', + 'usr.id': '123' + }) + sinon.assert.calledOnceWithExactly(waf.run, { + persistent: { + 'usr.login': 'login', + 'usr.id': '123', + 'server.business_logic.users.login.success': null + } + }) + }) + + it('should use disabled mode when collectionMode is not recognized', () => { + setCollectionMode('saperlipopette') + + const results = trackLogin('passport-local', 'login', { id: '123', email: 'a@b.c' }, true, rootSpan) + + assert.deepStrictEqual(results, undefined) + + sinon.assert.notCalled(log.error) + sinon.assert.notCalled(telemetry.incrementMissingUserLoginMetric) + sinon.assert.notCalled(keepTrace) + sinon.assert.notCalled(standalone.sample) + sinon.assert.notCalled(rootSpan.addTags) + sinon.assert.notCalled(waf.run) + }) + }) + }) +}) diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 1eb711dbd2c..32afdf7c8f7 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -170,7 +170,7 @@ describe('Config', () => { it('should correctly map OTEL_RESOURCE_ATTRIBUTES', () => { process.env.OTEL_RESOURCE_ATTRIBUTES = - 'deployment.environment=test1,service.name=test2,service.version=5,foo=bar1,baz=qux1' + 'deployment.environment=test1,service.name=test2,service.version=5,foo=bar1,baz=qux1' const config = new Config() expect(config).to.have.property('env', 'test1') @@ -259,8 +259,7 @@ describe('Config', () => { expect(config).to.have.nested.property('appsec.blockedTemplateHtml', undefined) expect(config).to.have.nested.property('appsec.blockedTemplateJson', undefined) expect(config).to.have.nested.property('appsec.blockedTemplateGraphql', undefined) - expect(config).to.have.nested.property('appsec.eventTracking.enabled', true) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'safe') + expect(config).to.have.nested.property('appsec.eventTracking.mode', 'identification') expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) expect(config).to.have.nested.property('appsec.apiSecurity.sampleDelay', 30) expect(config).to.have.nested.property('appsec.sca.enabled', null) @@ -285,6 +284,7 @@ describe('Config', () => { { name: 'appsec.blockedTemplateHtml', value: undefined, origin: 'default' }, { name: 'appsec.blockedTemplateJson', value: undefined, origin: 'default' }, { name: 'appsec.enabled', value: undefined, origin: 'default' }, + { name: 'appsec.eventTracking.mode', value: 'identification', origin: 'default' }, { name: 'appsec.obfuscatorKeyRegex', // eslint-disable-next-line @stylistic/js/max-len @@ -603,7 +603,6 @@ describe('Config', () => { expect(config).to.have.nested.property('appsec.blockedTemplateHtml', BLOCKED_TEMPLATE_HTML) expect(config).to.have.nested.property('appsec.blockedTemplateJson', BLOCKED_TEMPLATE_JSON) expect(config).to.have.nested.property('appsec.blockedTemplateGraphql', BLOCKED_TEMPLATE_GRAPHQL) - expect(config).to.have.nested.property('appsec.eventTracking.enabled', true) expect(config).to.have.nested.property('appsec.eventTracking.mode', 'extended') expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) expect(config).to.have.nested.property('appsec.apiSecurity.sampleDelay', 25) @@ -635,6 +634,7 @@ describe('Config', () => { { name: 'appsec.blockedTemplateHtml', value: BLOCKED_TEMPLATE_HTML_PATH, origin: 'env_var' }, { name: 'appsec.blockedTemplateJson', value: BLOCKED_TEMPLATE_JSON_PATH, origin: 'env_var' }, { name: 'appsec.enabled', value: true, origin: 'env_var' }, + { name: 'appsec.eventTracking.mode', value: 'extended', origin: 'env_var' }, { name: 'appsec.obfuscatorKeyRegex', value: '.*', origin: 'env_var' }, { name: 'appsec.obfuscatorValueRegex', value: '.*', origin: 'env_var' }, { name: 'appsec.rateLimit', value: '42', origin: 'env_var' }, @@ -773,6 +773,15 @@ describe('Config', () => { expect(config).to.have.nested.deep.property('crashtracking.enabled', false) }) + it('should prioritize DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE over DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING', () => { + process.env.DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE = 'anonymous' + process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = 'extended' + + const config = new Config() + + expect(config).to.have.nested.property('appsec.eventTracking.mode', 'anonymous') + }) + it('should initialize from the options', () => { const logger = {} const tags = { @@ -1187,6 +1196,7 @@ describe('Config', () => { process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML = BLOCKED_TEMPLATE_JSON_PATH // note the inversion between process.env.DD_APPSEC_HTTP_BLOCKED_TEMPLATE_JSON = BLOCKED_TEMPLATE_HTML_PATH // json and html here process.env.DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON = BLOCKED_TEMPLATE_JSON_PATH // json and html here + process.env.DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE = 'disabled' process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING = 'disabled' process.env.DD_API_SECURITY_ENABLED = 'false' process.env.DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS = 11 @@ -1251,7 +1261,7 @@ describe('Config', () => { blockedTemplateJson: BLOCKED_TEMPLATE_JSON_PATH, blockedTemplateGraphql: BLOCKED_TEMPLATE_GRAPHQL_PATH, eventTracking: { - mode: 'safe' + mode: 'anonymous' }, apiSecurity: { enabled: true @@ -1329,8 +1339,7 @@ describe('Config', () => { expect(config).to.have.nested.property('appsec.blockedTemplateHtml', BLOCKED_TEMPLATE_HTML) expect(config).to.have.nested.property('appsec.blockedTemplateJson', BLOCKED_TEMPLATE_JSON) expect(config).to.have.nested.property('appsec.blockedTemplateGraphql', BLOCKED_TEMPLATE_GRAPHQL) - expect(config).to.have.nested.property('appsec.eventTracking.enabled', true) - expect(config).to.have.nested.property('appsec.eventTracking.mode', 'safe') + expect(config).to.have.nested.property('appsec.eventTracking.mode', 'anonymous') expect(config).to.have.nested.property('appsec.apiSecurity.enabled', true) expect(config).to.have.nested.property('remoteConfig.pollInterval', 42) expect(config).to.have.nested.property('iast.enabled', true) @@ -1392,7 +1401,7 @@ describe('Config', () => { blockedTemplateJson: BLOCKED_TEMPLATE_JSON_PATH, blockedTemplateGraphql: BLOCKED_TEMPLATE_GRAPHQL_PATH, eventTracking: { - mode: 'safe' + mode: 'anonymous' }, apiSecurity: { enabled: false @@ -1427,7 +1436,6 @@ describe('Config', () => { blockedTemplateJson: undefined, blockedTemplateGraphql: undefined, eventTracking: { - enabled: false, mode: 'disabled' }, apiSecurity: { From e4d4cc3456c6f192082d3884010384c0d39b640c Mon Sep 17 00:00:00 2001 From: Bryan English Date: Mon, 16 Dec 2024 14:50:46 -0500 Subject: [PATCH 60/61] consolidate instances of `loadInst`, so code isn't repeated (#5020) --- .../dd-trace/test/setup/helpers/load-inst.js | 62 +++++++++++++++++++ packages/dd-trace/test/setup/mocha.js | 35 +---------- scripts/install_plugin_modules.js | 37 ++--------- scripts/verify-ci-config.js | 41 +----------- 4 files changed, 72 insertions(+), 103 deletions(-) create mode 100644 packages/dd-trace/test/setup/helpers/load-inst.js diff --git a/packages/dd-trace/test/setup/helpers/load-inst.js b/packages/dd-trace/test/setup/helpers/load-inst.js new file mode 100644 index 00000000000..91abd8baa77 --- /dev/null +++ b/packages/dd-trace/test/setup/helpers/load-inst.js @@ -0,0 +1,62 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const proxyquire = require('proxyquire') + +function loadInstFile (file, instrumentations) { + const instrument = { + addHook (instrumentation) { + instrumentations.push(instrumentation) + } + } + + const instPath = path.join(__dirname, `../../../../datadog-instrumentations/src/${file}`) + + proxyquire.noPreserveCache()(instPath, { + './helpers/instrument': instrument, + '../helpers/instrument': instrument + }) +} + +function loadOneInst (name) { + const instrumentations = [] + + try { + loadInstFile(`${name}/server.js`, instrumentations) + loadInstFile(`${name}/client.js`, instrumentations) + } catch (e) { + try { + loadInstFile(`${name}/main.js`, instrumentations) + } catch (e) { + loadInstFile(`${name}.js`, instrumentations) + } + } + + return instrumentations +} + +function getAllInstrumentations () { + const names = fs.readdirSync(path.join(__dirname, '../../../../', 'datadog-instrumentations', 'src')) + .filter(file => file.endsWith('.js')) + .map(file => file.slice(0, -3)) + + const instrumentations = names.reduce((acc, key) => { + const name = key + let instrumentations = loadOneInst(name) + + instrumentations = instrumentations.filter(i => i.versions) + if (instrumentations.length) { + acc[key] = instrumentations + } + + return acc + }, {}) + + return instrumentations +} + +module.exports = { + getInstrumentation: loadOneInst, + getAllInstrumentations +} diff --git a/packages/dd-trace/test/setup/mocha.js b/packages/dd-trace/test/setup/mocha.js index d3520c3fe1c..53a2c95897a 100644 --- a/packages/dd-trace/test/setup/mocha.js +++ b/packages/dd-trace/test/setup/mocha.js @@ -11,6 +11,7 @@ const agent = require('../plugins/agent') const Nomenclature = require('../../src/service-naming') const { storage } = require('../../../datadog-core') const { schemaDefinitions } = require('../../src/service-naming/schemas') +const { getInstrumentation } = require('./helpers/load-inst') global.withVersions = withVersions global.withExports = withExports @@ -19,38 +20,6 @@ global.withPeerService = withPeerService const testedPlugins = agent.testedPlugins -function loadInst (plugin) { - const instrumentations = [] - - try { - loadInstFile(`${plugin}/server.js`, instrumentations) - loadInstFile(`${plugin}/client.js`, instrumentations) - } catch (e) { - try { - loadInstFile(`${plugin}/main.js`, instrumentations) - } catch (e) { - loadInstFile(`${plugin}.js`, instrumentations) - } - } - - return instrumentations -} - -function loadInstFile (file, instrumentations) { - const instrument = { - addHook (instrumentation) { - instrumentations.push(instrumentation) - } - } - - const instPath = path.join(__dirname, `../../../datadog-instrumentations/src/${file}`) - - proxyquire.noPreserveCache()(instPath, { - './helpers/instrument': instrument, - '../helpers/instrument': instrument - }) -} - function withNamingSchema ( spanProducerFn, expected, @@ -174,7 +143,7 @@ function withPeerService (tracer, pluginName, spanGenerationFn, service, service } function withVersions (plugin, modules, range, cb) { - const instrumentations = typeof plugin === 'string' ? loadInst(plugin) : [].concat(plugin) + const instrumentations = typeof plugin === 'string' ? getInstrumentation(plugin) : [].concat(plugin) const names = instrumentations.map(instrumentation => instrumentation.name) modules = [].concat(modules) diff --git a/scripts/install_plugin_modules.js b/scripts/install_plugin_modules.js index 608fe71a992..212dc5928ed 100644 --- a/scripts/install_plugin_modules.js +++ b/scripts/install_plugin_modules.js @@ -5,10 +5,10 @@ const os = require('os') const path = require('path') const crypto = require('crypto') const semver = require('semver') -const proxyquire = require('proxyquire') const exec = require('./helpers/exec') const childProcess = require('child_process') const externals = require('../packages/dd-trace/test/plugins/externals') +const { getInstrumentation } = require('../packages/dd-trace/test/setup/helpers/load-inst') const requirePackageJsonPath = require.resolve('../packages/dd-trace/src/require-package-json') @@ -47,19 +47,7 @@ async function run () { async function assertVersions () { const internals = names - .map(key => { - const instrumentations = [] - const name = key - - try { - loadInstFile(`${name}/server.js`, instrumentations) - loadInstFile(`${name}/client.js`, instrumentations) - } catch (e) { - loadInstFile(`${name}.js`, instrumentations) - } - - return instrumentations - }) + .map(getInstrumentation) .reduce((prev, next) => prev.concat(next), []) for (const inst of internals) { @@ -117,10 +105,10 @@ function assertFolder (name, version) { } } -async function assertPackage (name, version, dependency, external) { - const dependencies = { [name]: dependency } +async function assertPackage (name, version, dependencyVersionRange, external) { + const dependencies = { [name]: dependencyVersionRange } if (deps[name]) { - await addDependencies(dependencies, name, dependency) + await addDependencies(dependencies, name, dependencyVersionRange) } const pkg = { name: [name, sha1(name).substr(0, 8), sha1(version)].filter(val => val).join('-'), @@ -240,18 +228,3 @@ function sha1 (str) { shasum.update(str) return shasum.digest('hex') } - -function loadInstFile (file, instrumentations) { - const instrument = { - addHook (instrumentation) { - instrumentations.push(instrumentation) - } - } - - const instPath = path.join(__dirname, `../packages/datadog-instrumentations/src/${file}`) - - proxyquire.noPreserveCache()(instPath, { - './helpers/instrument': instrument, - '../helpers/instrument': instrument - }) -} diff --git a/scripts/verify-ci-config.js b/scripts/verify-ci-config.js index 7a917132688..2e16ac0f7c3 100644 --- a/scripts/verify-ci-config.js +++ b/scripts/verify-ci-config.js @@ -4,39 +4,19 @@ const fs = require('fs') const path = require('path') const util = require('util') -const proxyquire = require('proxyquire') const yaml = require('yaml') const semver = require('semver') const { execSync } = require('child_process') const Module = require('module') +const { getAllInstrumentations } = require('../packages/dd-trace/test/setup/helpers/load-inst') + if (!Module.isBuiltin) { Module.isBuiltin = mod => Module.builtinModules.includes(mod) } const nodeMajor = Number(process.versions.node.split('.')[0]) -const names = fs.readdirSync(path.join(__dirname, '..', 'packages', 'datadog-instrumentations', 'src')) - .filter(file => file.endsWith('.js')) - .map(file => file.slice(0, -3)) - -const instrumentations = names.reduce((acc, key) => { - let instrumentations = [] - const name = key - - try { - loadInstFile(`${name}/server.js`, instrumentations) - loadInstFile(`${name}/client.js`, instrumentations) - } catch (e) { - loadInstFile(`${name}.js`, instrumentations) - } - - instrumentations = instrumentations.filter(i => i.versions) - if (instrumentations.length) { - acc[key] = instrumentations - } - - return acc -}, {}) +const instrumentations = getAllInstrumentations() const versions = {} @@ -84,21 +64,6 @@ Note that versions may be dependent on Node.js version. This is Node.js v${color } } -function loadInstFile (file, instrumentations) { - const instrument = { - addHook (instrumentation) { - instrumentations.push(instrumentation) - } - } - - const instPath = path.join(__dirname, `../packages/datadog-instrumentations/src/${file}`) - - proxyquire.noPreserveCache()(instPath, { - './helpers/instrument': instrument, - '../helpers/instrument': instrument - }) -} - function getRangesFromYaml (job) { // eslint-disable-next-line no-template-curly-in-string if (job.env && job.env.PACKAGE_VERSION_RANGE && job.env.PACKAGE_VERSION_RANGE !== '${{ matrix.range }}') { From a17c93f64f44e189e2d0eac45dbf1c9fcf00b747 Mon Sep 17 00:00:00 2001 From: Thomas Hunter II Date: Tue, 17 Dec 2024 00:40:28 -0800 Subject: [PATCH 61/61] repo: mandatory issue templates (#5023) --- .github/ISSUE_TEMPLATE/bug_report.yaml | 64 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 11 ++-- .github/ISSUE_TEMPLATE/feature_request.yaml | 50 ++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..8fb53ba14fa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,64 @@ +name: "Bug Report (Low Priority)" +description: "Create a public Bug Report. Note that these may not be addressed as quickly as the helpdesk and that looking up account information will be difficult." +title: "[BUG]: " +labels: bug +body: + - type: input + attributes: + label: Tracer Version(s) + description: "Version(s) of the tracer affected by this bug" + placeholder: 1.2.3, 4.5.6 + validations: + required: true + + - type: input + attributes: + label: Node.js Version(s) + description: "Version(s) of Node.js (`node --version`) that you've encountered this bug with" + placeholder: 20.1.1 + validations: + required: true + + - type: textarea + attributes: + label: Bug Report + description: Please add a clear and concise description of the bug here + validations: + required: true + + - type: textarea + attributes: + label: Reproduction Code + description: Please add code here to help us reproduce the problem + validations: + required: false + + - type: textarea + attributes: + label: Error Logs + description: "Please provide any error logs from the tracer (`DD_TRACE_DEBUG=true` can help)" + validations: + required: false + + - type: input + attributes: + label: Operating System + description: "Provide your operating system and version (e.g. `uname -a`)" + placeholder: Darwin Kernel Version 23.6.0 + validations: + required: false + + - type: dropdown + attributes: + label: Bundling + description: "How is your application being bundled" + options: + - Unsure + - No Bundling + - ESBuild + - Webpack + - Next.js + - Vite + - Rollup + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b5a5eb1d199..5f822733ea5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,9 +1,8 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - - name: Bug Report + - name: Bug Report (High Priority) url: https://help.datadoghq.com/hc/en-us/requests/new?tf_1260824651490=pt_product_type:apm&tf_1900004146284=pt_apm_language:node - about: This option creates an expedited Bug Report via the helpdesk (no login required). This will allow us to look up your account and allows you to provide additional information in private. Please do not create a GitHub issue to report a bug. - - name: Feature Request + about: Create an expedited Bug Report via the helpdesk (no login required). This will allow us to look up your account and allows you to provide additional information in private. Please do not create a GitHub issue to report a bug. + - name: Feature Request (High Priority) url: https://help.datadoghq.com/hc/en-us/requests/new?tf_1260824651490=pt_product_type:apm&tf_1900004146284=pt_apm_language:node&tf_1260825272270=pt_apm_category_feature_request - about: This option creates an expedited Feature Request via the helpdesk (no login required). This helps with prioritization and allows you to provide additional information in private. Please do not create a GitHub issue to request a feature. - + about: Create an expedited Feature Request via the helpdesk (no login required). This helps with prioritization and allows you to provide additional information in private. Please do not create a GitHub issue to request a feature. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000000..9d26ea1dd33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,50 @@ +name: Feature Request (Low Priority) +description: Create a public Feature Request. Note that these may not be addressed as quickly as the helpdesk and that looking up account information will be difficult. +title: "[FEATURE]: " +labels: feature-request +body: + - type: input + attributes: + label: Package Name + description: "If your feature request is to add instrumentation support for an npm package please provide the name here" + placeholder: left-pad + validations: + required: false + + - type: input + attributes: + label: Package Version(s) + description: "If your feature request is to add instrumentation support for an npm package please provide the version you use" + placeholder: 1.2.3 + validations: + required: false + + - type: textarea + attributes: + label: Describe the feature you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Is your feature request related to a problem? + description: | + Please add a clear and concise description of your problem. + E.g. I'm unable to instrument my database queries... + validations: + required: false + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here + validations: + required: false