From 5925132bb26bd26ce632f9c5e8dab7e806cddd7f Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 27 Jul 2026 21:51:49 +0200 Subject: [PATCH 01/43] chore(eslint): update eslint-plugin-unicorn to 72 (#9503) Activate Node.js 18 compatible rules that remove redundant syntax and catch correctness issues. 1. Skip esbuild resolution for built-in modules without package paths. 2. Contain detached Cucumber finalizer rejections without changing the returned test promise. 3. Preserve datastream varint continuation bits at 0x80 boundaries. 4. Avoid buffering Electron traces when the trace channel has no subscribers. 5. Replace regex-based HTTP path splitting with a shared delimiter scan, reducing representative cost from 41.4 ns to 14.7 ns per call. --- .../generated-files.js | 2 +- .../generated-test-contract.js | 4 +- .../manifest-scaffold.js | 3 +- .../runner-command.js | 2 +- .../scenarios/basic-reporting.js | 2 +- .../scenarios/test-management.js | 4 +- eslint.config.mjs | 108 ++++++-------- package.json | 2 +- packages/datadog-esbuild/index.js | 6 +- packages/datadog-esbuild/src/utils.js | 2 +- packages/datadog-esbuild/test/plugin.spec.js | 30 ++++ .../src/claude-agent-sdk.js | 8 +- .../datadog-instrumentations/src/cucumber.js | 11 +- .../extract-package-and-module-path.js | 10 ++ .../src/helpers/register.js | 8 +- .../src/helpers/rewriter/index.js | 1 + .../src/helpers/router-helper.js | 2 +- .../src/http/client.js | 4 +- packages/datadog-instrumentations/src/jest.js | 22 ++- .../src/mocha/common.js | 4 +- .../src/moleculer/client.js | 2 +- .../src/playwright.js | 2 +- packages/datadog-instrumentations/src/rhea.js | 2 +- .../test/helpers/register.spec.js | 15 ++ .../datadog-plugin-aerospike/src/index.js | 4 +- packages/datadog-plugin-amqplib/src/client.js | 2 +- .../datadog-plugin-amqplib/src/producer.js | 2 +- .../src/services/eventbridge.js | 4 +- .../src/services/stepfunctions.js | 4 +- .../src/producer.js | 4 +- .../src/producer.js | 8 +- .../datadog-plugin-bullmq/src/producer.js | 6 +- .../datadog-plugin-child_process/src/index.js | 6 +- .../src/scrub-cmd-params.js | 11 +- .../datadog-plugin-fastify/src/tracing.js | 2 +- packages/datadog-plugin-fs/src/index.js | 4 - .../datadog-plugin-graphql/src/execute.js | 1 + .../src/tools/signature.js | 2 +- packages/datadog-plugin-http/src/client.js | 3 +- packages/datadog-plugin-jest/src/index.js | 5 +- .../datadog-plugin-langchain/src/tokens.js | 4 +- packages/datadog-plugin-prisma/src/index.js | 6 +- packages/datadog-plugin-rhea/src/producer.js | 2 +- packages/datadog-plugin-undici/src/index.js | 3 +- packages/datadog-webpack/index.js | 4 +- .../src/appsec/downstream_requests.js | 2 +- .../iast/analyzers/hardcoded-base-analyzer.js | 2 +- .../analyzers/hardcoded-password-analyzer.js | 2 +- .../iast/analyzers/sql-injection-analyzer.js | 2 +- .../taint-tracking/taint-tracking-impl.js | 4 +- .../iast/vulnerabilities-formatter/utils.js | 2 +- .../src/appsec/iast/vulnerability-reporter.js | 4 +- .../dd-trace/src/appsec/sdk/track_event.js | 2 +- .../dd-trace/src/appsec/waf/waf_manager.js | 2 +- .../exporters/ci-visibility-exporter.js | 4 +- .../exporters/git/git_metadata.js | 3 +- .../src/ci-visibility/requests/request.js | 6 +- packages/dd-trace/src/datastreams/encoding.js | 7 +- .../debugger/devtools_client/probe_sampler.js | 5 +- .../src/debugger/devtools_client/send.js | 3 +- .../devtools_client/snapshot/index.js | 4 +- .../devtools_client/snapshot/redaction.js | 18 ++- .../dd-trace/src/encode/tags-processors.js | 4 +- packages/dd-trace/src/exporters/common/url.js | 8 +- .../dd-trace/src/exporters/electron/index.js | 5 +- .../dd-trace/src/external-logger/src/index.js | 3 +- packages/dd-trace/src/flare/index.js | 4 +- packages/dd-trace/src/id.js | 2 +- .../src/llmobs/plugins/anthropic/index.js | 2 +- .../src/llmobs/plugins/genai/index.js | 2 +- .../plugins/langchain/handlers/index.js | 2 +- packages/dd-trace/src/llmobs/sdk.js | 6 +- packages/dd-trace/src/llmobs/tagger.js | 3 +- packages/dd-trace/src/noop/proxy.js | 4 +- .../opentelemetry/metrics/otlp_transformer.js | 2 +- .../metrics/periodic_metric_reader.js | 3 +- packages/dd-trace/src/opentracing/span.js | 2 +- .../dd-trace/src/opentracing/span_context.js | 2 +- packages/dd-trace/src/plugins/plugin.js | 2 +- packages/dd-trace/src/plugins/tracing.js | 1 + packages/dd-trace/src/plugins/util/ci.js | 4 +- .../src/plugins/util/http-otel-semantics.js | 2 +- packages/dd-trace/src/plugins/util/test.js | 13 +- packages/dd-trace/src/priority_sampler.js | 9 +- .../src/profiling/profilers/poisson.js | 2 +- .../src/profiling/profilers/shared.js | 4 +- packages/dd-trace/src/util.js | 16 +++ .../test/datastreams/encoding.spec.js | 35 +++++ .../test/exporters/electron/exporter.spec.js | 3 +- .../dd-trace/test/plugins/util/test.spec.js | 13 ++ packages/dd-trace/test/util.spec.js | 29 +++- scripts/agentless-stress-test.js | 6 +- scripts/generate-config-types.js | 2 +- scripts/release/changelog.spec.js | 2 +- scripts/release/proposal.js | 3 +- scripts/verify-exercised-tests.js | 1 + scripts/verify-workflow-job-names.js | 4 +- yarn.lock | 136 +++++++++++++++--- 98 files changed, 497 insertions(+), 259 deletions(-) diff --git a/ci/test-optimization-validation/generated-files.js b/ci/test-optimization-validation/generated-files.js index 1433416e8f..a8d0e5af6b 100644 --- a/ci/test-optimization-validation/generated-files.js +++ b/ci/test-optimization-validation/generated-files.js @@ -132,7 +132,7 @@ function getMissingDirectories (root, directory) { function cleanupCreatedDirectories (root) { const outcome = { removed: 0, retained: 0 } const resolvedRoot = path.resolve(root) - const directories = [...createdGeneratedDirectories.entries()] + const directories = [...createdGeneratedDirectories] .filter(([directory]) => isPathInside(resolvedRoot, directory)) .sort(([left], [right]) => right.length - left.length) diff --git a/ci/test-optimization-validation/generated-test-contract.js b/ci/test-optimization-validation/generated-test-contract.js index 8db9b1400c..da6fcaf405 100644 --- a/ci/test-optimization-validation/generated-test-contract.js +++ b/ci/test-optimization-validation/generated-test-contract.js @@ -192,9 +192,7 @@ function getGeneratedTestContractError (framework) { if (!cleanupPaths.has(path.normalize(stepsFile))) { return 'must include the isolated Cucumber step definitions in cleanupPaths.' } - } - - if (framework.framework === 'playwright') { + } else if (framework.framework === 'playwright') { const configPath = playwrightAdapter.getGeneratedConfigPath(strategy.testDirectory) const config = files.find(file => path.normalize(file.path) === path.normalize(configPath)) if (config?.contentLines?.join('\n') !== playwrightAdapter.getGeneratedConfigContent()) { diff --git a/ci/test-optimization-validation/manifest-scaffold.js b/ci/test-optimization-validation/manifest-scaffold.js index 272f6b7ac8..33cbdc4aa3 100644 --- a/ci/test-optimization-validation/manifest-scaffold.js +++ b/ci/test-optimization-validation/manifest-scaffold.js @@ -477,8 +477,7 @@ function buildGeneratedTestStrategy ({ framework, projectRoot, representative }) const stepsFile = cucumber.getGeneratedStepsPath(testDirectory) files.push({ path: stepsFile, contentLines: cucumber.getGeneratedStepsContent().split('\n') }) cleanupPaths.push(stepsFile) - } - if (framework === 'playwright') { + } else if (framework === 'playwright') { const configFile = playwright.getGeneratedConfigPath(testDirectory) files.push({ path: configFile, contentLines: playwright.getGeneratedConfigContent().split('\n') }) cleanupPaths.push(configFile) diff --git a/ci/test-optimization-validation/runner-command.js b/ci/test-optimization-validation/runner-command.js index 55517766c0..5e29341762 100644 --- a/ci/test-optimization-validation/runner-command.js +++ b/ci/test-optimization-validation/runner-command.js @@ -211,7 +211,7 @@ function getRunnerArgs (framework, testFile, generated) { */ function getRunnerEnv (framework) { return { - ...(framework.validation.environment || {}), + ...framework.validation.environment, ...(framework.framework === 'cucumber' ? { CUCUMBER_PUBLISH_QUIET: 'true' } : {}), } } diff --git a/ci/test-optimization-validation/scenarios/basic-reporting.js b/ci/test-optimization-validation/scenarios/basic-reporting.js index fa19cfccf3..fabe2a6566 100644 --- a/ci/test-optimization-validation/scenarios/basic-reporting.js +++ b/ci/test-optimization-validation/scenarios/basic-reporting.js @@ -58,7 +58,7 @@ async function runBasicReporting ({ framework, out, options }) { selector, settingsLoadedFromCache: run.offline.inputs.settings?.status === 'loaded', } - evidence.foundationalReportingEstablished = evidence.foundationalReportingEstablished && selector.verified + evidence.foundationalReportingEstablished &&= selector.verified if (run.result.timedOut) { return inconclusive( diff --git a/ci/test-optimization-validation/scenarios/test-management.js b/ci/test-optimization-validation/scenarios/test-management.js index d2bdde91d8..8c98ee8452 100644 --- a/ci/test-optimization-validation/scenarios/test-management.js +++ b/ci/test-optimization-validation/scenarios/test-management.js @@ -149,7 +149,7 @@ function buildQuarantinedResponse (framework, scenario, discoveredIdentities = [ for (const identity of identities) { for (const suite of getSuiteCandidates(identity, scenario)) { for (const name of getNameCandidates(identity)) { - suites[suite] = suites[suite] || { tests: {} } + suites[suite] ||= { tests: {} } suites[suite].tests[name] = { properties: { quarantined: true, @@ -210,7 +210,7 @@ function summarizeManagedTests (testManagementTests) { } summary.set(displaySuite, testNames) } - return [...summary.entries()].slice(0, 5).map(([suite, tests]) => ({ + return [...summary].slice(0, 5).map(([suite, tests]) => ({ suite, tests: [...tests].slice(0, 5), })) diff --git a/eslint.config.mjs b/eslint.config.mjs index 4a869ca1b0..af77ab3c79 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -586,60 +586,57 @@ export default [ ...eslintPluginUnicorn.configs.recommended.rules, // Overriding recommended unicorn rules. - // Rules not listed here are left at the `recommended` default. The v65→v68 bump - // turned ~130 rules on in `recommended`; the entries below are the ones that fire - // on our source and are intentionally kept off (counts are from the v68 run). - 'unicorn/catch-error-name': ['off', { name: 'err' }], // Many errors + // Rules not listed here are left at the `recommended` default. The entries below + // document deliberate exceptions. Volume markers stay coarse so they do not drift: + // `few` is under ten sites, `many` is tens, `lots` is hundreds or more. + 'unicorn/catch-error-name': ['off', { name: 'err' }], // lots 'unicorn/expiring-todo-comments': 'off', - 'unicorn/filename-case': ['off', { case: 'kebabCase' }], // Many errors - 'unicorn/name-replacements': 'off', // Many errors | naming churn (split out of prevent-abbreviations) - 'unicorn/prevent-abbreviations': 'off', // Many errors + 'unicorn/filename-case': ['off', { case: 'kebabCase' }], // lots + 'unicorn/name-replacements': 'off', // lots | naming churn (split out of prevent-abbreviations) + 'unicorn/prevent-abbreviations': 'off', // Its replacements moved to name-replacements // These rules require a newer Node.js version than we support 'unicorn/no-array-reverse': 'off', // Node.js 20 'unicorn/no-array-sort': 'off', // Node.js 20 + 'unicorn/prefer-abort-signal-any': 'off', // Node.js 18.17 'unicorn/prefer-dispose': 'off', // Explicit resource management (newer Node.js) + 'unicorn/prefer-group-by': 'off', // Node.js 21 + 'unicorn/prefer-iterator-helpers': 'off', // Iterator helpers (Node.js 22) 'unicorn/prefer-iterator-to-array': 'off', // Iterator helpers (Node.js 22) 'unicorn/prefer-iterator-to-array-at-end': 'off', // Iterator helpers (Node.js 22) - 'unicorn/prefer-promise-with-resolvers': 'off', // 6 errors | Promise.withResolvers (Node.js 22) + 'unicorn/prefer-promise-try': 'off', // Promise.try (Node.js 24) + 'unicorn/prefer-promise-with-resolvers': 'off', // few | Promise.withResolvers (Node.js 22) + 'unicorn/prefer-set-methods': 'off', // Set methods (Node.js 22) 'unicorn/prefer-temporal': 'off', // Temporal is not stable on supported Node.js 'unicorn/prefer-uint8array-base64': 'off', // Uint8Array base64 (Node.js 22) // These rules could potentially be evaluated again at a much later point - 'unicorn/class-reference-in-static-methods': 'off', // 6 errors - 'unicorn/consistent-class-member-order': 'off', // 55 errors | ordering churn - 'unicorn/consistent-conditional-object-spread': 'off', // 3 errors - 'unicorn/consistent-optional-chaining': 'off', // 3 errors + 'unicorn/class-reference-in-static-methods': 'off', // few + 'unicorn/consistent-class-member-order': 'off', // many | ordering churn + 'unicorn/consistent-conditional-object-spread': 'off', // few 'unicorn/explicit-length-check': 'off', // Not a big advantage 'unicorn/explicit-timer-delay': 'off', // Covered by our own timer lint rules 'unicorn/no-array-callback-reference': 'off', - 'unicorn/no-computed-property-existence-check': 'off', // 160 errors | needs an audit - 'unicorn/no-declarations-before-early-exit': 'off', // 62 errors - 'unicorn/no-duplicate-if-branches': 'off', // 1 error | may surface real bugs - 'unicorn/no-duplicate-logical-operands': 'off', // 2 errors | may surface real bugs - 'unicorn/no-error-property-assignment': 'off', // 6 errors + 'unicorn/no-computed-property-existence-check': 'off', // lots | needs an audit + 'unicorn/no-declarations-before-early-exit': 'off', // many + 'unicorn/no-error-property-assignment': 'off', // few | all preserve upstream error metadata 'unicorn/no-for-loop': 'off', // Activate if this is resolved https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2664 - 'unicorn/no-incorrect-template-string-interpolation': 'off', // 3 errors | audit for real bugs first - 'unicorn/no-invalid-argument-count': 'off', // 98 errors | high false-positive risk, worth a focused pass - 'unicorn/no-loop-iterable-mutation': 'off', // 2 errors | may surface real bugs - 'unicorn/no-nonstandard-builtin-properties': 'off', // 34 errors | needs an audit + 'unicorn/no-nonstandard-builtin-properties': 'off', // many | needs an audit 'unicorn/no-this-assignment': 'off', // This would need some further refactoring and the benefit is small - 'unicorn/no-undeclared-class-members': 'off', // 272 errors | requires declaring every field - 'unicorn/no-unreadable-array-destructuring': 'off', // 4 errors | not autofixable, needs manual rewrite - 'unicorn/no-unreadable-for-of-expression': 'off', // 32 errors - 'unicorn/no-unreadable-object-destructuring': 'off', // 57 errors - 'unicorn/no-unsafe-string-replacement': 'off', // 6 errors - 'unicorn/no-useless-recursion': 'off', // 2 errors + 'unicorn/no-undeclared-class-members': 'off', // lots | requires declaring every field + 'unicorn/no-unreadable-array-destructuring': 'off', // few | not autofixable, needs manual rewrite + 'unicorn/no-unreadable-for-of-expression': 'off', // many + 'unicorn/no-unreadable-object-destructuring': 'off', // many + 'unicorn/no-unsafe-string-replacement': 'off', // many | replacement callbacks reduce readability + 'unicorn/no-useless-recursion': 'off', // few | iterative rewrites add substantial nesting 'unicorn/prefer-code-point': 'off', // Should be activated, but needs a refactor of some code - 'unicorn/prefer-early-return': 'off', // 67 errors | tension with our positive-`if` style - 'unicorn/prefer-hoisting-branch-code': 'off', // 2 errors | reshapes branch bodies - 'unicorn/prefer-minimal-ternary': 'off', // 24 errors - 'unicorn/prefer-number-is-safe-integer': 'off', // 17 errors - 'unicorn/prefer-object-iterable-methods': 'off', // 56 errors + 'unicorn/prefer-early-return': 'off', // many | tension with our positive-`if` style + 'unicorn/prefer-number-is-safe-integer': 'off', // many + 'unicorn/prefer-object-iterable-methods': 'off', // many 'unicorn/prefer-queue-microtask': 'off', // process.nextTick semantics differ - 'unicorn/prefer-smaller-scope': 'off', // 3 errors - 'unicorn/prefer-split-limit': 'off', // 23 errors - 'unicorn/require-array-sort-compare': 'off', // 8 errors | may surface real default-sort bugs + 'unicorn/prefer-simple-condition-first': 'off', // lots | needs a short-circuit behavior audit + 'unicorn/prefer-then-catch': 'off', // many | broadens rejection boundaries + 'unicorn/require-array-sort-compare': 'off', // many | many intentional lexicographic sorts // The following rules should not be activated! 'unicorn/consistent-boolean-name': 'off', // Would rename public API and config booleans @@ -649,6 +646,8 @@ export default [ 'unicorn/no-array-splice': 'off', // toSpliced copies the whole array (perf) 'unicorn/no-break-in-nested-loop': 'off', // Conflicts with our performance-oriented loops 'unicorn/no-global-object-property-assignment': 'off', // We use globalThis[Symbol.for('dd-trace')] + 'unicorn/no-negated-array-predicate': 'off', // Predicate inversion is harder to read and creates churn + 'unicorn/no-negated-comparison': 'off', // Opposite comparisons do not preserve NaN handling 'unicorn/no-nested-ternary': 'off', // Not really an issue in the code and the benefit is small 'unicorn/no-new-array': 'off', // new Array is often used for performance reasons 'unicorn/no-null': 'off', // We do not control external APIs and it is hard to differentiate these @@ -659,47 +658,32 @@ export default [ 'unicorn/operator-assignment': 'off', // Covered by core operator-assignment 'unicorn/prefer-array-last-methods': 'off', // Questionable benefit 'unicorn/prefer-await': 'off', // We avoid async/await in production hot paths + 'unicorn/prefer-dom-node-html-methods': 'off', // Browser compatibility and different serialization semantics 'unicorn/prefer-event-target': 'off', // Benefit only outside of Node.js 'unicorn/prefer-global-this': 'off', // Questionable benefit in Node.js alone 'unicorn/prefer-includes-over-repeated-comparisons': 'off', // Bad for performance 'unicorn/prefer-math-trunc': 'off', // Math.trunc is not a 1-to-1 replacement for most of our usage + 'unicorn/prefer-minimal-ternary': 'off', // Conflicts with our restricted-syntax rule on require(cond ? a : b) 'unicorn/prefer-module': 'off', // We use CJS 'unicorn/prefer-node-protocol': 'off', // May not be used due to guardrails 'unicorn/prefer-number-coercion': 'off', // Number() is not a 1-to-1 replacement for parseInt/parseFloat 'unicorn/prefer-private-class-fields': 'off', // Many `_underscore` fields cross module boundaries - 'unicorn/prefer-reflect-apply': 'off', // Questionable benefit and more than 500 matches + 'unicorn/prefer-reflect-apply': 'off', // lots | questionable benefit 'unicorn/prefer-short-arrow-method': 'off', // Method shorthand is intentional; arrow properties change `this` + 'unicorn/prefer-split-limit': 'off', // A limit is slower than getSegment; the rest read every segment 'unicorn/prefer-switch': 'off', // Questionable benefit 'unicorn/prefer-top-level-await': 'off', // Only useful when using ESM 'unicorn/prefer-unicode-code-point-escapes': 'off', // Replaces the dropped no-hex-escape; questionable benefit 'unicorn/switch-case-braces': 'off', // Questionable benefit - // Safe to enable in a follow-up: autofixable and aligned with our style. Kept off - // here only to keep this version bump free of source churn (counts from the v68 run). - 'unicorn/logical-assignment-operators': 'off', // 42 errors | matches our ??=/||= usage - 'unicorn/no-confusing-array-splice': 'off', // 1 error - 'unicorn/no-for-each': 'off', // 10 errors | we already prefer for-of in production - 'unicorn/no-negated-array-predicate': 'off', // 2 errors - 'unicorn/no-negated-comparison': 'off', // 1 error - 'unicorn/no-subtraction-comparison': 'off', // 2 errors - 'unicorn/no-unnecessary-boolean-comparison': 'off', // 6 errors - 'unicorn/no-unnecessary-global-this': 'off', // 4 errors - 'unicorn/no-unnecessary-splice': 'off', // 2 errors - 'unicorn/no-useless-concat': 'off', // 4 errors - 'unicorn/no-useless-continue': 'off', // 1 error - 'unicorn/no-useless-delete-check': 'off', // 1 error - 'unicorn/no-useless-fallback-in-spread': 'off', // 5 errors - 'unicorn/no-useless-override': 'off', // 1 error - 'unicorn/no-useless-template-literals': 'off', // 13 errors - 'unicorn/prefer-array-from-map': 'off', // 6 errors - 'unicorn/prefer-boolean-return': 'off', // 1 error - 'unicorn/prefer-continue': 'off', // 52 errors - 'unicorn/prefer-direct-iteration': 'off', // 5 errors - 'unicorn/prefer-else-if': 'off', // 6 errors - 'unicorn/prefer-global-number-constants': 'off', // 3 errors - 'unicorn/prefer-logical-operator-over-ternary': 'off', // 3 errors - 'unicorn/prefer-ternary': 'off', // 16 errors - 'unicorn/prefer-unary-minus': 'off', // 1 error + // These remaining rules need focused rewrites before activation. + 'unicorn/no-confusing-array-splice': 'off', // few + 'unicorn/no-for-each': 'off', // many | we already prefer for-of in production + 'unicorn/no-unnecessary-global-this': 'off', // few | explicit globals are clearer + 'unicorn/no-useless-continue': 'off', // few + 'unicorn/prefer-array-from-map': 'off', // few | loops avoid callback allocation + 'unicorn/prefer-continue': 'off', // many + 'unicorn/prefer-ternary': 'off', // many }, }, { diff --git a/package.json b/package.json index 80df4d6843..ba973aabe5 100644 --- a/package.json +++ b/package.json @@ -213,7 +213,7 @@ "eslint-plugin-n": "^18.2.1", "eslint-plugin-promise": "^7.3.0", "eslint-plugin-sonarjs": "^4.2.0", - "eslint-plugin-unicorn": "^68.0.0", + "eslint-plugin-unicorn": "^72.0.0", "express": "^5.1.0", "glob": "^10.4.5", "globals": "^17.7.0", diff --git a/packages/datadog-esbuild/index.js b/packages/datadog-esbuild/index.js index 675f6d5abc..e819ce7f70 100644 --- a/packages/datadog-esbuild/index.js +++ b/packages/datadog-esbuild/index.js @@ -265,10 +265,12 @@ ${build.initialOptions.banner.js}` } } // The file namespace is used when requiring files from disk in userland + if (extracted.pkg === null) return + let pathToPackageJson try { // we can't use require.resolve('pkg/package.json') as ESM modules don't make the file available - pathToPackageJson = require.resolve(`${extracted.pkg}`, { paths: [args.resolveDir] }) + pathToPackageJson = require.resolve(extracted.pkg, { paths: [args.resolveDir] }) pathToPackageJson = extractPackageAndModulePath(pathToPackageJson).pkgJson } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { @@ -340,7 +342,7 @@ ${build.initialOptions.banner.js}` if (data.isESM) { if (args.path.endsWith(ESM_INTERCEPTED_SUFFIX)) { - args.path = args.path.slice(0, -1 * ESM_INTERCEPTED_SUFFIX.length) + args.path = args.path.slice(0, -ESM_INTERCEPTED_SUFFIX.length) if (data.internal) { args.path = args.path.slice(INTERNAL_ESM_INTERCEPTED_PREFIX.length) diff --git a/packages/datadog-esbuild/src/utils.js b/packages/datadog-esbuild/src/utils.js index 9289e88e11..b61db16959 100644 --- a/packages/datadog-esbuild/src/utils.js +++ b/packages/datadog-esbuild/src/utils.js @@ -174,7 +174,7 @@ async function processModule ({ path, internal = false, context, excludeDefault for (const n of exportNames) { if (n === 'default' && excludeDefault) continue - if (isStarExportLine(n) === true) { + if (isStarExportLine(n)) { // export * from 'wherever' const [, modFile] = n.split('* from ') diff --git a/packages/datadog-esbuild/test/plugin.spec.js b/packages/datadog-esbuild/test/plugin.spec.js index cf3cb48a35..a405a407f8 100644 --- a/packages/datadog-esbuild/test/plugin.spec.js +++ b/packages/datadog-esbuild/test/plugin.spec.js @@ -17,7 +17,37 @@ function captureOptionalPeerOnLoad () { return onLoad } +function captureOnResolve () { + let onResolve + ddPlugin.setup({ + initialOptions: {}, + /** + * @param {object} options + * @param {Function} callback + */ + onResolve (options, callback) { + onResolve = callback + }, + onLoad () {}, + }) + return onResolve +} + describe('datadog-esbuild plugin', () => { + it('ignores builtins without a package path', () => { + const onResolve = captureOnResolve() + + const result = onResolve({ + path: 'fs', + resolveDir: process.cwd(), + kind: 'require-call', + namespace: 'file', + importer: '/app/index.js', + }) + + assert.strictEqual(result, undefined) + }) + describe('optional peer bundling', () => { it('rewrites the installed peer load in require-provider into a literal require', () => { const onLoad = captureOptionalPeerOnLoad() diff --git a/packages/datadog-instrumentations/src/claude-agent-sdk.js b/packages/datadog-instrumentations/src/claude-agent-sdk.js index b512c3794d..0b64f3112e 100644 --- a/packages/datadog-instrumentations/src/claude-agent-sdk.js +++ b/packages/datadog-instrumentations/src/claude-agent-sdk.js @@ -54,7 +54,7 @@ function buildTracerHooks (sessionCtx) { sessionCtx.cwd = input.cwd sessionCtx.transcriptPath = input.transcript_path sessionCtx.agentType = input.agent_type - sessionCtx.permissionMode = sessionCtx.permissionMode || input.permission_mode + sessionCtx.permissionMode ||= input.permission_mode return {} } @@ -64,8 +64,8 @@ function buildTracerHooks (sessionCtx) { } function onUserPromptSubmit (input) { - sessionCtx.sessionId = sessionCtx.sessionId || input.session_id - sessionCtx.prompt = sessionCtx.prompt || input.prompt + sessionCtx.sessionId ||= input.session_id + sessionCtx.prompt ||= input.prompt return {} } @@ -254,7 +254,7 @@ function createStreamLookup (chunks) { scanLocalLifecycle(chunks, startIndex, toolUseId, localLifecycle) if (localLifecycle.toolResultIndex !== undefined) return localLifecycle - streamIndex = streamIndex || buildStreamIndex(chunks) + streamIndex ||= buildStreamIndex(chunks) const indexedLifecycle = streamIndex.get(toolUseId) return { taskStartedChunk: localLifecycle.taskStartedChunk || indexedLifecycle?.taskStartedChunk, diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index e15928078d..0971c18e02 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -6,6 +6,7 @@ const { createCoverageMap } = require('../../../vendor/dist/istanbul-lib-coverag const shimmer = require('../../datadog-shimmer') const log = require('../../dd-trace/src/log') const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper') +const { getSegment } = require('../../dd-trace/src/util') const { getCoveredFilesFromCoverage, getExecutableFilesFromCoverage, @@ -342,7 +343,7 @@ function handleParallelTestCaseFinished (pickle, worstTestStepResult) { } const testFileAbsolutePath = pickle.uri - const finished = pickleResultByFile[testFileAbsolutePath] || (pickleResultByFile[testFileAbsolutePath] = []) + const finished = (pickleResultByFile[testFileAbsolutePath] ||= []) if (isEarlyFlakeDetectionEnabled && isNew) { const testFullname = `${pickle.uri}:${pickle.name}` @@ -555,8 +556,7 @@ function getErrorFromCucumberResult (cucumberResult) { return } - const [message] = cucumberResult.message.split('\n') - const error = new Error(message) + const error = new Error(getSegment(cucumberResult.message, '\n', 0)) if (cucumberResult.exception) { error.type = cucumberResult.exception.type } @@ -793,7 +793,7 @@ function wrapRun (pl, isLatestVersion, version) { testFnCh.runStores(ctx, () => { promise = run.apply(this, args) }) - promise.finally(async () => { + const finalize = async () => { if (!canAwaitRetries) { this.eventBroadcaster.removeListener('envelope', onEnvelope) } @@ -966,6 +966,9 @@ function wrapRun (pl, isLatestVersion, version) { ...attemptCtx.currentStore, finalStatus, }) + } + promise.then(finalize, finalize).catch(error => { + log.error('Cucumber test finalization error', error) }) return promise } catch (err) { diff --git a/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js b/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js index 0c79beebbf..b1b688560e 100644 --- a/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +++ b/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js @@ -2,11 +2,21 @@ const NM = 'node_modules/' +/** + * @typedef {object} PackageAndModulePath + * @property {string|null} pkg + * @property {string|null} path + * @property {string} [pkgJson] + */ + /** * For a given full path to a module, * return the package name it belongs to and the local path to the module * input: '/foo/node_modules/@co/stuff/foo/bar/baz.js' * output: { pkg: '@co/stuff', path: 'foo/bar/baz.js', pkgJson: '/foo/node_modules/@co/stuff/package.json' } + * + * @param {string} fullPath + * @returns {PackageAndModulePath} */ module.exports = function extractPackageAndModulePath (fullPath) { const nm = fullPath.lastIndexOf(NM) diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index bbf2a56526..db0b876674 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -57,19 +57,23 @@ const alreadyLoggedIncompatibleIntegrations = new Set() // Always disable prefixed and unprefixed node modules if one is disabled. if (disabledInstrumentations.size) { const builtinsSet = new Set(builtinModules) + const disabledBuiltinCounterparts = [] for (const name of disabledInstrumentations) { const hasPrefix = name.startsWith('node:') if (hasPrefix || builtinsSet.has(name)) { if (hasPrefix) { const unprefixedName = name.slice(5) if (!disabledInstrumentations.has(unprefixedName)) { - disabledInstrumentations.add(unprefixedName) + disabledBuiltinCounterparts.push(unprefixedName) } } else if (!disabledInstrumentations.has(`node:${name}`)) { - disabledInstrumentations.add(`node:${name}`) + disabledBuiltinCounterparts.push(`node:${name}`) } } } + for (const name of disabledBuiltinCounterparts) { + disabledInstrumentations.add(name) + } builtinsSet.clear() } diff --git a/packages/datadog-instrumentations/src/helpers/rewriter/index.js b/packages/datadog-instrumentations/src/helpers/rewriter/index.js index aab0046ccf..8cc736ff6f 100644 --- a/packages/datadog-instrumentations/src/helpers/rewriter/index.js +++ b/packages/datadog-instrumentations/src/helpers/rewriter/index.js @@ -39,6 +39,7 @@ for (const matcher of [matcherCjs, matcherEsm]) { // Keep the marker split: source-map scanners can read a contiguous token in // string literals as this file's own inline map. +// eslint-disable-next-line unicorn/no-useless-concat -- Keep the source-map marker non-contiguous. const SOURCE_MAP_PREFIX = '//# sourceMapping' + 'URL=data:application/json;base64,' function rewrite (content, filename, format) { diff --git a/packages/datadog-instrumentations/src/helpers/router-helper.js b/packages/datadog-instrumentations/src/helpers/router-helper.js index 173e4ebdbd..73e2d6781a 100644 --- a/packages/datadog-instrumentations/src/helpers/router-helper.js +++ b/packages/datadog-instrumentations/src/helpers/router-helper.js @@ -88,7 +88,7 @@ function collectRoutesFromRouter (router, prefix) { function normalizeRoutePaths (path) { if (path == null) return [] - if (Array.isArray(path) === false) { + if (!Array.isArray(path)) { const normalized = normalizeRoutePath(path) return [normalized] } diff --git a/packages/datadog-instrumentations/src/http/client.js b/packages/datadog-instrumentations/src/http/client.js index c266c84479..48d98980f6 100644 --- a/packages/datadog-instrumentations/src/http/client.js +++ b/packages/datadog-instrumentations/src/http/client.js @@ -89,9 +89,7 @@ function setupResponseInstrumentation (ctx, res) { const collectChunk = chunk => { if (!bodyChunks || !chunk) return - if (typeof chunk === 'string') { - bodyChunks.push(chunk) - } else if (Buffer.isBuffer(chunk)) { + if (typeof chunk === 'string' || Buffer.isBuffer(chunk)) { bodyChunks.push(chunk) } else { // Handle Uint8Array or other array-like types diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 0cd20675b4..d683e9c630 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -96,7 +96,7 @@ const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-even const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog') const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original') const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID') -const jestSessionState = globalThis[JEST_SESSION_STATE] || (globalThis[JEST_SESSION_STATE] = {}) +const jestSessionState = (globalThis[JEST_SESSION_STATE] ||= {}) // https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54 const RETRY_TIMES = Symbol.for('RETRY_TIMES') @@ -1192,9 +1192,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { ctx.testParameters = testParameters ctx.frameworkVersion = jestVersion ctx.isNew = isNewTest - ctx.isEfdRetry = ctx.isEfdRetry || numEfdRetry > 0 + ctx.isEfdRetry ||= numEfdRetry > 0 ctx.isAttemptToFix = isAttemptToFix - ctx.isAttemptToFixRetry = ctx.isAttemptToFixRetry || numOfAttemptsToFixRetries > 0 + ctx.isAttemptToFixRetry ||= numOfAttemptsToFixRetries > 0 ctx.isJestRetry = isJestRetry ctx.isDisabled = isDisabled ctx.isQuarantined = isQuarantined @@ -1361,8 +1361,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { } } } - } - if (event.name === 'test_done') { + } else if (event.name === 'test_done') { const originalError = event.test?.errors?.[0] let status = 'pass' if (event.test.errors && event.test.errors.length) { @@ -1565,8 +1564,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { if (ctx.concurrentTestState) { ctx.currentStore = undefined } - } - if (event.name === 'run_finish') { + } else if (event.name === 'run_finish') { for (const [test, errors] of atrSuppressedErrors) { // Do not restore errors for non-ATF quarantined tests — they should stay suppressed // so Jest doesn't see the failure (prevents --bail from stopping the run). @@ -1595,8 +1593,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { attemptToFixRetriedTestsStatuses.clear() testsToBeRetried.clear() testSuiteDatadogEnvironments.delete(this.testSuiteAbsolutePath) - } - if (event.name === 'test_skip' || event.name === 'test_todo') { + } else if (event.name === 'test_skip' || event.name === 'test_todo') { const testName = getJestTestName(event.test) testSkippedCh.publish({ test: { @@ -2298,12 +2295,11 @@ function getCliWrapper (isNewJestVersion) { } = skippableSuitesResponse || await getChannelPromise(skippableSuitesCh) if (err) { skippableSuitesCoverage = {} - skippedSuitesCoverage = {} } else { skippableSuites = receivedSkippableSuites skippableSuitesCoverage = receivedSkippableSuitesCoverage || {} - skippedSuitesCoverage = {} } + skippedSuitesCoverage = {} } catch (err) { log.error('Jest test-suite skippable error', err) } @@ -3037,9 +3033,7 @@ function wrapJestObject (jestObject, suiteFilePath) { shimmer.wrap(jestObject, 'mock', mock => function (moduleName) { // If the library is mocked with `jest.mock`, we don't want to bypass jest's own require engine - if (LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) { - LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName) - } + LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName) recordMockedFile(suiteFilePath, moduleName) return mock.apply(this, arguments) }) diff --git a/packages/datadog-instrumentations/src/mocha/common.js b/packages/datadog-instrumentations/src/mocha/common.js index 975e5c1cff..6e77efc44a 100644 --- a/packages/datadog-instrumentations/src/mocha/common.js +++ b/packages/datadog-instrumentations/src/mocha/common.js @@ -41,11 +41,9 @@ addHook({ }, (Suite) => { shimmer.wrap(Suite.prototype, 'addTest', addTest => function (test) { const callSites = getCallSites() - let startLine const testCallSite = callSites.find(site => site.getFileName() === test.file) if (testCallSite) { - startLine = testCallSite.getLineNumber() - testToStartLine.set(test, startLine) + testToStartLine.set(test, testCallSite.getLineNumber()) } return addTest.apply(this, arguments) }) diff --git a/packages/datadog-instrumentations/src/moleculer/client.js b/packages/datadog-instrumentations/src/moleculer/client.js index eb0c0d72fc..9d31bb2bc6 100644 --- a/packages/datadog-instrumentations/src/moleculer/client.js +++ b/packages/datadog-instrumentations/src/moleculer/client.js @@ -10,7 +10,7 @@ const errorChannel = channel('apm:moleculer:call:error') function wrapCall (call) { return function (actionName, params, opts) { opts = arguments[2] = opts || {} - opts.meta = opts.meta || {} + opts.meta ||= {} arguments.length = Math.max(3, arguments.length) diff --git a/packages/datadog-instrumentations/src/playwright.js b/packages/datadog-instrumentations/src/playwright.js index 8d153b083e..84a2f285e6 100644 --- a/packages/datadog-instrumentations/src/playwright.js +++ b/packages/datadog-instrumentations/src/playwright.js @@ -1350,7 +1350,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) { let totalFailedTestCount = 0 let totalPureQuarantinedFailedTestCount = 0 - for (const [fqn, testStatuses] of testsToTestStatuses.entries()) { + for (const [fqn, testStatuses] of testsToTestStatuses) { // Only count as failed if the final status (after retries) is 'fail' const lastStatus = testStatuses.at(-1) if (lastStatus === 'fail') { diff --git a/packages/datadog-instrumentations/src/rhea.js b/packages/datadog-instrumentations/src/rhea.js index 135b381866..2efb910eb2 100644 --- a/packages/datadog-instrumentations/src/rhea.js +++ b/packages/datadog-instrumentations/src/rhea.js @@ -222,7 +222,7 @@ function beforeFinish (delivery, state) { } function getStateFromData (stateData) { - if (stateData && stateData.descriptor && stateData.descriptor) { + if (stateData?.descriptor) { switch (stateData.descriptor.value) { case 0x24: return 'accepted' case 0x25: return 'rejected' diff --git a/packages/datadog-instrumentations/test/helpers/register.spec.js b/packages/datadog-instrumentations/test/helpers/register.spec.js index 56c0f5afea..489fcfee6f 100644 --- a/packages/datadog-instrumentations/test/helpers/register.spec.js +++ b/packages/datadog-instrumentations/test/helpers/register.spec.js @@ -82,4 +82,19 @@ describe('register', () => { sinon.assert.notCalled(hooksMock['@confluentinc/kafka-javascript'].fn) sinon.assert.notCalled(hooksMock['mongodb-core'].fn) }) + + for (const disabledName of ['fs', 'node:fs']) { + it(`should disable both builtin hook names when ${disabledName} is disabled`, () => { + hooksMock.fs = { fn: sinon.stub() } + hooksMock['node:fs'] = { fn: sinon.stub() } + + loadRegisterWithEnv({ DD_TRACE_DISABLED_INSTRUMENTATIONS: disabledName }) + + const registeredNames = [] + for (const [names] of HookMock.args) { + registeredNames.push(names[0]) + } + assert.deepStrictEqual(registeredNames.sort(), ['@confluentinc/kafka-javascript', 'mongodb-core']) + }) + } }) diff --git a/packages/datadog-plugin-aerospike/src/index.js b/packages/datadog-plugin-aerospike/src/index.js index d7f70f574b..d0676f6709 100644 --- a/packages/datadog-plugin-aerospike/src/index.js +++ b/packages/datadog-plugin-aerospike/src/index.js @@ -67,9 +67,7 @@ function getMeta (resourceName, commandArgs) { const [ns, set, bin, exp, index] = commandArgs // The `ext` argument was added to IndexCreate in 6.3.0 - meta = commandArgs.length > 8 - ? getMetaForIndex(ns, set, bin, index) - : getMetaForIndex(ns, set, bin, exp) + meta = getMetaForIndex(ns, set, bin, commandArgs.length > 8 ? index : exp) } else if (resourceName === 'Query') { const { ns, set } = commandArgs[2] meta = getMetaForQuery({ ns, set }) diff --git a/packages/datadog-plugin-amqplib/src/client.js b/packages/datadog-plugin-amqplib/src/client.js index 943cfd0f31..0985d6389b 100644 --- a/packages/datadog-plugin-amqplib/src/client.js +++ b/packages/datadog-plugin-amqplib/src/client.js @@ -33,7 +33,7 @@ class AmqplibClientPlugin extends ClientPlugin { }, }, ctx) - fields.headers = fields.headers || {} + fields.headers ||= {} this.tracer.inject(span, TEXT_MAP, fields.headers) diff --git a/packages/datadog-plugin-amqplib/src/producer.js b/packages/datadog-plugin-amqplib/src/producer.js index e902c0e852..75ee015d48 100644 --- a/packages/datadog-plugin-amqplib/src/producer.js +++ b/packages/datadog-plugin-amqplib/src/producer.js @@ -52,7 +52,7 @@ class AmqplibProducerPlugin extends ProducerPlugin { }, }, ctx) - fields.headers = fields.headers || {} + fields.headers ||= {} this.tracer.inject(span, TEXT_MAP, fields.headers) diff --git a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js index 471d8f109c..e9e6b2b879 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +++ b/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js @@ -39,9 +39,9 @@ class EventBridge extends BaseAwsSdkPlugin { const rulename = params.Name ?? '' return { 'resource.name': operation ? `${operation} ${params.source}` : params.source, - 'aws.eventbridge.source': `${params.source}`, + 'aws.eventbridge.source': params.source, 'messaging.system': 'aws_eventbridge', - rulename: `${rulename}`, + rulename, } } diff --git a/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js b/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js index 283ccd22be..28a5a6a844 100644 --- a/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +++ b/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js @@ -29,9 +29,9 @@ class Stepfunctions extends BaseAwsSdkPlugin { generateTags (params, operation, response) { if (!params) return - const tags = { 'resource.name': params.name ? `${operation} ${params.name}` : `${operation}` } + const tags = { 'resource.name': params.name ? `${operation} ${params.name}` : operation } if (operation === 'startExecution' || operation === 'startSyncExecution') { - tags.statemachinearn = `${params.stateMachineArn}` + tags.statemachinearn = params.stateMachineArn } return tags } diff --git a/packages/datadog-plugin-azure-event-hubs/src/producer.js b/packages/datadog-plugin-azure-event-hubs/src/producer.js index 4c17ff6867..22ad0b1528 100644 --- a/packages/datadog-plugin-azure-event-hubs/src/producer.js +++ b/packages/datadog-plugin-azure-event-hubs/src/producer.js @@ -46,9 +46,7 @@ class AzureEventHubsProducerPlugin extends ProducerPlugin { } injectTraceContext(this.tracer, span, ctx.eventData) } - } - - if (ctx.functionName === 'sendBatch') { + } else if (ctx.functionName === 'sendBatch') { const eventData = ctx.eventData const eventDataLength = eventData.length || eventData._context.connection._eventsCount span.setTag('messaging.operation', 'send') diff --git a/packages/datadog-plugin-azure-service-bus/src/producer.js b/packages/datadog-plugin-azure-service-bus/src/producer.js index 83f8a1a3b0..32085fc1c9 100644 --- a/packages/datadog-plugin-azure-service-bus/src/producer.js +++ b/packages/datadog-plugin-azure-service-bus/src/producer.js @@ -45,9 +45,11 @@ class AzureServiceBusProducerPlugin extends ProducerPlugin { } injectTraceContext(this.tracer, span, ctx.msg) } - } - - if (ctx.functionName === 'send' || ctx.functionName === 'sendBatch' || ctx.functionName === 'scheduleMessages') { + } else if ( + ctx.functionName === 'send' || + ctx.functionName === 'sendBatch' || + ctx.functionName === 'scheduleMessages' + ) { const messages = ctx.msg const isBatch = messages.constructor?.name === 'ServiceBusMessageBatchImpl' if (isBatch) { diff --git a/packages/datadog-plugin-bullmq/src/producer.js b/packages/datadog-plugin-bullmq/src/producer.js index 529620a713..96c63d948d 100644 --- a/packages/datadog-plugin-bullmq/src/producer.js +++ b/packages/datadog-plugin-bullmq/src/producer.js @@ -222,7 +222,7 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin { for (let i = 0; i < jobs.length; i++) { const job = jobs[i] if (!job) continue - job.opts = job.opts || {} + job.opts ||= {} cache[i] = this._injectIntoOpts(span, job.opts) } ctx._ddMetadata = cache @@ -274,7 +274,7 @@ class FlowProducerAddPlugin extends BaseBullmqProducerPlugin { injectTraceContext (span, ctx) { const flow = ctx.arguments?.[0] if (!flow) return - flow.opts = flow.opts || {} + flow.opts ||= {} ctx._ddMetadata = this._injectIntoOpts(span, flow.opts) } @@ -283,7 +283,7 @@ class FlowProducerAddPlugin extends BaseBullmqProducerPlugin { if (!flow) { return { queueName: 'bullmq', payloadSize: 0, optsTarget: undefined } } - flow.opts = flow.opts || {} + flow.opts ||= {} return { queueName: flow.queueName || 'bullmq', payloadSize: flow.data ? getMessageSize(flow.data) : 0, diff --git a/packages/datadog-plugin-child_process/src/index.js b/packages/datadog-plugin-child_process/src/index.js index 44ae3d7c2f..8036a0fecd 100644 --- a/packages/datadog-plugin-child_process/src/index.js +++ b/packages/datadog-plugin-child_process/src/index.js @@ -52,7 +52,7 @@ class ChildProcessPlugin extends TracingPlugin { } if (truncated) { - meta['cmd.truncated'] = `${truncated}` + meta['cmd.truncated'] = 'true' } this.startSpan('command_execution', { @@ -81,7 +81,7 @@ class ChildProcessPlugin extends TracingPlugin { const span = ctx.currentStore?.span || this.activeSpan - span?.setTag('cmd.exit_code', `${exitCode}`) + span?.setTag('cmd.exit_code', String(exitCode)) span?.finish() return ctx.parentStore @@ -112,7 +112,7 @@ class ChildProcessPlugin extends TracingPlugin { const span = ctx.currentStore?.span || this.activeSpan - span?.setTag('cmd.exit_code', `${exitCode}`) + span?.setTag('cmd.exit_code', String(exitCode)) span?.finish() return ctx.parentStore 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 29f3b62aa7..0ee58361ec 100644 --- a/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +++ b/packages/datadog-plugin-child_process/src/scrub-cmd-params.js @@ -14,18 +14,15 @@ const envVarRegex = new RegExp(ENV_PATTERN) const REDACTED = '?' function extractVarNames (expression) { - const varNames = new Set() + const varNames = {} let match while ((match = VARNAMES_REGEX.exec(expression))) { - varNames.add(match[1]) + const name = match[1] + varNames[name] = `$${name}` } - const varNamesObject = {} - for (const varName of varNames.keys()) { - varNamesObject[varName] = `$${varName}` - } - return varNamesObject + return varNames } function getTokensByExpression (expressionTokens) { diff --git a/packages/datadog-plugin-fastify/src/tracing.js b/packages/datadog-plugin-fastify/src/tracing.js index debe8cf61b..bf6656e2f3 100644 --- a/packages/datadog-plugin-fastify/src/tracing.js +++ b/packages/datadog-plugin-fastify/src/tracing.js @@ -27,7 +27,7 @@ class FastifyTracingPlugin extends RouterPlugin { } function getParentStore (ctx) { - ctx.parentStore = ctx.parentStore ?? storage('legacy').getStore() + ctx.parentStore ??= storage('legacy').getStore() return ctx.parentStore } diff --git a/packages/datadog-plugin-fs/src/index.js b/packages/datadog-plugin-fs/src/index.js index c7d720669f..bbf6a620f0 100644 --- a/packages/datadog-plugin-fs/src/index.js +++ b/packages/datadog-plugin-fs/src/index.js @@ -7,10 +7,6 @@ class FsPlugin extends TracingPlugin { static operation = 'operation' static experimental = true - configure (...args) { - return super.configure(...args) - } - bindStart (ctx) { if (!this.activeSpan) return { noop: true } diff --git a/packages/datadog-plugin-graphql/src/execute.js b/packages/datadog-plugin-graphql/src/execute.js index 438c8de828..5e54fcee30 100644 --- a/packages/datadog-plugin-graphql/src/execute.js +++ b/packages/datadog-plugin-graphql/src/execute.js @@ -495,6 +495,7 @@ function wrapResolve (resolve) { // run in the parent store, not field.currentStore: the first sibling's // synchronous resolver already finished the shared graphql.resolve span, so // re-entering its store would parent user spans to a closed span. + // eslint-disable-next-line unicorn/prefer-else-if -- Keep sibling early return outside first-field setup. if (!isFirst) { return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.parentStore, (err) => { if (updateFieldCh.hasSubscribers) { diff --git a/packages/datadog-plugin-graphql/src/tools/signature.js b/packages/datadog-plugin-graphql/src/tools/signature.js index e36bda9d2f..01015d0ba5 100644 --- a/packages/datadog-plugin-graphql/src/tools/signature.js +++ b/packages/datadog-plugin-graphql/src/tools/signature.js @@ -11,7 +11,7 @@ const transforms = require('./transforms') const cache = new WeakMap() function defaultEngineReportingSignature (ast, operationName) { - const key = operationName == null ? '' : operationName + const key = operationName ?? '' let inner = cache.get(ast) if (inner !== undefined) { const cached = inner.get(key) diff --git a/packages/datadog-plugin-http/src/client.js b/packages/datadog-plugin-http/src/client.js index 46f143915c..13f2b7a9af 100644 --- a/packages/datadog-plugin-http/src/client.js +++ b/packages/datadog-plugin-http/src/client.js @@ -10,6 +10,7 @@ const HTTP_HEADERS = formats.HTTP_HEADERS const urlFilter = require('../../dd-trace/src/plugins/util/urlfilter') const { buildClientHttpUrl } = require('../../dd-trace/src/plugins/util/url') const log = require('../../dd-trace/src/log') +const { stripQueryAndFragment } = require('../../dd-trace/src/util') const { CLIENT_PORT_KEY, COMPONENT, ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants') const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE @@ -32,7 +33,7 @@ class HttpClientPlugin extends ClientPlugin { // A URL object (e.g. from the fetch integration) carries the query in // `options.search`, not `options.path`; keep it so url.full retains the query. const pathname = options.path || `${options.pathname || ''}${options.search || ''}` - const path = pathname ? pathname.split(/[?#]/)[0] : '/' + const path = pathname ? stripQueryAndFragment(pathname) : '/' const uri = `${base}${path}` const allowed = this.config.filter(uri) diff --git a/packages/datadog-plugin-jest/src/index.js b/packages/datadog-plugin-jest/src/index.js index 9b0d7e5f32..069e1bc0dd 100644 --- a/packages/datadog-plugin-jest/src/index.js +++ b/packages/datadog-plugin-jest/src/index.js @@ -237,9 +237,8 @@ class JestPlugin extends CiPlugin { const testSuiteMetadata = { ...getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, 'jest'), - // requestErrorTags from test env options may be undefined - ...(requestErrorTags !== undefined && requestErrorTags !== null ? requestErrorTags : {}), - ...(itrSkippingEnabledTags !== undefined && itrSkippingEnabledTags !== null ? itrSkippingEnabledTags : {}), + ...requestErrorTags, + ...itrSkippingEnabledTags, } if (_ddUnskippable) { diff --git a/packages/datadog-plugin-langchain/src/tokens.js b/packages/datadog-plugin-langchain/src/tokens.js index 856115e4b3..5c66b0d4b6 100644 --- a/packages/datadog-plugin-langchain/src/tokens.js +++ b/packages/datadog-plugin-langchain/src/tokens.js @@ -9,7 +9,7 @@ function getTokensFromLlmOutput (result) { const { llmOutput } = result if (!llmOutput) return tokens - const tokenUsage = llmOutput.tokenUsage || llmOutput.usage_metadata || llmOutput.usage_metadata + const tokenUsage = llmOutput.tokenUsage || llmOutput.usage_metadata if (!tokenUsage) return tokens for (const tokenNames of [['input', 'prompt'], ['output', 'completion'], ['total']]) { @@ -25,7 +25,7 @@ function getTokensFromLlmOutput (result) { } // assign total_tokens again in case it was improperly set the first time, or was not on tokenUsage - tokens.total = tokens.total || tokens.input + tokens.output + tokens.total ||= tokens.input + tokens.output return tokens } diff --git a/packages/datadog-plugin-prisma/src/index.js b/packages/datadog-plugin-prisma/src/index.js index 313851bf27..96572e3c9d 100644 --- a/packages/datadog-plugin-prisma/src/index.js +++ b/packages/datadog-plugin-prisma/src/index.js @@ -125,9 +125,13 @@ class PrismaPlugin extends DatabasePlugin { } } +/** + * @param {string} resource + * @param {{ name?: string, model?: string, method?: string }} [attributes] + */ function formatResourceName (resource, attributes) { if (attributes?.name) { - return `${attributes.name}`.trim() + return attributes.name.trim() } if (attributes?.model && attributes.method) { return `${attributes.model}.${attributes.method}`.trim() diff --git a/packages/datadog-plugin-rhea/src/producer.js b/packages/datadog-plugin-rhea/src/producer.js index 70ca0058d5..e5bbccebbc 100644 --- a/packages/datadog-plugin-rhea/src/producer.js +++ b/packages/datadog-plugin-rhea/src/producer.js @@ -37,7 +37,7 @@ class RheaProducerPlugin extends ProducerPlugin { function addDeliveryAnnotations (msg, tracer, span) { if (msg) { - msg.delivery_annotations = msg.delivery_annotations || {} + msg.delivery_annotations ||= {} tracer.inject(span, 'text_map', msg.delivery_annotations) diff --git a/packages/datadog-plugin-undici/src/index.js b/packages/datadog-plugin-undici/src/index.js index cb164b28a2..90f50f077e 100644 --- a/packages/datadog-plugin-undici/src/index.js +++ b/packages/datadog-plugin-undici/src/index.js @@ -7,6 +7,7 @@ const formats = require('../../../ext/formats') const HTTP_HEADERS = formats.HTTP_HEADERS const log = require('../../dd-trace/src/log') const { buildClientHttpUrl } = require('../../dd-trace/src/plugins/util/url') +const { stripQueryAndFragment } = require('../../dd-trace/src/util') const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants') const { @@ -62,7 +63,7 @@ class UndiciPlugin extends HttpClientPlugin { const host = port ? `${hostname}:${port}` : hostname const base = `${protocol}//${host}` - const pathname = path.split(/[?#]/)[0] + const pathname = stripQueryAndFragment(path) const uri = `${base}${pathname}` const allowed = this.config.filter(uri) diff --git a/packages/datadog-webpack/index.js b/packages/datadog-webpack/index.js index 18e06db5cf..728a29b670 100644 --- a/packages/datadog-webpack/index.js +++ b/packages/datadog-webpack/index.js @@ -141,7 +141,7 @@ class DatadogWebpackPlugin { // (#8980); absent peers stay opaque, so a build that does not opt into the feature does // not follow their dependency chain (#8635). if (matchesOptionalPeerFile(normalizedResource)) { - createData.loaders = createData.loaders || [] + createData.loaders ||= [] createData.loaders.push({ loader: require.resolve('./src/optional-peer-loader') }) log.debug('INLINE: optional-peer loader applied to %s', normalizedResource) return @@ -188,7 +188,7 @@ class DatadogWebpackPlugin { const version = packageJson.version const pkgPath = request === pkg ? pkg : `${pkg}/${modulePath}` - createData.loaders = createData.loaders || [] + createData.loaders ||= [] createData.loaders.unshift({ loader: require.resolve('./src/loader'), options: { pkg, version, path: pkgPath }, diff --git a/packages/dd-trace/src/appsec/downstream_requests.js b/packages/dd-trace/src/appsec/downstream_requests.js index 893bb55a9f..a2b50ad1c5 100644 --- a/packages/dd-trace/src/appsec/downstream_requests.js +++ b/packages/dd-trace/src/appsec/downstream_requests.js @@ -307,7 +307,7 @@ function parseBody (body, contentType) { const formBody = Buffer.isBuffer(body) ? body.toString('utf8') : String(body) const params = new URLSearchParams(formBody) const result = {} - for (const [key, value] of params.entries()) { + for (const [key, value] of params) { if (key in result) { const existing = result[key] if (Array.isArray(existing)) { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js index 79e7a8dbcd..6cb6f9f554 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js @@ -56,7 +56,7 @@ class HardcodedBaseAnalyzer extends Analyzer { } _getEvidence (value) { - return { value: `${value.data}` } + return { value: value.data } } _getLocation (value) { diff --git a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js index 509b292291..34eb922382 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js @@ -11,7 +11,7 @@ class HardcodedPasswordAnalyzer extends HardcodedBaseAnalyzer { } _getEvidence (value) { - return { value: `${value.ident}` } + return { value: value.ident } } } diff --git a/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js b/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js index 2f2f5434b1..9c88f74344 100644 --- a/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +++ b/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js @@ -74,7 +74,7 @@ class SqlInjectionAnalyzer extends StoredInjectionAnalyzer { } analyze (value, store, dialect) { - store = store || storage('legacy').getStore() + store ||= storage('legacy').getStore() if (!(store && store.sqlAnalyzed)) { super.analyze(value, store, dialect) } diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js b/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js index 15d70e3db6..70ea25a731 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js @@ -215,9 +215,7 @@ function getTaintTrackingImpl (telemetryVerbosity, dummy = false) { if (dummy) return TaintTrackingNoop // with Verbosity.DEBUG every invocation of a TaintedUtils method increases the EXECUTED_PROPAGATION metric - return isDebugAllowed(telemetryVerbosity) - ? createImplWith(getContextDebug) - : createImplWith(getContextDefault) + return createImplWith(isDebugAllowed(telemetryVerbosity) ? getContextDebug : getContextDefault) } function getTaintTrackingNoop () { 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 882da0e028..736f127740 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +++ b/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js @@ -35,7 +35,7 @@ function stringifyWithRanges (obj, objRanges, loadSensitiveRanges = false) { let value const ranges = [] const sensitiveRanges = [] - objRanges = objRanges || {} + objRanges ||= {} if (objRanges || loadSensitiveRanges) { const cloneObj = Array.isArray(obj) ? [] : {} diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index 34de46a5a8..22beee3ca1 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -21,7 +21,7 @@ let stackTraceMaxDepth let maxStackTraces function canAddVulnerability (vulnerability) { - const hasRequiredFields = vulnerability?.evidence && vulnerability?.type && vulnerability?.location + const hasRequiredFields = vulnerability?.evidence && vulnerability.type && vulnerability.location if (!hasRequiredFields) return false const isDuplicated = deduplicationEnabled && isDuplicatedVulnerability(vulnerability) @@ -64,7 +64,7 @@ function addVulnerability (iastContext, vulnerability, callSiteFrames) { } if (iastContext?.rootSpan) { - iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || [] + iastContext[VULNERABILITIES_KEY] ||= [] iastContext[VULNERABILITIES_KEY].push(vulnerability) } else { sendVulnerabilities([vulnerability], span) diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index 10388a51ae..cbd7685d34 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -160,7 +160,7 @@ function flattenFields (fields, depth = 0) { if (value && typeof value === 'object') { const { result: flatValue, truncated: inheritTruncated } = flattenFields(value, depth + 1) - truncated = truncated || inheritTruncated + truncated ||= inheritTruncated if (flatValue) { for (const flatKey of Object.keys(flatValue)) { diff --git a/packages/dd-trace/src/appsec/waf/waf_manager.js b/packages/dd-trace/src/appsec/waf/waf_manager.js index f1379b2d46..1620661df1 100644 --- a/packages/dd-trace/src/appsec/waf/waf_manager.js +++ b/packages/dd-trace/src/appsec/waf/waf_manager.js @@ -39,7 +39,7 @@ class WAFManager { const { obfuscatorKeyRegex, obfuscatorValueRegex } = this.config return new DDWAF(rules, WAFManager.defaultWafConfigPath, { obfuscatorKeyRegex, obfuscatorValueRegex }) } catch (err) { - this.ddwafVersion = this.ddwafVersion || 'unknown' + this.ddwafVersion ||= 'unknown' Reporter.reportWafInit(this.ddwafVersion, 'unknown') log.error('[ASM] AppSec could not load native package. In-app WAF features will not be available.', err) 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 0893ab8d1d..a502df8990 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 @@ -15,6 +15,7 @@ const { uploadCoverageReport: uploadCoverageReportRequest } = require('../reques const { uploadTestScreenshot: uploadTestScreenshotRequest } = require('../requests/upload-test-screenshot') const { parsers } = require('../../config/parsers') const log = require('../../log') +const { getSegment } = require('../../util') const BufferingExporter = require('../../exporters/common/buffering-exporter') const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../../plugins/util/tags') const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata') @@ -28,8 +29,7 @@ function getTestConfigurationTags (tags) { } return Object.keys(tags).reduce((acc, key) => { if (key.startsWith('test.configuration.')) { - const [, configKey] = key.split('test.configuration.') - acc[configKey] = tags[key] + acc[getSegment(key, 'test.configuration.', 1)] = tags[key] } return acc }, {}) diff --git a/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js b/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js index 4fcde1070d..987c39ba3c 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +++ b/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js @@ -8,6 +8,7 @@ const FormData = require('../../../exporters/common/form-data') const request = require('../../../exporters/common/request') const log = require('../../../log') +const { getSegment } = require('../../../util') const { getLatestCommits, getRepositoryUrl, @@ -145,7 +146,7 @@ function uploadPackFile ({ url, isEvpProxy, evpProxyPrefix, packFileToUpload, re try { const packFileContent = fs.readFileSync(packFileToUpload) // The original filename includes a random prefix, so we remove it here - const [, filename] = path.basename(packFileToUpload).split('-') + const filename = getSegment(path.basename(packFileToUpload), '-', 1) form.append('packfile', packFileContent, { filename, contentType: 'application/octet-stream', diff --git a/packages/dd-trace/src/ci-visibility/requests/request.js b/packages/dd-trace/src/ci-visibility/requests/request.js index ef6e6e699a..bd3b194e7f 100644 --- a/packages/dd-trace/src/ci-visibility/requests/request.js +++ b/packages/dd-trace/src/ci-visibility/requests/request.js @@ -42,7 +42,7 @@ function request (data, options, callback) { if (url.protocol === 'unix:') { opts.socketPath = url.pathname } else { - opts.path = opts.path ?? url.path + opts.path ??= url.path opts.protocol = url.protocol opts.hostname = url.hostname opts.port = url.port @@ -100,9 +100,9 @@ function request (data, options, callback) { if (res.statusCode === 429 && !hasRetried) { const resetHeader = res.headers['x-ratelimit-reset'] const resetTs = (resetHeader === null || resetHeader === undefined) - ? Number.NaN + ? NaN : Number.parseInt(resetHeader, 10) - const waitMs = Number.isFinite(resetTs) ? Math.max(0, resetTs * 1000 - Date.now()) : Number.NaN + const waitMs = Number.isFinite(resetTs) ? Math.max(0, resetTs * 1000 - Date.now()) : NaN if (Number.isFinite(waitMs) && waitMs <= RATE_LIMIT_MAX_WAIT_MS) { hasRetried = true diff --git a/packages/dd-trace/src/datastreams/encoding.js b/packages/dd-trace/src/datastreams/encoding.js index e91b184dd4..e9eab5d67b 100644 --- a/packages/dd-trace/src/datastreams/encoding.js +++ b/packages/dd-trace/src/datastreams/encoding.js @@ -40,7 +40,7 @@ function encodeVarintInto (target, offset, value) { let i = offset const limit = offset + maxVarLen64 - 1 // if first byte is 1, the number is negative in javascript, but we want to interpret it as positive - while ((high !== 0 || low < 0 || low > 0x80) && i < limit) { + while ((high !== 0 || low < 0 || low > 0x7F) && i < limit) { target[i] = (low & 0x7F) | 0x80 low >>>= 7 low |= (high & 0x7F) << 25 @@ -88,7 +88,7 @@ function decodeUvarint64 ( low |= n << s } if (s > 0) { - high |= s - 32 > 0 ? n << (s - 32) : n >> (32 - s) + high |= s > 32 ? n << (s - 32) : n >> (32 - s) } return [low, high, bytes] } @@ -96,8 +96,7 @@ function decodeUvarint64 ( low |= (n & 0x7F) << s } if (s > 0) { - high |= - s - 32 > 0 ? (n & 0x7F) << (s - 32) : (n & 0x7F) >> (32 - s) + high |= s > 32 ? (n & 0x7F) << (s - 32) : (n & 0x7F) >> (32 - s) } s += 7 } diff --git a/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js b/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js index 001d05efca..bc447a56e0 100644 --- a/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +++ b/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js @@ -36,10 +36,7 @@ function getRemoveProbeExpression (id) { * @returns {string} */ function compileBreakpointCondition (probes) { - const probeConditions = [] - for (const probe of probes) { - probeConditions.push(compileProbeCondition(probe)) - } + const probeConditions = probes.map(compileProbeCondition) // NOTE: $dd_sampler is read from the realm-local `globalThis` where it was installed (the main // realm). A probe whose code runs in a different V8 realm (e.g. a `vm.createContext` script with a diff --git a/packages/dd-trace/src/debugger/devtools_client/send.js b/packages/dd-trace/src/debugger/devtools_client/send.js index ec4aeead80..460af4f268 100644 --- a/packages/dd-trace/src/debugger/devtools_client/send.js +++ b/packages/dd-trace/src/debugger/devtools_client/send.js @@ -67,14 +67,13 @@ function send (message, logger, dd, snapshot, processTags) { if (pruned) { json = pruned - size = Buffer.byteLength(json) } else { // Fallback if pruning fails const line = Object.keys(snapshot.captures.lines)[0] snapshot.captures.lines[line] = { pruned: true } json = JSON.stringify(payload) - size = Buffer.byteLength(json) } + size = Buffer.byteLength(json) } jsonBuffer.write(json, size) diff --git a/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js b/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js index ac0c5824ad..938331d792 100644 --- a/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js @@ -61,7 +61,7 @@ async function getLocalStateForCallFrame (callFrame, limits, deadlineNs = BIGINT // Delay calling `processRawState` so caller can resume the main thread before processing `rawState` return { processLocalState () { - processedState = processedState ?? processRawState(rawState, maxLength) + processedState ??= processRawState(rawState, maxLength) return processedState }, fatalErrors: ctx.fatalErrors, @@ -155,7 +155,7 @@ async function evaluateCaptureExpressions (callFrame, expressions, deadlineNs = fatalErrors.push(...ctx.fatalErrors) } - if (ctx.deadlineReached === true) { + if (ctx.deadlineReached) { // Add the current expression (properties may be incomplete due to timeout) rawResults.push({ name, remoteObject: result, maxLength }) // Add stub entries for remaining uncaptured expressions diff --git a/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js b/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js index 020f94150a..af573a69f9 100644 --- a/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +++ b/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js @@ -97,10 +97,24 @@ const REDACTED_IDENTIFIERS = new Set( 'XSRF-TOKEN', ...config.dynamicInstrumentation.redactedIdentifiers, ] - .map((name) => normalizeName(name)) - .filter((name) => excludedIdentifiers.has(name) === false) + .map(normalizeIdentifier) + .filter(isIncludedIdentifier) ) +/** + * @param {string} name + */ +function normalizeIdentifier (name) { + return normalizeName(name) +} + +/** + * @param {string} name + */ +function isIncludedIdentifier (name) { + return !excludedIdentifiers.has(name) +} + function normalizeName (name, isSymbol) { if (isSymbol) name = name.slice(7, -1) // Remove `Symbol(` and `)` return name.toLowerCase().replaceAll(/[-_@$.]/g, '') diff --git a/packages/dd-trace/src/encode/tags-processors.js b/packages/dd-trace/src/encode/tags-processors.js index 99c15ff747..22f7386033 100644 --- a/packages/dd-trace/src/encode/tags-processors.js +++ b/packages/dd-trace/src/encode/tags-processors.js @@ -60,11 +60,11 @@ function eventTimeNano (event) { } function normalizeSpan (span) { - span.service = span.service || DEFAULT_SERVICE_NAME + span.service ||= DEFAULT_SERVICE_NAME if (span.service.length > MAX_SERVICE_LENGTH) { span.service = span.service.slice(0, MAX_SERVICE_LENGTH) } - span.name = span.name || DEFAULT_SPAN_NAME + span.name ||= DEFAULT_SPAN_NAME if (span.name.length > MAX_NAME_LENGTH) { span.name = span.name.slice(0, MAX_NAME_LENGTH) } diff --git a/packages/dd-trace/src/exporters/common/url.js b/packages/dd-trace/src/exporters/common/url.js index 7bf2ab9b82..bd381c996d 100644 --- a/packages/dd-trace/src/exporters/common/url.js +++ b/packages/dd-trace/src/exporters/common/url.js @@ -32,11 +32,9 @@ function parseUrl (urlObjOrString) { // `urlToHttpOptions` returns `pathname` at runtime, but @types/node narrows it // to `ClientRequestArgs`, which omits it; cast so the named-pipe fold below can // read and rewrite it. - const url = /** @type {import('node:http').ClientRequestArgs & { pathname: string }} */ ( - urlObjOrString !== null && typeof urlObjOrString === 'object' - ? urlToHttpOptions(urlObjOrString) - : urlToHttpOptions(new URL(urlObjOrString)) - ) + const url = /** @type {import('node:http').ClientRequestArgs & { pathname: string }} */ (urlToHttpOptions( + urlObjOrString !== null && typeof urlObjOrString === 'object' ? urlObjOrString : new URL(urlObjOrString) + )) if (url.protocol === 'unix:' && url.hostname === '.') { url.path = url.pathname = `//.${url.pathname}` diff --git a/packages/dd-trace/src/exporters/electron/index.js b/packages/dd-trace/src/exporters/electron/index.js index 813c0da2f0..88ef3e0276 100644 --- a/packages/dd-trace/src/exporters/electron/index.js +++ b/packages/dd-trace/src/exporters/electron/index.js @@ -16,6 +16,8 @@ class ElectronExporter { } export (spans) { + if (!traceChannel.hasSubscribers) return + this.#traces.push(spans) const { flushInterval } = this._config @@ -35,7 +37,8 @@ class ElectronExporter { clearTimeout(this.#timer) this.#timer = undefined - const traces = this.#traces.splice(0) + const traces = this.#traces + this.#traces = [] if (traces.length > 0 && traceChannel.hasSubscribers) { const formattedTraces = traces.map(spans => spans.map(span => normalizeSpan(truncateSpan(span)))) diff --git a/packages/dd-trace/src/external-logger/src/index.js b/packages/dd-trace/src/external-logger/src/index.js index c00e519f5e..cd3532a7bb 100644 --- a/packages/dd-trace/src/external-logger/src/index.js +++ b/packages/dd-trace/src/external-logger/src/index.js @@ -78,7 +78,6 @@ class ExternalLogger { // Flushes logs with optional callback for when the call is complete flush (cb = () => {}) { - let logs let numLogs let encodedLogs @@ -88,7 +87,7 @@ class ExternalLogger { } try { - logs = this.queue + const logs = this.queue this.queue = [] numLogs = logs.length diff --git a/packages/dd-trace/src/flare/index.js b/packages/dd-trace/src/flare/index.js index b270bbc8ce..584a861661 100644 --- a/packages/dd-trace/src/flare/index.js +++ b/packages/dd-trace/src/flare/index.js @@ -39,8 +39,8 @@ const flare = { logChannel?.unsubscribe(logger) logChannel = new LogChannel(logLevel) logChannel.subscribe(logger) - tracerLogs = tracerLogs || new FlareFile() - timer = timer || setTimeout(flare.cleanup, TIMEOUT) + tracerLogs ||= new FlareFile() + timer ||= setTimeout(flare.cleanup, TIMEOUT) }, send (task) { diff --git a/packages/dd-trace/src/id.js b/packages/dd-trace/src/id.js index ff7bbc9d88..b0006e44ba 100644 --- a/packages/dd-trace/src/id.js +++ b/packages/dd-trace/src/id.js @@ -185,7 +185,7 @@ function toNumberString (buffer, radix) { let low = readInt32(buffer, buffer.length - 4) let str = '' - radix = radix || 10 + radix ||= 10 while (1) { const mod = (high % radix) * UINT_MAX + low diff --git a/packages/dd-trace/src/llmobs/plugins/anthropic/index.js b/packages/dd-trace/src/llmobs/plugins/anthropic/index.js index db2d652d45..1535859e44 100644 --- a/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +++ b/packages/dd-trace/src/llmobs/plugins/anthropic/index.js @@ -93,7 +93,7 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin { const { usage } = chunk if (usage) { - const responseUsage = response.usage ?? (response.usage = { input_tokens: 0, output_tokens: 0 }) + const responseUsage = (response.usage ??= { input_tokens: 0, output_tokens: 0 }) responseUsage.output_tokens = usage.output_tokens const cacheCreationTokens = usage.cache_creation_input_tokens diff --git a/packages/dd-trace/src/llmobs/plugins/genai/index.js b/packages/dd-trace/src/llmobs/plugins/genai/index.js index 69c5d63cab..12da7da905 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai/index.js +++ b/packages/dd-trace/src/llmobs/plugins/genai/index.js @@ -24,7 +24,7 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { // Subscribe to streaming chunk events this.addSub('apm:google:genai:request:chunk', ({ ctx, chunk, done }) => { ctx.isStreaming = true - ctx.chunks = ctx.chunks || [] + ctx.chunks ||= [] if (chunk) ctx.chunks.push(chunk) if (!done) return diff --git a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js index 2438e9ed8e..fd10a858a7 100644 --- a/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +++ b/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js @@ -36,7 +36,7 @@ class LangChainLLMObsHandler { const runIdBase = runId ? runId.split('-').slice(0, -1).join('-') : '' const responseMetadata = message.response_metadata || {} - usage = usage || responseMetadata.usage || responseMetadata.tokenUsage || {} + usage ||= responseMetadata.usage || responseMetadata.tokenUsage || {} const inputTokens = usage.promptTokens || usage.inputTokens || usage.prompt_tokens || usage.input_tokens || 0 const outputTokens = diff --git a/packages/dd-trace/src/llmobs/sdk.js b/packages/dd-trace/src/llmobs/sdk.js index a93bcaf05a..0e2b2a1a19 100644 --- a/packages/dd-trace/src/llmobs/sdk.js +++ b/packages/dd-trace/src/llmobs/sdk.js @@ -318,14 +318,14 @@ class LLMObs extends NoopLLMObs { } throw e } finally { - if (autoinstrumented === false) { + if (!autoinstrumented) { telemetry.recordLLMObsAnnotate(span, err) } } } exportSpan (span) { - span = span || this._active() + span ||= this._active() let err = '' try { if (!span) { @@ -426,7 +426,7 @@ class LLMObs extends NoopLLMObs { err = 'invalid_metric_value' throw new Error('value must be a boolean for a boolean metric') } - if (metricType === 'json' && !(typeof value === 'object' && value != null && !Array.isArray(value))) { + if (metricType === 'json' && (typeof value !== 'object' || value == null || Array.isArray(value))) { err = 'invalid_metric_value' throw new Error('value must be a JSON object for a json metric') } diff --git a/packages/dd-trace/src/llmobs/tagger.js b/packages/dd-trace/src/llmobs/tagger.js index a1ec7a1bb7..6873208ba1 100644 --- a/packages/dd-trace/src/llmobs/tagger.js +++ b/packages/dd-trace/src/llmobs/tagger.js @@ -158,8 +158,7 @@ class LLMObsTagger { if (modelName) this.tagModelName(span, modelName) if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider) - sessionId = sessionId || - registry.get(parent)?.[SESSION_ID] || + sessionId ||= registry.get(parent)?.[SESSION_ID] || traceTags[SESSION_ID_TRACE_DEFAULT_KEY] || traceTags[PROPAGATED_SESSION_ID_KEY] if (sessionId) this._setTag(span, SESSION_ID, sessionId) diff --git a/packages/dd-trace/src/noop/proxy.js b/packages/dd-trace/src/noop/proxy.js index 47969c1ea5..6f6feaa1d9 100644 --- a/packages/dd-trace/src/noop/proxy.js +++ b/packages/dd-trace/src/noop/proxy.js @@ -55,7 +55,7 @@ class NoopProxy { if (typeof fn !== 'function') return - options = options || {} + options ||= {} return this._tracer.trace(name, options, fn) } @@ -68,7 +68,7 @@ class NoopProxy { if (typeof fn !== 'function') return fn - options = options || {} + options ||= {} return this._tracer.wrap(name, options, fn) } diff --git a/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js index c383d1e85a..a49e1e59cc 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +++ b/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js @@ -198,7 +198,7 @@ class OtlpTransformer extends OtlpTransformerBase { if (isJson) { dataPoint.startTimeUnixNano = String(dataPoint.startTimeUnixNano) dataPoint.timeUnixNano = String(dataPoint.timeUnixNano) - dataPoint.count = dataPoint.count || 0 + dataPoint.count ||= 0 } return dataPoint diff --git a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js index a192969103..ccfd8ac151 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +++ b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js @@ -253,7 +253,8 @@ class PeriodicMetricReader { #collectAndExport (callback = () => {}) { // Atomically drain measurements for export. New measurements can be recorded // during export without interfering with this batch. - const allMeasurements = this.#measurements.splice(0) + const allMeasurements = this.#measurements + this.#measurements = [] for (const instrument of this.observableInstruments) { const observableMeasurements = instrument.collect() diff --git a/packages/dd-trace/src/opentracing/span.js b/packages/dd-trace/src/opentracing/span.js index b5f20d61b0..6cfee05656 100644 --- a/packages/dd-trace/src/opentracing/span.js +++ b/packages/dd-trace/src/opentracing/span.js @@ -440,7 +440,7 @@ class DatadogSpan { } } - spanContext._trace.ticks = spanContext._trace.ticks || now() + spanContext._trace.ticks ||= now() if (startTime) { spanContext._trace.startTime = startTime } diff --git a/packages/dd-trace/src/opentracing/span_context.js b/packages/dd-trace/src/opentracing/span_context.js index 28fdd29123..6daee283b4 100644 --- a/packages/dd-trace/src/opentracing/span_context.js +++ b/packages/dd-trace/src/opentracing/span_context.js @@ -8,7 +8,7 @@ const TRACE_ID_128 = '_dd.p.tid' class DatadogSpanContext { constructor (props) { - props = props || {} + props ||= {} this._traceId = props.traceId this._spanId = props.spanId diff --git a/packages/dd-trace/src/plugins/plugin.js b/packages/dd-trace/src/plugins/plugin.js index 6a117dde41..d5e75fb67d 100644 --- a/packages/dd-trace/src/plugins/plugin.js +++ b/packages/dd-trace/src/plugins/plugin.js @@ -103,7 +103,7 @@ module.exports = class Plugin { * @returns {void} */ enter (span, store) { - store = store || legacyStorage.getStore() + store ||= legacyStorage.getStore() legacyStorage.enterWith({ ...store, span }) } diff --git a/packages/dd-trace/src/plugins/tracing.js b/packages/dd-trace/src/plugins/tracing.js index 0f4e2dd349..aee18b069a 100644 --- a/packages/dd-trace/src/plugins/tracing.js +++ b/packages/dd-trace/src/plugins/tracing.js @@ -265,6 +265,7 @@ class TracingPlugin extends Plugin { analyticsSampler.sample(span, config.measured) // TODO: Remove this after migration to TracingChannel is done. + // eslint-disable-next-line unicorn/no-unnecessary-boolean-comparison -- Context objects use the other branch. if (enterOrCtx === true) { legacyStorage.enterWith({ ...store, span }) } else if (enterOrCtx) { diff --git a/packages/dd-trace/src/plugins/util/ci.js b/packages/dd-trace/src/plugins/util/ci.js index ef7cf9f377..ac957e6237 100644 --- a/packages/dd-trace/src/plugins/util/ci.js +++ b/packages/dd-trace/src/plugins/util/ci.js @@ -316,10 +316,8 @@ module.exports = { } if (NODE_LABELS) { - let nodeLabels try { - nodeLabels = JSON.stringify(NODE_LABELS.split(' ')) - tags[CI_NODE_LABELS] = nodeLabels + tags[CI_NODE_LABELS] = JSON.stringify(NODE_LABELS.split(' ')) } catch { // ignore errors } diff --git a/packages/dd-trace/src/plugins/util/http-otel-semantics.js b/packages/dd-trace/src/plugins/util/http-otel-semantics.js index e139affd16..690ba3b607 100644 --- a/packages/dd-trace/src/plugins/util/http-otel-semantics.js +++ b/packages/dd-trace/src/plugins/util/http-otel-semantics.js @@ -258,7 +258,7 @@ function applyHttpOtelSemantics (formattedSpan) { // type): server spans are errors on 5xx only (4xx MUST be left unset per the // spec); client spans on any status >= 400. if (status !== undefined && newMeta[ERROR_TYPE] === undefined) { - const isError = kind === 'server' ? statusCode >= 500 : statusCode >= 400 + const isError = statusCode >= (kind === 'server' ? 500 : 400) if (isError) { newMeta[ERROR_TYPE] = status formattedSpan.error = 1 diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index 3345540218..21d6365e08 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -941,8 +941,7 @@ function getCodeOwnersFileEntries (rootDir) { const lines = codeOwnersContent.split('\n') for (const line of lines) { - const [content] = line.split('#') - const trimmed = content.trim() + const trimmed = getSegment(line, '#', 0).trim() if (trimmed === '') continue const [pattern, ...owners] = trimmed.split(/\s+/) entries.push(setCodeOwnersPatternRegex({ pattern, owners })) @@ -988,9 +987,7 @@ function codeOwnersPatternToRegexSource (pattern) { if (character === '\\') { const escapedCharacter = pattern[i + 1] - source += escapedCharacter === undefined - ? escapeRegexCharacter(character) - : escapeRegexCharacter(escapedCharacter) + source += escapeRegexCharacter(escapedCharacter ?? character) i++ } else if (character === '*') { if (pattern[i + 1] === '*') { @@ -1184,7 +1181,7 @@ function getCoveredFilenamesFromCoverage (coverage) { } function getCoverageMap (coverage) { - if (coverage?.files && coverage?.fileCoverageFor) { + if (coverage?.files && coverage.fileCoverageFor) { return coverage } return istanbul.createCoverageMap(coverage) @@ -1917,8 +1914,8 @@ function recordAttemptToFixExecution (attemptToFixExecutions, execution) { } result.executions++ - result.isDisabled = result.isDisabled || !!isDisabled - result.isQuarantined = result.isQuarantined || !!isQuarantined + result.isDisabled ||= !!isDisabled + result.isQuarantined ||= !!isQuarantined if (status === 'fail') { result.failedCount++ diff --git a/packages/dd-trace/src/priority_sampler.js b/packages/dd-trace/src/priority_sampler.js index c883158e77..c46e166e33 100644 --- a/packages/dd-trace/src/priority_sampler.js +++ b/packages/dd-trace/src/priority_sampler.js @@ -140,7 +140,7 @@ class PrioritySampler { samplers[key] = new Sampler(rates[key]) } - samplers[DEFAULT_KEY] = samplers[DEFAULT_KEY] || defaultSampler + samplers[DEFAULT_KEY] ||= defaultSampler this._samplers = samplers @@ -257,8 +257,11 @@ class PrioritySampler { context._trace[SAMPLING_RULE_DECISION] = rule.sampleRate context._trace.tags[SAMPLING_KNUTH_RATE] = formatKnuthRate(rule.sampleRate) context._sampling.mechanism = SAMPLING_MECHANISM_RULE - if (rule.provenance === 'customer') context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER - if (rule.provenance === 'dynamic') context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC + if (rule.provenance === 'customer') { + context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_USER + } else if (rule.provenance === 'dynamic') { + context._sampling.mechanism = SAMPLING_MECHANISM_REMOTE_DYNAMIC + } return rule.sample(context) && this._isSampledByRateLimit(context) ? USER_KEEP diff --git a/packages/dd-trace/src/profiling/profilers/poisson.js b/packages/dd-trace/src/profiling/profilers/poisson.js index aad121b7ec..2adf3a1875 100644 --- a/packages/dd-trace/src/profiling/profilers/poisson.js +++ b/packages/dd-trace/src/profiling/profilers/poisson.js @@ -6,7 +6,7 @@ class PoissonProcessSamplingFilter { #samplingInterval #resetInterval #now - #lastNow = Number.NEGATIVE_INFINITY + #lastNow = -Infinity #samplingInstantCount = 0 constructor ({ samplingInterval, now, resetInterval }) { diff --git a/packages/dd-trace/src/profiling/profilers/shared.js b/packages/dd-trace/src/profiling/profilers/shared.js index 9fbe37e066..916194099f 100644 --- a/packages/dd-trace/src/profiling/profilers/shared.js +++ b/packages/dd-trace/src/profiling/profilers/shared.js @@ -17,8 +17,8 @@ function getThreadLabels () { const nativeThreadId = pprof.getNativeThreadId() return { [THREAD_NAME_LABEL]: eventLoopThreadName, - [THREAD_ID_LABEL]: `${threadId}`, - [OS_THREAD_ID_LABEL]: `${nativeThreadId}`, + [THREAD_ID_LABEL]: threadId.toString(), + [OS_THREAD_ID_LABEL]: nativeThreadId.toString(), } } diff --git a/packages/dd-trace/src/util.js b/packages/dd-trace/src/util.js index 582bfafc06..cc7fbe4198 100644 --- a/packages/dd-trace/src/util.js +++ b/packages/dd-trace/src/util.js @@ -102,6 +102,21 @@ function getSegment (string, separator, index, fallback) { return end === -1 ? string.slice(start) : string.slice(start, end) } +/** + * Return the path portion of a request target, dropping any query string and fragment. + * The two scans avoid the array and substring a `split(/[?#]/)` allocates per call. + * + * @param {string} target + */ +function stripQueryAndFragment (target) { + let cut = target.indexOf('?') + const fragment = target.indexOf('#') + if (cut === -1 || (fragment !== -1 && fragment < cut)) { + cut = fragment + } + return cut === -1 ? target : target.slice(0, cut) +} + function calculateDDBasePath (dirname) { const dirSteps = dirname.split(path.sep) const packagesIndex = dirSteps.lastIndexOf('packages') @@ -136,6 +151,7 @@ module.exports = { isError, globMatch, getSegment, + stripQueryAndFragment, ddBasePath: globalThis.__DD_ESBUILD_BASEPATH || calculateDDBasePath(__dirname), normalizePluginEnvName, formatKnuthRate, diff --git a/packages/dd-trace/test/datastreams/encoding.spec.js b/packages/dd-trace/test/datastreams/encoding.spec.js index 19fba5176f..408c8853c2 100644 --- a/packages/dd-trace/test/datastreams/encoding.spec.js +++ b/packages/dd-trace/test/datastreams/encoding.spec.js @@ -37,6 +37,41 @@ describe('encoding', () => { assert.strictEqual(bytes2.length, 0) }) + it('encoding then decoding should be a no op at the largest encodable magnitude', () => { + const n = Math.floor(Number.MAX_SAFE_INTEGER / 2) + + const encoded = encodeVarint(n) + assert.deepStrictEqual([...encoded], [254, 255, 255, 255, 255, 255, 255, 15]) + const [decoded, bytes] = decodeVarint(encoded) + assert.strictEqual(decoded, n) + assert.strictEqual(bytes.length, 0) + + const encodedNegative = encodeVarint(-n) + assert.deepStrictEqual([...encodedNegative], [255, 255, 255, 255, 255, 255, 255, 15]) + const [decodedNegative, negativeBytes] = decodeVarint(encodedNegative) + assert.strictEqual(decodedNegative, -n) + assert.strictEqual(negativeBytes.length, 0) + }) + + it('encoding then decoding should be a no op when a group shifts down to 0x80', () => { + // The zig-zag value shifts down to exactly 0x80 for `2 ** exponent` and the + // `2 ** (exponent - 7)` values above it. Math.floor(Number.MAX_SAFE_INTEGER / 2) + // spans seven 7-bit groups, so there are seven such bands. + for (let exponent = 6; exponent <= 48; exponent += 7) { + const base = 2 ** exponent + const bandTop = base + 2 ** Math.max(exponent - 7, 0) - 1 + + for (const n of new Set([base, bandTop, -base, -bandTop])) { + const encoded = encodeVarint(n) + const [decoded, bytes] = decodeVarint(encoded) + assert.strictEqual(decoded, n) + assert.strictEqual(bytes.length, 0) + } + } + + assert.deepStrictEqual([...encodeVarint(2 ** 6)], [128, 1]) + }) + it('encoding a number bigger than Max safe int fails.', () => { const n = Number.MAX_SAFE_INTEGER + 10 const encoded = encodeVarint(n) diff --git a/packages/dd-trace/test/exporters/electron/exporter.spec.js b/packages/dd-trace/test/exporters/electron/exporter.spec.js index 185ba2449a..f63b5b3ba1 100644 --- a/packages/dd-trace/test/exporters/electron/exporter.spec.js +++ b/packages/dd-trace/test/exporters/electron/exporter.spec.js @@ -80,10 +80,11 @@ describe('ElectronExporter', () => { sinon.assert.notCalled(traceChannel.publish) }) - it('should not publish when there are no subscribers', () => { + it('should not buffer when there are no subscribers', () => { traceChannel.hasSubscribers = false exporter.export([span]) + traceChannel.hasSubscribers = true exporter.flush() sinon.assert.notCalled(traceChannel.publish) diff --git a/packages/dd-trace/test/plugins/util/test.spec.js b/packages/dd-trace/test/plugins/util/test.spec.js index df98f2c7c5..dde0a063a9 100644 --- a/packages/dd-trace/test/plugins/util/test.spec.js +++ b/packages/dd-trace/test/plugins/util/test.spec.js @@ -761,6 +761,11 @@ describe('getCodeOwnersForFilename', () => { matches: ['file1.js', 'packages/dd-trace/fileA.js'], misses: ['file10.js', 'packages/dd-trace/file/name.js'], }, + { + pattern: String.raw`file\*.js`, + matches: ['file*.js', 'packages/dd-trace/file*.js'], + misses: ['file1.js', 'packages/dd-trace/fileA.js'], + }, ] for (const { pattern, matches = [], misses = [] } of patternTests) { @@ -777,6 +782,14 @@ describe('getCodeOwnersForFilename', () => { } }) + it('treats a trailing backslash as a literal instead of throwing', () => { + const codeOwnersFileEntries = [ + { pattern: 'docs\\', owners: ['@datadog-docs'] }, + ] + + assert.strictEqual(getCodeOwnersForFilename('docs/README.md', codeOwnersFileEntries), null) + }) + it('keeps CODEOWNERS matching case-sensitive', () => { const codeOwnersFileEntries = [ { pattern: '/Docs/', owners: ['@datadog-docs'] }, diff --git a/packages/dd-trace/test/util.spec.js b/packages/dd-trace/test/util.spec.js index 72e664613c..daeef948b3 100644 --- a/packages/dd-trace/test/util.spec.js +++ b/packages/dd-trace/test/util.spec.js @@ -5,7 +5,7 @@ const assert = require('node:assert/strict') const { describe, it } = require('mocha') require('./setup/core') -const { isEmpty, isTrue, isFalse, globMatch, getSegment } = require('../src/util') +const { isEmpty, isTrue, isFalse, globMatch, getSegment, stripQueryAndFragment } = require('../src/util') const TRUES = [ 1, @@ -120,4 +120,31 @@ describe('util', () => { assert.strictEqual(getSegment('whole', '.', 0), 'whole') }) }) + + describe('stripQueryAndFragment', () => { + const cases = [ + '/api/v1/users', + '/api/v1/users?page=2&limit=50', + '/search?q=hello+world#results', + '/docs#section-3', + '/docs#section?not-a-query', + '/redirect?to=/other%3Fnested#frag', + '/?only-query', + '/#only-fragment', + '?leading-query', + '#leading-fragment', + '/', + '', + ] + + it('matches splitting on the query and fragment delimiters', () => { + for (const target of cases) { + assert.strictEqual( + stripQueryAndFragment(target), + target.split(/[?#]/)[0], + `stripQueryAndFragment(${JSON.stringify(target)})` + ) + } + }) + }) }) diff --git a/scripts/agentless-stress-test.js b/scripts/agentless-stress-test.js index 181aee772b..6b043c56b6 100644 --- a/scripts/agentless-stress-test.js +++ b/scripts/agentless-stress-test.js @@ -21,9 +21,9 @@ if (!process.env.DD_API_KEY) { } process.env._DD_APM_TRACING_AGENTLESS_ENABLED = 'true' -process.env.DD_TRACE_DEBUG = process.env.DD_TRACE_DEBUG || 'false' -process.env.DD_ENV = process.env.DD_ENV || 'agentless-stress-test' -process.env.DD_SERVICE = process.env.DD_SERVICE || 'agentless-stress-test' +process.env.DD_TRACE_DEBUG ||= 'false' +process.env.DD_ENV ||= 'agentless-stress-test' +process.env.DD_SERVICE ||= 'agentless-stress-test' process.env.DD_TRACE_FLUSH_INTERVAL = '2000' const tracer = require('../packages/dd-trace').init() diff --git a/scripts/generate-config-types.js b/scripts/generate-config-types.js index 6376b8c86e..235a07a1f6 100644 --- a/scripts/generate-config-types.js +++ b/scripts/generate-config-types.js @@ -260,7 +260,7 @@ function generateEnvVarConfigTypes (supportedConfigurations) { } } - const body = [...envVarTypes.entries()] + const body = [...envVarTypes] .sort(([left], [right]) => left.localeCompare(right)) .map(([name, type]) => ` ${renderPropertyName(name)}: ${type};`) .join('\n') diff --git a/scripts/release/changelog.spec.js b/scripts/release/changelog.spec.js index 75934f7b6e..cc6c7376cb 100644 --- a/scripts/release/changelog.spec.js +++ b/scripts/release/changelog.spec.js @@ -95,7 +95,7 @@ describe('release changelog', () => { '- **AppSec:** Avoid the possibility of sensitive data going to the telemetry logs backend ' + `via WAF strings ${prLink(3884)}`, '- **AppSec:** Treat cleared shared memory as no-config rather than an error in AppSec helper ' + - `${prLink(3876)}`, + prLink(3876), `- **General:** Encoder JSON number type fix ${prLink(38_799)}`, '- **LLM Observability:** Revert "Fan array-valued user tags out into one wire entry per element ' + `(${prLink(8689)})" ${prLink(8790)}`, diff --git a/scripts/release/proposal.js b/scripts/release/proposal.js index 6cef054ca9..da73e83c82 100644 --- a/scripts/release/proposal.js +++ b/scripts/release/proposal.js @@ -112,10 +112,11 @@ try { // the capped commits actually included in the proposal, not deferred ones. // Excludes changes that are listed in the dedicated breaking changes section. const notesShas = capture(`${notesDiffCmd} --format=sha --reverse v${releaseLine}.x ${upperBoundRef}`) - .split('\n').filter(Boolean) + .split('\n') const contributorBySha = getContributorsBySha(`v${releaseLine}.x`, upperBoundSha) const notesEntries = [] for (const sha of notesShas) { + if (!sha) continue notesEntries.push({ sha, subject: capture(`git show -s --format=%s ${sha}`), diff --git a/scripts/verify-exercised-tests.js b/scripts/verify-exercised-tests.js index 0b72d6e440..ca71b6f974 100644 --- a/scripts/verify-exercised-tests.js +++ b/scripts/verify-exercised-tests.js @@ -1112,6 +1112,7 @@ function main () { const packageJsonPath = path.join(repoRoot, 'package.json') const pluginsVar = '$' + '{PLUGINS}' const bracePluginsVar = '{' + pluginsVar + '}' + // eslint-disable-next-line unicorn/no-useless-concat -- Keep the GitHub expression marker non-contiguous. const ghaExprStart = '$' + '{{' /** @type {{ scripts?: Record }} */ diff --git a/scripts/verify-workflow-job-names.js b/scripts/verify-workflow-job-names.js index 77b4b3ba4b..f95eff3716 100644 --- a/scripts/verify-workflow-job-names.js +++ b/scripts/verify-workflow-job-names.js @@ -141,7 +141,7 @@ for (const jobRecord of allJobRecords) { workflowNameToWorkflowFiles.set(jobRecord.workflowName, workflowFileSet) } -const workflowNameEntries = [...workflowNameToWorkflowFiles.entries()] +const workflowNameEntries = [...workflowNameToWorkflowFiles] workflowNameEntries.sort((first, second) => first[0].localeCompare(second[0])) for (const [workflowName, workflowFileSet] of workflowNameEntries) { @@ -168,7 +168,7 @@ for (const jobRecord of allJobRecords) { checkNameToJobRecords.set(jobRecord.checkName, jobRecordList) } -const duplicateCheckNameGroups = [...checkNameToJobRecords.entries()].filter(([, jobRecordList]) => { +const duplicateCheckNameGroups = [...checkNameToJobRecords].filter(([, jobRecordList]) => { return jobRecordList.length > 1 }) duplicateCheckNameGroups.sort((first, second) => { diff --git a/yarn.lock b/yarn.lock index e2cddd5d37..c12784fd98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -378,6 +378,14 @@ dependencies: "@types/json-schema" "^7.0.15" +"@eslint/css-tree@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@eslint/css-tree/-/css-tree-4.0.4.tgz#5400c64acf00468d262b5b08c938a3fa8f6a73c4" + integrity sha512-nxMparyhqVWQvadx9x8dIfubfIPOE+X2b2waua8fzdnM9vdp9rgVtwEZlG0TmCwEUz/d/f40fzvO/eqBwdxz0A== + dependencies: + mdn-data "2.28.1" + source-map-js "^1.2.1" + "@eslint/eslintrc@^3.3.5": version "3.3.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60" @@ -1453,7 +1461,7 @@ browser-stdout@^1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.2: +browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.4: version "4.28.7" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.7.tgz#409046517fccd2e51cdc20f077454b7141184028" integrity sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw== @@ -1722,6 +1730,11 @@ content-type@^2.0.0: resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df" integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ== +convert-hrtime@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/convert-hrtime/-/convert-hrtime-5.0.0.tgz#f2131236d4598b95de856926a67100a0a97e9fa3" + integrity sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg== + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -1884,6 +1897,11 @@ enhanced-resolve@^5.17.1: graceful-fs "^4.2.4" tapable "^2.3.0" +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -2050,27 +2068,31 @@ eslint-plugin-sonarjs@^4.2.0: typescript ">=5 <6.1.0" yaml "^2.9.0" -eslint-plugin-unicorn@^68.0.0: - version "68.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-68.0.0.tgz#2d9f9a3035609a65a17ee227ac414b23a631e14e" - integrity sha512-mHYWvX948Q4H3bGc39bsNMxD/leOuNI+Iws9NVsoSz5VA7EGP86wnz7mZ/SPSvRhefT8L4hd8DHfDuGC+lIoCQ== +eslint-plugin-unicorn@^72.0.0: + version "72.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-72.0.0.tgz#3d8caf428fd84c02457e6f152128fd1944ce19a9" + integrity sha512-hqO6ksoOHO+ZhdseTuKRVQbx9U7PRO/cv8qAR1mctwzdVO2hYud8uS9luAhp43RJgziYgHAph8eHyipT8GL0ng== dependencies: - "@babel/helper-validator-identifier" "^7.29.7" "@eslint-community/eslint-utils" "^4.9.1" - browserslist "^4.28.2" + "@eslint/css-tree" "^4.0.4" + browserslist "^4.28.4" change-case "^5.4.4" ci-info "^4.4.0" core-js-compat "^3.49.0" detect-indent "^7.0.2" + entities "^4.5.0" find-up-simple "^1.0.1" - globals "^17.6.0" + globals "^17.7.0" indent-string "^5.0.0" is-builtin-module "^5.0.0" - jsesc "^3.1.0" + is-identifier "^1.1.0" pluralize "^8.0.0" - regjsparser "^0.13.1" - semver "^7.8.4" + quote-js-string "^0.1.0" + regjsparser "^0.13.2" + reserved-identifiers "^1.2.0" + semver "^7.8.5" strip-indent "^4.1.1" + yaml "^2.9.0" eslint-scope@^9.1.2: version "9.1.2" @@ -2376,6 +2398,11 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function-timeout@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/function-timeout/-/function-timeout-1.0.2.tgz#e5a7b6ffa523756ff20e1231bbe37b5f373aadd5" + integrity sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -2470,7 +2497,7 @@ globals@^15.11.0, globals@^15.14.0: resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8" integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg== -globals@^17.6.0, globals@^17.7.0: +globals@^17.7.0: version "17.7.0" resolved "https://registry.yarnpkg.com/globals/-/globals-17.7.0.tgz#553d55090b4dde8209ec2da42580d6e7e7d8b10d" integrity sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg== @@ -2581,6 +2608,13 @@ iconv-lite@^0.7.2, iconv-lite@~0.7.0: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +identifier-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/identifier-regex/-/identifier-regex-1.1.0.tgz#042abfaf28db15223436661f3b9ec882649f6ef0" + integrity sha512-SLX4H/vtcYlYnL7XqnuJKHU7Z8517TgsW9nmQiGOgMCjQ8V/deLYu6bEmbGoXe7WMMhc9+EUGyFFneHja8KabA== + dependencies: + reserved-identifiers "^1.0.0" + ignore@^5.2.0, ignore@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -2669,6 +2703,14 @@ is-glob@^4.0.0, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" +is-identifier@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-identifier/-/is-identifier-1.1.0.tgz#6b406e15a429f7196f6496e09f8333bcb4b2db1b" + integrity sha512-NhOds0mDx9lJu+1lBRO0xbwFo5nobA7GCk/0e5xjr6+6XugX985+0OyGX35BNrTkPAsdLcIKg02HUQJOK8D8kw== + dependencies: + identifier-regex "^1.1.0" + super-regex "^1.1.0" + is-node-process@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" @@ -2824,7 +2866,7 @@ jsdoc-type-pratt-parser@~7.2.0: resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz#0a29c27bd4e01e85e4617625e34e797be1486a9b" integrity sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw== -jsesc@^3.0.2, jsesc@^3.1.0, jsesc@~3.1.0: +jsesc@^3.0.2, jsesc@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== @@ -2955,6 +2997,15 @@ lru-cache@^7.14.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +make-asynchronous@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-asynchronous/-/make-asynchronous-1.1.0.tgz#6225f7f1ccaab9acaac5e2fcd0b075afefff19aa" + integrity sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg== + dependencies: + p-event "^6.0.0" + type-fest "^4.6.0" + web-worker "^1.5.0" + make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -2983,6 +3034,11 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" +mdn-data@2.28.1: + version "2.28.1" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.28.1.tgz#798ed0511d4097c22b091718435059fdb0a64ad9" + integrity sha512-U9w+PzSZ00Z5m9rZ5ARVFL5xOfuCHdKYi/1RRwDCJsboFgJDNT3zT6PIPD7mZQYaQLhsZM3GfDRgSMRHhSmVng== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -3325,6 +3381,13 @@ oxc-parser@^0.132.0: "@oxc-parser/binding-win32-ia32-msvc" "0.132.0" "@oxc-parser/binding-win32-x64-msvc" "0.132.0" +p-event@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-6.0.1.tgz#8f62a1e3616d4bc01fce3abda127e0383ef4715b" + integrity sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w== + dependencies: + p-timeout "^6.1.2" + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -3367,6 +3430,11 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-timeout@^6.1.2: + version "6.1.4" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.1.4.tgz#418e1f4dd833fa96a2e3f532547dd2abdb08dbc2" + integrity sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -3540,6 +3608,11 @@ qs@^6.14.0, qs@^6.15.2: dependencies: side-channel "^1.1.0" +quote-js-string@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/quote-js-string/-/quote-js-string-0.1.0.tgz#812aef957a2dfb31d32f0d9e9662fe558bc9842b" + integrity sha512-Y3NoRtprEEZQD8RfxMCfS0ZTqc4e+i18OrXEXAvpM6TfC/3y+0L5rNbZiSnbBBEkDfFzbpd8o+cE8q3/anjMGA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -3604,7 +3677,7 @@ regexp-ast-analysis@^0.7.0: "@eslint-community/regexpp" "^4.8.0" refa "^0.12.1" -regjsparser@^0.13.1: +regjsparser@^0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.2.tgz#f654734b5c588b22ba3e21693b30523417180808" integrity sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ== @@ -3628,7 +3701,7 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -reserved-identifiers@^1.0.0: +reserved-identifiers@^1.0.0, reserved-identifiers@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz#d2982cd698e317dd3dced1ee1c52412dbd64fc64" integrity sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw== @@ -3715,7 +3788,7 @@ semver@^6.0.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.0, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.2, semver@^7.8.4, semver@^7.8.5: +semver@^7.5.0, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3, semver@^7.7.2, semver@^7.8.5: version "7.8.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== @@ -3841,6 +3914,11 @@ sinon@^22.0.0: "@sinonjs/samsam" "^10.0.2" diff "^9.0.0" +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-support@^0.5.21: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -4006,6 +4084,15 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +super-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/super-regex/-/super-regex-1.1.0.tgz#14b69b6374f7b3338db52ecd511dae97c27acf75" + integrity sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ== + dependencies: + function-timeout "^1.0.1" + make-asynchronous "^1.0.1" + time-span "^5.1.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -4055,6 +4142,13 @@ tiktoken@^1.0.21: resolved "https://registry.yarnpkg.com/tiktoken/-/tiktoken-1.0.22.tgz#a6c674839228bb88f32dfe646dff47193762f7d3" integrity sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA== +time-span@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/time-span/-/time-span-5.1.0.tgz#80c76cf5a0ca28e0842d3f10a4e99034ce94b90d" + integrity sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA== + dependencies: + convert-hrtime "^5.0.0" + to-valid-identifier@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz#27337955333c3c517feb60bea533cf27ce54b79f" @@ -4115,6 +4209,11 @@ type-fest@^0.8.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.6.0: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + type-is@^1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -4243,6 +4342,11 @@ vary@^1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +web-worker@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5" + integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From ef0ee0b58bd11bcf4a6dd20f195f61d2662d253f Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Mon, 27 Jul 2026 16:14:06 -0400 Subject: [PATCH 02/43] feat(startup-log): report OTLP export status (#9517) Add three boolean fields to the DATADOG TRACER CONFIGURATION startup log indicating whether the tracer exports each telemetry signal over OTLP: - otlp_traces_export_enabled (OTEL_TRACES_EXPORTER === 'otlp', excluding Test Optimization mode, which keeps test spans on the citestcycle endpoint) - otlp_metrics_export_enabled (DD_METRICS_OTEL_ENABLED) - otlp_logs_export_enabled (DD_LOGS_OTEL_ENABLED) The snake_case keys are chosen for cross-language startup-log consistency. --- packages/dd-trace/src/startup-log.js | 3 + packages/dd-trace/test/startup-log.spec.js | 75 +++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/dd-trace/src/startup-log.js b/packages/dd-trace/src/startup-log.js index 7e74e3d508..52fafff7f5 100644 --- a/packages/dd-trace/src/startup-log.js +++ b/packages/dd-trace/src/startup-log.js @@ -108,6 +108,9 @@ function configInfo () { profiling_enabled: profilingEnabled === 'true' || profilingEnabled === 'auto', appsec_enabled: config.appsec.enabled, data_streams_enabled: !!config.dsmEnabled, + otlp_traces_export_enabled: config.OTEL_TRACES_EXPORTER === 'otlp' && !config.isCiVisibility, + otlp_metrics_export_enabled: !!config.DD_METRICS_OTEL_ENABLED, + otlp_logs_export_enabled: !!config.DD_LOGS_OTEL_ENABLED, } } diff --git a/packages/dd-trace/test/startup-log.spec.js b/packages/dd-trace/test/startup-log.spec.js index aa0df80865..47f6a0b732 100644 --- a/packages/dd-trace/test/startup-log.spec.js +++ b/packages/dd-trace/test/startup-log.spec.js @@ -3,7 +3,7 @@ const assert = require('node:assert') const os = require('node:os') -const { describe, it, before, afterEach } = require('mocha') +const { describe, it, before, beforeEach, afterEach } = require('mocha') const sinon = require('sinon') require('./setup/core') @@ -80,6 +80,9 @@ describe('startup logging', () => { assert.strictEqual(logObj.debug, true) assert.strictEqual(logObj.appsec_enabled, true) assert.strictEqual(logObj.data_streams_enabled, true) + assert.strictEqual('otlp_traces_export_enabled' in logObj, true) + assert.strictEqual('otlp_metrics_export_enabled' in logObj, true) + assert.strictEqual('otlp_logs_export_enabled' in logObj, true) }) it('logIntegrations should output loaded integrations', () => { @@ -121,6 +124,9 @@ describe('startup logging', () => { integrations_loaded: ['http', 'fs', 'semver'], appsec_enabled: true, data_streams_enabled: true, + otlp_traces_export_enabled: false, + otlp_metrics_export_enabled: false, + otlp_logs_export_enabled: false, }) }) }) @@ -314,3 +320,70 @@ describe('profiling_enabled', () => { }) }) }) + +describe('otlp export flags', () => { + function clearOtlpEnv () { + delete process.env.OTEL_TRACES_EXPORTER + delete process.env.OTEL_METRICS_EXPORTER + delete process.env.OTEL_LOGS_EXPORTER + delete process.env.DD_METRICS_OTEL_ENABLED + delete process.env.DD_LOGS_OTEL_ENABLED + } + + // Datadog-instrumented dev shells export the OTEL_*_EXPORTER selectors: a leaked + // OTEL_TRACES_EXPORTER=otlp corrupts the default-state assertion, and OTEL_METRICS_EXPORTER=none + // makes config force DD_METRICS_OTEL_ENABLED back to false, breaking the metrics positive case. + beforeEach(clearOtlpEnv) + afterEach(clearOtlpEnv) + + function startupLogObj (configOptions) { + sinon.stub(console, 'warn') + delete require.cache[require.resolve('../src/startup-log')] + const { + setStartupLogConfig, + startupLog, + } = require('../src/startup-log') + process.env.DD_TRACE_STARTUP_LOGS = 'true' + setStartupLogConfig(getConfigFresh(configOptions)) + startupLog() + /* eslint-disable-next-line no-console */ + const warnStub = /** @type {sinon.SinonStub} */ (console.warn) + const logObj = JSON.parse(warnStub.firstCall.args[0].replace('DATADOG TRACER CONFIGURATION - ', '')) + warnStub.restore() + return logObj + } + + it('should default to false when no OTLP env vars are set', () => { + const logObj = startupLogObj() + assert.strictEqual(logObj.otlp_traces_export_enabled, false) + assert.strictEqual(logObj.otlp_metrics_export_enabled, false) + assert.strictEqual(logObj.otlp_logs_export_enabled, false) + }) + + it('otlp_traces_export_enabled should be true when OTEL_TRACES_EXPORTER is otlp', () => { + process.env.OTEL_TRACES_EXPORTER = 'otlp' + assert.strictEqual(startupLogObj().otlp_traces_export_enabled, true) + }) + + it('otlp_traces_export_enabled should be false when OTEL_TRACES_EXPORTER is none', () => { + process.env.OTEL_TRACES_EXPORTER = 'none' + assert.strictEqual(startupLogObj().otlp_traces_export_enabled, false) + }) + + it('otlp_traces_export_enabled should be false in Test Optimization mode even when exporter is otlp', () => { + // Test Optimization keeps test spans on the citestcycle endpoint, so the OTLP + // trace exporter is not used regardless of OTEL_TRACES_EXPORTER (see opentracing/tracer.js). + process.env.OTEL_TRACES_EXPORTER = 'otlp' + assert.strictEqual(startupLogObj({ isCiVisibility: true }).otlp_traces_export_enabled, false) + }) + + it('otlp_metrics_export_enabled should be true when DD_METRICS_OTEL_ENABLED is true', () => { + process.env.DD_METRICS_OTEL_ENABLED = 'true' + assert.strictEqual(startupLogObj().otlp_metrics_export_enabled, true) + }) + + it('otlp_logs_export_enabled should be true when DD_LOGS_OTEL_ENABLED is true', () => { + process.env.DD_LOGS_OTEL_ENABLED = 'true' + assert.strictEqual(startupLogObj().otlp_logs_export_enabled, true) + }) +}) From 4d5ba6cd20faffce41b518e1f2e739ab6914511f Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 27 Jul 2026 23:18:48 +0200 Subject: [PATCH 03/43] ci(scripts): speed up verify-exercised-tests and close detection gaps (#9542) * test(scripts): cover verify-exercised-tests detection The checker's failure branches are unpinned, so a rework can narrow what it detects while still exiting 0 on a healthy tree. This adds fixture checkouts that drive each branch. `main` takes the repository root as an optional argument so a fixture can stand in for the script's own checkout. * ci(scripts): answer test-file globs from one repository walk verify-exercised-tests expanded its script globs with 227 separate `globSync` traversals of the same tree. Walking once and matching the patterns in memory cuts the run from 312 ms to 178 ms, medians of ten interleaved runs on one checkout. Matching has to stay identical to the walk it replaces, so the index compiles patterns with the options `glob` derives internally. Its spec cross-checks every result against `globSync`, over hand-written pattern shapes and over the glob corpus this repository's own package.json produces. Answering with too few files would let an unexercised spec pass unseen, so the index refuses instead: an unreadable directory warns, and a `..` segment, which needs a real tree to resolve against, throws. Literal path segments are compared case-sensitively on every platform, the rule `glob` applies on Linux, so a result no longer depends on whether the developer's filesystem folds case. * ci(scripts): resolve workflow steps into one ordered event stream Two ways a suite could go unverified survived the checker, both because a job's steps were never modeled as one ordered, per-leg-resolved sequence. 1. A coverage upload placed before the suite it collects passed the check, because uploads were tracked as a per-job boolean and position never entered it. Five composite actions run a suite and upload its report, so the order that decides whether a report survives usually lives inside the action, not in the workflow. 2. `SPEC: ${{ matrix.spec }}` reached the glob unresolved, degrading `${SPEC:-*}` to `*` and making every spec in a directory look exercised by every matrix leg. Resolving it per leg surfaces packages/datadog-plugin-aws-sdk/test/base-inject-field.spec.js, which no leg selects; the serverless matrix gains the entry that runs it. One traversal now yields the `run` and `upload` events a step contributes. The second traversal that answered only "does this job upload?" is gone, and with it the missing `using: composite` guard that made it throw on a JavaScript or Docker action. Such an action is reported now, so an upload hidden inside one cannot read as "this job never uploads". --- .github/CODEOWNERS | 3 + .github/workflows/serverless.yml | 1 + package.json | 1 + scripts/helpers/test-file-index.js | 288 ++++++++++++++ scripts/helpers/test-file-index.spec.js | 366 +++++++++++++++++ scripts/verify-exercised-tests.js | 474 ++++++++++------------ scripts/verify-exercised-tests.spec.mjs | 498 ++++++++++++++++++++++++ 7 files changed, 1370 insertions(+), 261 deletions(-) create mode 100644 scripts/helpers/test-file-index.js create mode 100644 scripts/helpers/test-file-index.spec.js create mode 100644 scripts/verify-exercised-tests.spec.mjs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6d95dfeda7..8635221094 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -279,7 +279,10 @@ /scripts/generate-config-types.js @DataDog/apm-sdk-capabilities-js /scripts/generate-supported-integrations.js @DataDog/apm-sdk-capabilities-js +/scripts/helpers/test-file-index.js @DataDog/apm-sdk-capabilities-js +/scripts/helpers/test-file-index.spec.js @DataDog/apm-sdk-capabilities-js /scripts/verify-exercised-tests.js @DataDog/apm-sdk-capabilities-js +/scripts/verify-exercised-tests.spec.mjs @DataDog/apm-sdk-capabilities-js /supported_versions_output.json @DataDog/apm-sdk-capabilities-js /supported_versions_table.csv @DataDog/apm-sdk-capabilities-js diff --git a/.github/workflows/serverless.yml b/.github/workflows/serverless.yml index 63831a2223..6bc8ecc696 100644 --- a/.github/workflows/serverless.yml +++ b/.github/workflows/serverless.yml @@ -52,6 +52,7 @@ jobs: spec: - client - aws-sdk + - base-inject-field - bedrockruntime - dynamodb - eventbridge diff --git a/package.json b/package.json index ba973aabe5..bd9c64f8d8 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "istanbul-lib-report": "^3.0.0", "istanbul-reports": "^3.0.2", "jszip": "^3.10.1", + "minimatch": "^10.2.5", "mocha": "^11.7.6", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", diff --git a/scripts/helpers/test-file-index.js b/scripts/helpers/test-file-index.js new file mode 100644 index 0000000000..b5801dd19f --- /dev/null +++ b/scripts/helpers/test-file-index.js @@ -0,0 +1,288 @@ +'use strict' + +const fs = require('node:fs') +const path = require('node:path') + +const { Glob } = require('glob') +const { Minimatch } = require('minimatch') + +const TEST_FILE_GLOB = '**/*.@(spec|test).@(js|mjs|cjs)' + +// Case-insensitive because `glob` matches case-insensitively on darwin and win32. Indexing a +// `FOO.SPEC.JS` that a case-sensitive platform would reject is harmless: the pattern matcher +// applies that platform's own rules and drops it again. +const TEST_FILE_NAME = /\.(?:spec|test)\.(?:js|mjs|cjs)$/i + +// Any character that can start a wildcard, brace, class, or extglob. Over-reporting only shortens +// the literal prefix, which widens the candidate set and can never drop a match. +const PATTERN_MAGIC = /[*?[\]{}()!+@\\]/ + +// `glob` walks `..` on the real filesystem before matching. The index has no tree to walk, so it +// would answer such a pattern with too few files and report exercised specs as unexercised. +const PARENT_SEGMENT = /(?:^|\/)\.\.(?:\/|$)/ + +/** + * @param {string} repoRoot + * @returns {string[]} + */ +function walkTestFiles (repoRoot) { + /** @type {string[]} */ + const found = [] + /** @type {string[]} */ + const pending = [''] + + while (pending.length) { + const relativeDir = pending.pop() + + /** @type {import('node:fs').Dirent[]} */ + let entries + try { + entries = fs.readdirSync(path.join(repoRoot, relativeDir), { withFileTypes: true }) + } catch (error) { + // `glob` skips unreadable directories without a word. Staying silent here would shrink the + // index instead, and an unexercised spec below that directory would pass the check unseen. + process.stderr.write(`test-file-index: skipped unreadable ${relativeDir || '.'} (${error.code})\n`) + continue + } + + for (const entry of entries) { + const { name } = entry + + // `glob` runs with `dot: false`, so a dot entry can never satisfy TEST_FILE_GLOB. Every + // consumer of this index also ignores `node_modules`, so descending into it is dead work. + if (name.startsWith('.') || name === 'node_modules') continue + + const relativePath = relativeDir ? `${relativeDir}/${name}` : name + + if (entry.isDirectory()) { + pending.push(relativePath) + } else if (TEST_FILE_NAME.test(name)) { + // Symlinks are kept: `nodir: true` filters directories, not links to files. + found.push(relativePath) + } + } + } + + return found +} + +/** + * Drops `.` and empty path segments the way `glob` does while building its pattern. `minimatch` + * on a bare string keeps them and would then compare one segment too many. + * + * @param {string} pattern + * @returns {string} + */ +function normalizePattern (pattern) { + if (!pattern.includes('./') && !pattern.includes('//')) return pattern + + const segments = pattern.split('/') + const kept = [] + + for (let i = 0; i < segments.length; i++) { + const segment = segments[i] + // A leading empty segment marks an absolute pattern and a trailing one a directory-only + // pattern; both change what matches, so only interior blanks are collapsed. + if (segment === '.' || (segment === '' && i !== 0 && i !== segments.length - 1)) continue + kept.push(segment) + } + + return kept.join('/') +} + +/** + * Longest leading directory path of `pattern` that contains no wildcard. + * + * @param {string} pattern + * @returns {string} + */ +function literalPrefix (pattern) { + const magic = pattern.search(PATTERN_MAGIC) + const literal = magic === -1 ? pattern : pattern.slice(0, magic) + const lastSlash = literal.lastIndexOf('/') + return lastSlash === -1 ? '' : literal.slice(0, lastSlash + 1) +} + +/** + * In-memory stand-in for repeated `globSync` walks over the repository's test files. + * + * The repository is traversed once; every later pattern is answered from the resulting list. Only + * paths named like a test file are indexed, so a pattern that also selects non-test files reports + * just the test-file subset. + * + * Results have to stay identical to the `globSync` walk they replace; `test-file-index.spec.js` + * pins that against `glob` itself, including the repository's own pattern corpus. + */ +class TestFileIndex { + /** @type {string[]} */ + files + + /** @type {boolean} */ + #nocase + + /** @type {NodeJS.Platform} */ + #platform + + /** @type {Map>} */ + #matchers = new Map() + + /** @type {Map>} */ + #ignoreMatchers = new Map() + + /** @type {Map} */ + #scopes = new Map() + + /** @type {Map} */ + #candidates = new Map() + + /** + * @param {string[]} files Sorted repository-relative POSIX paths. + * @param {{ nocase: boolean, platform: NodeJS.Platform }} options + */ + constructor (files, { nocase, platform }) { + this.files = files + this.#nocase = nocase + this.#platform = platform + } + + /** + * A literal path segment is compared case-sensitively on every platform, which is what `glob` + * does on Linux. On a case-insensitive filesystem `glob` instead resolves `foo/BAR.spec.js` to + * the differently-spelled file on disk; reproducing that would make the result depend on the + * developer's machine, so the index keeps the Linux rule everywhere. + * + * @param {string} pattern + * @param {string[]} [ignoreGlobs] + * @throws {Error} If `pattern` contains a `..` segment, which the index cannot resolve. + * @returns {string[]} Matching paths, in the index's sort order. + */ + match (pattern, ignoreGlobs = []) { + if (PARENT_SEGMENT.test(pattern)) { + throw new Error(`test-file-index cannot resolve the '..' segment in pattern '${pattern}'`) + } + + const normalized = normalizePattern(pattern) + const scope = this.#scope(ignoreGlobs) + const matcher = this.#matcher(normalized) + + /** @type {string[]} */ + const matched = [] + for (const file of this.#candidatesFor(scope, ignoreGlobs, normalized)) { + // eslint-disable-next-line unicorn/prefer-regexp-test -- Minimatch#match, not String#match. + if (matcher.match(file)) matched.push(file) + } + + return matched + } + + /** + * @param {string[]} ignoreGlobs + * @returns {{ files: string[], folded: string[] }} + */ + #scope (ignoreGlobs) { + const key = ignoreGlobs.join('\0') + + let scope = this.#scopes.get(key) + if (scope === undefined) { + const matchers = ignoreGlobs.map(glob => this.#ignoreMatcher(glob)) + const files = matchers.length === 0 + ? this.files + : this.files.filter(file => !matchers.some(matcher => matcher.match(file) || matcher.match(`${file}/`))) + + scope = { files, folded: this.#nocase ? files.map(file => file.toLowerCase()) : files } + this.#scopes.set(key, scope) + } + + return scope + } + + /** + * @param {{ files: string[], folded: string[] }} scope + * @param {string[]} ignoreGlobs + * @param {string} pattern + * @returns {string[]} + */ + #candidatesFor (scope, ignoreGlobs, pattern) { + const prefix = literalPrefix(pattern) + if (prefix === '') return scope.files + + const key = `${ignoreGlobs.join('\0')}\0${prefix}` + + let candidates = this.#candidates.get(key) + if (candidates === undefined) { + const wanted = this.#nocase ? prefix.toLowerCase() : prefix + + candidates = [] + for (let i = 0; i < scope.files.length; i++) { + if (scope.folded[i].startsWith(wanted)) candidates.push(scope.files[i]) + } + + this.#candidates.set(key, candidates) + } + + return candidates + } + + /** + * @param {string} pattern + * @returns {InstanceType} + */ + #matcher (pattern) { + let matcher = this.#matchers.get(pattern) + if (matcher === undefined) { + matcher = new Minimatch(pattern, { + dot: false, + nocase: this.#nocase, + nocaseMagicOnly: this.#platform === 'darwin' || this.#platform === 'win32', + nocomment: true, + nonegate: true, + optimizationLevel: 2, + platform: this.#platform, + windowsPathsNoEscape: true, + }) + this.#matchers.set(pattern, matcher) + } + + return matcher + } + + /** + * @param {string} glob + * @returns {InstanceType} + */ + #ignoreMatcher (glob) { + let matcher = this.#ignoreMatchers.get(glob) + if (matcher === undefined) { + // Mirrors the options `glob`'s Ignore class builds; notably `dot: true`, so `**/.git/**` + // still matches paths that the main pattern would skip. + matcher = new Minimatch(glob, { + dot: true, + nocase: this.#nocase, + nocomment: true, + nonegate: true, + optimizationLevel: 2, + platform: this.#platform, + }) + this.#ignoreMatchers.set(glob, matcher) + } + + return matcher + } +} + +/** + * @param {string} repoRoot + * @returns {TestFileIndex} + */ +function createTestFileIndex (repoRoot) { + // `glob` decides case sensitivity from the platform's filesystem; read it back rather than + // re-deriving it here, so the index cannot drift from the walker it replaces. + const probe = new Glob(TEST_FILE_GLOB, { cwd: repoRoot, windowsPathsNoEscape: true }) + + const files = walkTestFiles(repoRoot) + files.sort((a, b) => a.localeCompare(b, 'en')) + + return new TestFileIndex(files, { nocase: probe.nocase, platform: probe.platform }) +} + +module.exports = { TEST_FILE_GLOB, TestFileIndex, createTestFileIndex } diff --git a/scripts/helpers/test-file-index.spec.js b/scripts/helpers/test-file-index.spec.js new file mode 100644 index 0000000000..339e3a8be6 --- /dev/null +++ b/scripts/helpers/test-file-index.spec.js @@ -0,0 +1,366 @@ +'use strict' + +const assert = require('node:assert/strict') +const fs = require('node:fs') +const os = require('node:os') +const path = require('node:path') + +const { globSync } = require('glob') + +const { TEST_FILE_GLOB, TestFileIndex, createTestFileIndex } = require('./test-file-index') + +const REPO_ROOT = path.resolve(__dirname, '..', '..') + +const DEFAULT_IGNORE_GLOBS = [ + '**/node_modules/**', + '**/coverage/**', + '**/.git/**', + '**/.nyc_output/**', + '**/.junit-tmp/**', + 'vendor/dist/**', +] +const NODE_MODULES_ONLY = ['**/node_modules/**'] + +const FIXTURE_FILES = [ + 'a.spec.js', + 'b.test.js', + 'c.spec.mjs', + 'd.test.mjs', + 'e.spec.cjs', + 'f.test.cjs', + 'plain.js', + 'notspec.js', + 'wrong.spec.jsx', + 'wrong.spec.ts', + '.hidden.spec.js', + '.hiddendir/inner.spec.js', + 'node_modules/pkg/dep.spec.js', + 'nested/node_modules/deep/dep.spec.js', + 'packages/alpha/test/one.spec.js', + 'packages/alpha/test/deep/two.spec.js', + 'packages/beta/test/three.spec.js', + 'packages/beta/test/integration-test/four.spec.js', + 'packages/datadog-plugin-http/test/index.spec.js', + 'packages/datadog-plugin-redis/test/index.spec.js', + 'vendor/dist/bundled.spec.js', + 'coverage/report.spec.js', + 'sub/dir/with.dots.in.name.spec.js', + 'sub/dir/UPPER.SPEC.JS', + 'integration-tests/one.spec.js', + 'integration-tests/nested/two.spec.js', +] + +const FIXTURE_PATTERNS = [ + TEST_FILE_GLOB, + '**/*.spec.js', + '**/*.test.js', + '*.spec.js', + 'packages/*/test/*.spec.js', + 'packages/*/test/**/*.spec.js', + 'packages/**/*.spec.js', + 'packages/alpha/test/one.spec.js', + 'packages/alpha/test/**/*.@(spec|test).@(js|mjs|cjs)', + 'packages/datadog-plugin-@(http|redis)/test/**/*.spec.js', + 'packages/datadog-plugin-@(http)/test/**/*.spec.js', + 'packages/datadog-plugin-*/test/**/*.spec.js', + 'packages/{alpha,beta}/test/**/*.spec.js', + 'packages/{alpha}/test/**/*.spec.js', + 'integration-tests/*.spec.js', + 'integration-tests/**/*.spec.js', + 'sub/dir/*.spec.js', + 'sub/dir/with.dots.in.name.spec.js', + 'vendor/dist/*.spec.js', + 'coverage/*.spec.js', + '**/node_modules/**/*.spec.js', + '.hiddendir/*.spec.js', + 'does/not/exist/*.spec.js', + 'packages/*/test/**/**.spec.js', + '**/*.@(spec|test).@(js|mjs|cjs)', + '**/*.{spec,test}.js', + 'packages/?????/test/*.spec.js', + 'packages/[ab]*/test/*.spec.js', + './packages/alpha/test/*.spec.js', + './/packages/alpha/test/*.spec.js', + '././packages/alpha/test/*.spec.js', + 'packages/./alpha/test/*.spec.js', + 'packages/alpha/test/', + '', +] + +/** + * @param {string} root + * @param {string[]} files + */ +function writeFixture (root, files) { + for (const file of files) { + const full = path.join(root, file) + fs.mkdirSync(path.dirname(full), { recursive: true }) + fs.writeFileSync(full, '') + } +} + +/** + * The index deliberately holds only test-named files outside `node_modules`. Expectations are + * intersected with that same set so a pattern selecting other files is compared fairly. + * + * @param {string} root + * @returns {Set} + */ +function indexableFiles (root) { + return new Set(globSync(TEST_FILE_GLOB, { + cwd: root, + nodir: true, + windowsPathsNoEscape: true, + ignore: NODE_MODULES_ONLY, + })) +} + +/** + * @param {string} root + * @param {Set} indexable + * @param {string} pattern + * @param {string[]} [ignore] + * @returns {string[]} + */ +function globExpectation (root, indexable, pattern, ignore) { + return globSync(pattern, { cwd: root, nodir: true, windowsPathsNoEscape: true, ignore }) + .filter(file => indexable.has(file)) + .sort((a, b) => a.localeCompare(b, 'en')) +} + +/** + * @param {Record} scripts + * @returns {string[]} + */ +function globTokensFromScripts (scripts) { + const tokens = new Set() + + for (const command of Object.values(scripts)) { + if (typeof command !== 'string') continue + + for (const raw of command.split(/\s+/)) { + const unquoted = raw.replaceAll(/^["']+|["']+$/g, '') + if (!unquoted.includes('/') || !/[*?[\]{}()]/.test(unquoted)) continue + + tokens.add(unquoted.replaceAll(/\$\{[^}]+\}/g, '*').replaceAll(/\$[A-Za-z_][A-Za-z0-9_]*/g, '*')) + } + } + + return [...tokens] +} + +describe('test-file-index', () => { + /** @type {string} */ + let root + /** @type {ReturnType} */ + let index + /** @type {Set} */ + let indexable + + before(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-test-file-index-')) + writeFixture(root, FIXTURE_FILES) + fs.symlinkSync(path.join(root, 'packages', 'alpha', 'test'), path.join(root, 'linked-tests'), 'dir') + + index = createTestFileIndex(root) + indexable = indexableFiles(root) + }) + + after(() => { + fs.rmSync(root, { recursive: true, force: true }) + }) + + describe('inventory', () => { + it('indexes every supported test-file extension', () => { + assert.deepStrictEqual( + index.files.filter(file => !file.includes('/')), + ['a.spec.js', 'b.test.js', 'c.spec.mjs', 'd.test.mjs', 'e.spec.cjs', 'f.test.cjs'] + ) + }) + + it('excludes files that are not named like a test', () => { + for (const file of ['plain.js', 'notspec.js', 'wrong.spec.jsx', 'wrong.spec.ts']) { + assert.ok(!index.files.includes(file), `${file} should not be indexed`) + } + }) + + it('excludes node_modules at any depth', () => { + for (const file of index.files) { + assert.ok(!file.includes('node_modules/'), `${file} should not be indexed`) + } + }) + + it('excludes dot files and dot directories', () => { + for (const file of index.files) { + assert.ok(!file.split('/').some(segment => segment.startsWith('.')), `${file} should not be indexed`) + } + }) + + it('does not descend into symlinked directories', () => { + assert.ok(!index.files.some(file => file.startsWith('linked-tests/'))) + }) + + it('returns sorted repository-relative POSIX paths', () => { + assert.deepStrictEqual(index.files, [...index.files].sort((a, b) => a.localeCompare(b, 'en'))) + assert.ok(index.files.every(file => !file.startsWith('/') && !file.includes('\\'))) + }) + + it('indexes an empty directory as no files', () => { + const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-test-file-index-empty-')) + try { + assert.deepStrictEqual(createTestFileIndex(empty).files, []) + } finally { + fs.rmSync(empty, { recursive: true, force: true }) + } + }) + + it('warns instead of silently shrinking the index when a directory cannot be read', () => { + const { write } = process.stderr + /** @type {string[]} */ + const warnings = [] + process.stderr.write = chunk => warnings.push(String(chunk)) + + try { + assert.deepStrictEqual(createTestFileIndex(path.join(os.tmpdir(), 'dd-test-file-index-missing')).files, []) + } finally { + process.stderr.write = write + } + + assert.deepStrictEqual(warnings, ['test-file-index: skipped unreadable . (ENOENT)\n']) + }) + }) + + describe('matching', () => { + for (const pattern of FIXTURE_PATTERNS) { + it(`matches globSync for ${pattern}`, () => { + assert.deepStrictEqual(index.match(pattern), globExpectation(root, indexable, pattern)) + }) + } + + for (const pattern of FIXTURE_PATTERNS) { + it(`matches globSync for ${pattern} under the default ignore list`, () => { + assert.deepStrictEqual( + index.match(pattern, DEFAULT_IGNORE_GLOBS), + globExpectation(root, indexable, pattern, DEFAULT_IGNORE_GLOBS) + ) + }) + } + + it('matches globSync for every glob token in the real package.json', () => { + const { scripts } = require(path.join(REPO_ROOT, 'package.json')) + const repoIndex = createTestFileIndex(REPO_ROOT) + const repoIndexable = indexableFiles(REPO_ROOT) + const patterns = globTokensFromScripts(scripts) + + assert.ok(patterns.length > 20, `expected a representative corpus, got ${patterns.length}`) + + for (const pattern of patterns) { + assert.deepStrictEqual( + repoIndex.match(pattern, DEFAULT_IGNORE_GLOBS), + globExpectation(REPO_ROOT, repoIndexable, pattern, DEFAULT_IGNORE_GLOBS), + `pattern ${pattern} diverged from globSync` + ) + } + }) + + it('matches globSync for the repository-wide test glob', () => { + const repoIndex = createTestFileIndex(REPO_ROOT) + + assert.deepStrictEqual( + repoIndex.match(TEST_FILE_GLOB, DEFAULT_IGNORE_GLOBS), + globSync(TEST_FILE_GLOB, { + cwd: REPO_ROOT, + nodir: true, + windowsPathsNoEscape: true, + ignore: DEFAULT_IGNORE_GLOBS, + }).sort((a, b) => a.localeCompare(b, 'en')) + ) + }) + }) + + describe('ignore lists', () => { + it('drops paths excluded by the default ignore list', () => { + const matched = index.match('**/*.spec.js', DEFAULT_IGNORE_GLOBS) + + assert.ok(!matched.includes('vendor/dist/bundled.spec.js')) + assert.ok(!matched.includes('coverage/report.spec.js')) + assert.ok(matched.includes('packages/alpha/test/one.spec.js')) + }) + + it('keeps paths a narrower ignore list allows', () => { + const matched = index.match('**/*.spec.js', NODE_MODULES_ONLY) + + assert.ok(matched.includes('vendor/dist/bundled.spec.js')) + assert.ok(matched.includes('coverage/report.spec.js')) + }) + + it('treats an empty ignore list as no filtering', () => { + assert.deepStrictEqual(index.match('**/*.spec.js'), index.match('**/*.spec.js', [])) + }) + + it('keeps results for the same pattern independent per ignore list', () => { + const wide = index.match('**/*.spec.js', NODE_MODULES_ONLY) + const narrow = index.match('**/*.spec.js', DEFAULT_IGNORE_GLOBS) + + assert.ok(wide.length > narrow.length) + assert.deepStrictEqual(index.match('**/*.spec.js', NODE_MODULES_ONLY), wide) + }) + }) + + describe('platform semantics', () => { + // `glob` reads case sensitivity off the filesystem, so a run on one platform never exercises + // the other's rules. Both are pinned here, independent of the host this suite runs on. + const files = ['sub/dir/lower.spec.js', 'sub/dir/UPPER.SPEC.JS'] + const platforms = [ + { platform: 'darwin', nocase: true }, + { platform: 'win32', nocase: true }, + { platform: 'linux', nocase: false }, + ] + const caseInsensitivePlatforms = platforms.filter(entry => entry.nocase) + + it('matches a wildcard segment case-insensitively where the filesystem is', () => { + for (const { platform } of caseInsensitivePlatforms) { + assert.deepStrictEqual(new TestFileIndex(files, { nocase: true, platform }).match('sub/dir/*.spec.js'), + files, `platform ${platform}`) + } + }) + + it('matches a wildcard segment case-sensitively on linux', () => { + const index = new TestFileIndex(files, { nocase: false, platform: 'linux' }) + assert.deepStrictEqual(index.match('sub/dir/*.spec.js'), ['sub/dir/lower.spec.js']) + }) + + it('rejects a literal segment whose case differs, on every platform', () => { + for (const { platform, nocase } of platforms) { + assert.deepStrictEqual(new TestFileIndex(files, { nocase, platform }).match('sub/dir/upper.spec.js'), + [], `platform ${platform}`) + } + }) + + it('throws on a pattern it cannot resolve rather than matching too few files', () => { + const index = new TestFileIndex(files, { nocase: false, platform: 'linux' }) + const expected = { message: /cannot resolve the '\.\.' segment/ } + + assert.throws(() => index.match('packages/../sub/dir/lower.spec.js'), expected) + assert.throws(() => index.match('../sub/*.spec.js'), expected) + assert.throws(() => index.match('sub/..'), expected) + assert.deepStrictEqual(index.match('sub/dir/with..dots.spec.js'), []) + }) + }) + + describe('repeated queries', () => { + it('returns equal results when a pattern is matched twice', () => { + for (const pattern of ['**/*.spec.js', 'packages/*/test/**/*.spec.js', 'does/not/exist/*.spec.js']) { + assert.deepStrictEqual(index.match(pattern, DEFAULT_IGNORE_GLOBS), index.match(pattern, DEFAULT_IGNORE_GLOBS)) + } + }) + + it('does not leak matches between patterns sharing a literal prefix', () => { + const one = index.match('packages/alpha/test/one.spec.js') + const deep = index.match('packages/alpha/test/deep/*.spec.js') + + assert.deepStrictEqual(one, ['packages/alpha/test/one.spec.js']) + assert.deepStrictEqual(deep, ['packages/alpha/test/deep/two.spec.js']) + }) + }) +}) diff --git a/scripts/verify-exercised-tests.js b/scripts/verify-exercised-tests.js index ca71b6f974..64fac64292 100644 --- a/scripts/verify-exercised-tests.js +++ b/scripts/verify-exercised-tests.js @@ -6,8 +6,13 @@ const path = require('node:path') const { globSync } = require('glob') const YAML = require('yaml') +const { TEST_FILE_GLOB, createTestFileIndex } = require('./helpers/test-file-index') + +/** @typedef {import('./helpers/test-file-index').TestFileIndex} TestFileIndex */ + +const NODE_MODULES_IGNORE_GLOBS = ['**/node_modules/**'] const DEFAULT_IGNORE_GLOBS = [ - '**/node_modules/**', + ...NODE_MODULES_IGNORE_GLOBS, '**/coverage/**', '**/.git/**', '**/.nyc_output/**', @@ -15,12 +20,6 @@ const DEFAULT_IGNORE_GLOBS = [ 'vendor/dist/**', ] -const GLOB_CACHE_MAX = 2000 -/** @type {Map} */ -const globCache = new Map() -let globCacheHits = 0 -let globCacheMisses = 0 - const COVERAGE_ACTIONS = new Set([ './.github/actions/coverage', './.github/actions/upload-coverage-artifact', @@ -30,36 +29,6 @@ const COVERAGE_COLLECTORS = new Set([ 'scripts/c8-ci.js', ]) -/** - * @param {string} pattern - * @param {{cwd: string, nodir: boolean, windowsPathsNoEscape: boolean, ignore?: string[]}} opts - * @returns {string[]} - */ -function globSyncCached (pattern, opts) { - const ignoreKey = Array.isArray(opts.ignore) ? opts.ignore.join('\n') : '' - const key = `${opts.cwd}\0${pattern}\0${opts.nodir ? 1 : 0}\0${opts.windowsPathsNoEscape ? 1 : 0}\0${ignoreKey}` - - const cached = globCache.get(key) - if (cached) { - globCacheHits++ - // Basic LRU: refresh insertion order. - globCache.delete(key) - globCache.set(key, cached) - return cached - } - - globCacheMisses++ - const res = globSync(pattern, opts) - - globCache.set(key, res) - if (globCache.size > GLOB_CACHE_MAX) { - const first = globCache.keys().next().value - if (first !== undefined) globCache.delete(first) - } - - return res -} - /** * @param {string} s * @returns {string} @@ -350,30 +319,23 @@ function parseExportAssignments (run) { } /** - * @param {string} repoRoot + * @param {TestFileIndex} index * @returns {string[]} */ -function findTestFiles (repoRoot) { - const commonGlobOpts = { cwd: repoRoot, nodir: true, windowsPathsNoEscape: true, ignore: DEFAULT_IGNORE_GLOBS } - +function findTestFiles (index) { // Collect every test-file naming convention used in the repo so an unrun spec // can never slip through untracked. Kept deliberately wide (js/mjs/cjs) even // where no file currently uses an extension, so a future one is caught. - const files = globSyncCached('**/*.@(spec|test).@(js|mjs|cjs)', commonGlobOpts) - - files.sort((a, b) => a.localeCompare(b, 'en')) - return files + return index.match(TEST_FILE_GLOB, DEFAULT_IGNORE_GLOBS) } /** - * @param {string} repoRoot + * @param {TestFileIndex} index * @param {Record} scripts * @param {Record} [env] * @returns {{ globs: string[], matchedFiles: Set }} */ -function expandScriptGlobs (repoRoot, scripts, env = {}) { - const ignore = DEFAULT_IGNORE_GLOBS - +function expandScriptGlobs (index, scripts, env = {}) { /** @type {string[]} */ const allGlobs = [] const matchedFiles = new Set() @@ -392,7 +354,7 @@ function expandScriptGlobs (repoRoot, scripts, env = {}) { let expanded try { - expanded = globSyncCached(normalized, { cwd: repoRoot, nodir: true, windowsPathsNoEscape: true, ignore }) + expanded = index.match(normalized, DEFAULT_IGNORE_GLOBS) } catch { // If a pattern can't be parsed by glob, skip expanding it. It still counts as an extracted glob. continue @@ -466,14 +428,14 @@ function extractScriptInvocations (run, knownScripts) { /** * Expand a script and any nested `npm run`/`yarn