From 2dacae17a4683a2cba50febfbb8309c43128afc5 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 1 Apr 2026 01:11:27 +0200 Subject: [PATCH 1/2] chore(config): use generated config types Adopt the generated internal config contract across the runtime consumers that only need stronger typing and shared config shape guarantees. This keeps the metadata-driven runtime and its internal consumers aligned separating the larger behavioral refactor. --- packages/dd-trace/src/aiguard/sdk.js | 4 + packages/dd-trace/src/appsec/blocking.js | 3 + packages/dd-trace/src/appsec/sdk/index.js | 4 + .../test-api-manual/test-api-manual-plugin.js | 4 + .../src/crashtracking/crashtracker.js | 8 +- packages/dd-trace/src/debugger/index.js | 2 +- packages/dd-trace/src/heap_snapshots.js | 3 + .../dd-trace/src/opentelemetry/logs/index.js | 2 +- .../src/opentelemetry/metrics/index.js | 2 +- .../src/payload-tagging/config/index.js | 11 +- packages/dd-trace/src/plugin_manager.js | 9 +- packages/dd-trace/src/plugins/ci_plugin.js | 4 + packages/dd-trace/src/plugins/plugin.js | 7 +- packages/dd-trace/src/process-tags/index.js | 3 + packages/dd-trace/src/profiler.js | 7 +- .../dd-trace/src/profiling/ssi-heuristics.js | 5 +- .../dd-trace/src/propagation-hash/index.js | 3 +- packages/dd-trace/src/proxy.js | 16 +- packages/dd-trace/src/remote_config/index.js | 3 + .../dd-trace/src/runtime_metrics/index.js | 3 + .../src/runtime_metrics/runtime_metrics.js | 3 + packages/dd-trace/src/sampler.js | 2 +- packages/dd-trace/src/standalone/index.js | 3 + .../config/generated-config-types.spec.js | 20 ++ packages/dd-trace/test/plugin_manager.spec.js | 53 +++-- scripts/generate-config-types.js | 217 ++++++++++++++++++ 26 files changed, 357 insertions(+), 44 deletions(-) create mode 100644 packages/dd-trace/test/config/generated-config-types.spec.js create mode 100644 scripts/generate-config-types.js diff --git a/packages/dd-trace/src/aiguard/sdk.js b/packages/dd-trace/src/aiguard/sdk.js index 64886ba092a..cbd3a486199 100644 --- a/packages/dd-trace/src/aiguard/sdk.js +++ b/packages/dd-trace/src/aiguard/sdk.js @@ -57,6 +57,10 @@ class AIGuard extends NoopAIGuard { #maxContentSize #meta + /** + * @param {import('../tracer')} tracer - Tracer instance + * @param {import('../config/config-base')} config - Tracer configuration + */ constructor (tracer, config) { super() diff --git a/packages/dd-trace/src/appsec/blocking.js b/packages/dd-trace/src/appsec/blocking.js index 3615e7ef2dc..a21aab0b76d 100644 --- a/packages/dd-trace/src/appsec/blocking.js +++ b/packages/dd-trace/src/appsec/blocking.js @@ -164,6 +164,9 @@ function getBlockingAction (actions) { return actions?.redirect_request || actions?.block_request } +/** + * @param {import('../config/config-base')} config - Tracer configuration + */ function setTemplates (config) { templateHtml = config.appsec.blockedTemplateHtml || blockedTemplates.html diff --git a/packages/dd-trace/src/appsec/sdk/index.js b/packages/dd-trace/src/appsec/sdk/index.js index 1b07e25c902..499079c2b4f 100644 --- a/packages/dd-trace/src/appsec/sdk/index.js +++ b/packages/dd-trace/src/appsec/sdk/index.js @@ -26,6 +26,10 @@ class EventTrackingV2 { } class AppsecSdk { + /** + * @param {import('../../tracer')} tracer - Tracer instance + * @param {import('../../config/config-base')} config - Tracer configuration + */ constructor (tracer, config) { this._tracer = tracer if (config) { diff --git a/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js b/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js index 90602ba0a1a..c6883eb0c8d 100644 --- a/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +++ b/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js @@ -54,6 +54,10 @@ class TestApiManualPlugin extends CiPlugin { }) } + /** + * @param {import('../../config/config-base')} config - Tracer configuration + * @param {boolean} shouldGetEnvironmentData - Whether to get environment data + */ configure (config, shouldGetEnvironmentData) { this._config = config super.configure(config, shouldGetEnvironmentData) diff --git a/packages/dd-trace/src/crashtracking/crashtracker.js b/packages/dd-trace/src/crashtracking/crashtracker.js index 1fd2a822fb6..10b02988dc2 100644 --- a/packages/dd-trace/src/crashtracking/crashtracker.js +++ b/packages/dd-trace/src/crashtracking/crashtracker.js @@ -23,6 +23,9 @@ class Crashtracker { } } + /** + * @param {import('../config/config-base')} config - Tracer configuration + */ start (config) { if (this.#started) return this.configure(config) @@ -35,7 +38,7 @@ class Crashtracker { this.#getMetadata(config) ) } catch (e) { - log.error('Error initialising crashtracker', e) + log.error('Error initializing crashtracker', e) } } @@ -49,6 +52,9 @@ class Crashtracker { } // TODO: Send only configured values when defaults are fixed. + /** + * @param {import('../config/config-base')} config - Tracer configuration + */ #getConfig (config) { const url = getAgentUrl(config) diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index fe70f9dc5c2..9f3e750702e 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -147,7 +147,7 @@ function start (config, rcInstance) { * Sends the new configuration to the worker thread via the config channel. * Does nothing if the worker is not started. * - * @param {Config} config - The updated tracer configuration object + * @param {import('../config/config-base')} config - The updated tracer configuration object */ function configure (config) { if (configChannel === null) return diff --git a/packages/dd-trace/src/heap_snapshots.js b/packages/dd-trace/src/heap_snapshots.js index 35360d892c0..19ef1206a69 100644 --- a/packages/dd-trace/src/heap_snapshots.js +++ b/packages/dd-trace/src/heap_snapshots.js @@ -45,6 +45,9 @@ function getName (destination) { } module.exports = { + /** + * @param {import('./config/config-base')} config - Tracer configuration + */ async start (config) { const destination = config.heapSnapshot.destination diff --git a/packages/dd-trace/src/opentelemetry/logs/index.js b/packages/dd-trace/src/opentelemetry/logs/index.js index 2d9ec8c71d7..a36446d7dbe 100644 --- a/packages/dd-trace/src/opentelemetry/logs/index.js +++ b/packages/dd-trace/src/opentelemetry/logs/index.js @@ -33,7 +33,7 @@ const OtlpHttpLogExporter = require('./otlp_http_log_exporter') /** * Initializes OpenTelemetry Logs support - * @param {Config} config - Tracer configuration instance + * @param {import('../../config/config-base')} config - Tracer configuration instance */ function initializeOpenTelemetryLogs (config) { // Build resource attributes diff --git a/packages/dd-trace/src/opentelemetry/metrics/index.js b/packages/dd-trace/src/opentelemetry/metrics/index.js index c0d116e2075..914baeee330 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/index.js +++ b/packages/dd-trace/src/opentelemetry/metrics/index.js @@ -35,7 +35,7 @@ const OtlpHttpMetricExporter = require('./otlp_http_metric_exporter') /** * Initializes OpenTelemetry Metrics support - * @param {Config} config - Tracer configuration instance + * @param {import('../../config/config-base')} config - Tracer configuration instance */ function initializeOpenTelemetryMetrics (config) { const resourceAttributes = { diff --git a/packages/dd-trace/src/payload-tagging/config/index.js b/packages/dd-trace/src/payload-tagging/config/index.js index 1f91dd9d6e7..c103349ca8b 100644 --- a/packages/dd-trace/src/payload-tagging/config/index.js +++ b/packages/dd-trace/src/payload-tagging/config/index.js @@ -3,16 +3,17 @@ const aws = require('./aws.json') const sdks = { aws } +/** @typedef {Record} SDKRules */ /** * Builds rules per service for a given SDK, appending user-provided rules. * - * @param {Record} sdk + * @param {SDKRules} sdk * @param {string[]} requestInput * @param {string[]} responseInput - * @returns {Record} + * @returns {SDKRules} */ function getSDKRules (sdk, requestInput, responseInput) { - const sdkServiceRules = {} + const sdkServiceRules = /** @type {SDKRules} */ ({}) for (const [service, serviceRules] of Object.entries(sdk)) { sdkServiceRules[service] = { // Make a copy. Otherwise calling the function multiple times would append @@ -31,10 +32,10 @@ function getSDKRules (sdk, requestInput, responseInput) { * * @param {string[]} [requestInput=[]] * @param {string[]} [responseInput=[]] - * @returns {Record>} + * @returns {Record} */ function appendRules (requestInput = [], responseInput = []) { - const sdkRules = {} + const sdkRules = /** @type {Record} */ ({}) for (const [name, sdk] of Object.entries(sdks)) { sdkRules[name] = getSDKRules(sdk, requestInput, responseInput) } diff --git a/packages/dd-trace/src/plugin_manager.js b/packages/dd-trace/src/plugin_manager.js index 2bf92b390c6..5b78fb048f5 100644 --- a/packages/dd-trace/src/plugin_manager.js +++ b/packages/dd-trace/src/plugin_manager.js @@ -67,7 +67,7 @@ function getEnabled (Plugin) { module.exports = class PluginManager { constructor (tracer) { this._tracer = tracer - this._tracerConfig = null + this._tracerConfig = /** @type {import('./config/config-base')} */ (null) this._pluginsByName = {} this._configsByName = {} @@ -121,8 +121,11 @@ module.exports = class PluginManager { this.loadPlugin(name) } - // like instrumenter.enable() - configure (config = {}) { + /** + * Like instrumenter.enable() + * @param {import('./config/config-base')} config - Tracer configuration + */ + configure (config) { this._tracerConfig = config this._tracer._nomenclature.configure(config) diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index 9008186c107..d318174bf61 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -469,6 +469,10 @@ module.exports = class CiPlugin extends Plugin { return getSessionRequestErrorTags(this.testSessionSpan) } + /** + * @param {import('../config/config-base')} config - Tracer configuration + * @param {boolean} shouldGetEnvironmentData - Whether to get environment data + */ configure (config, shouldGetEnvironmentData = true) { super.configure(config) diff --git a/packages/dd-trace/src/plugins/plugin.js b/packages/dd-trace/src/plugins/plugin.js index 0f12da1d81c..7b6a565288d 100644 --- a/packages/dd-trace/src/plugins/plugin.js +++ b/packages/dd-trace/src/plugins/plugin.js @@ -163,9 +163,10 @@ module.exports = class Plugin { /** * Enable or disable the plugin and (re)apply its configuration. * - * @param {boolean|object} config Either a boolean to enable/disable or a configuration object - * containing at least `{ enabled: boolean }`. - * @returns {void} + * TODO: Remove the overloading with `enabled` and use the config object directly. + * + * @param {boolean|import('../config/config-base')} config Either a boolean to enable/disable + * or a configuration object containing at least `{ enabled: boolean }`. */ configure (config) { if (typeof config === 'boolean') { diff --git a/packages/dd-trace/src/process-tags/index.js b/packages/dd-trace/src/process-tags/index.js index 6fe87b848cb..98f7cf3a2aa 100644 --- a/packages/dd-trace/src/process-tags/index.js +++ b/packages/dd-trace/src/process-tags/index.js @@ -72,6 +72,9 @@ function buildProcessTags (config) { // Singleton with constant defaults so pre-init reads don't blow up const processTags = module.exports = { + /** + * @param {import('../config/config-base')} config + */ initialize (config) { // check if one of the properties added during build exist and if so return if (processTags.tags) return diff --git a/packages/dd-trace/src/profiler.js b/packages/dd-trace/src/profiler.js index 4990212fe92..36e30a0c80a 100644 --- a/packages/dd-trace/src/profiler.js +++ b/packages/dd-trace/src/profiler.js @@ -5,13 +5,16 @@ const { profiler } = require('./profiling') globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(() => { profiler.stop() }) module.exports = { - start: config => { + /** + * @param {import('./config/config-base')} config - Tracer configuration + */ + start (config) { // Forward the full tracer config to the profiling layer. // Profiling code is responsible for deriving the specific options it needs. return profiler.start(config) }, - stop: () => { + stop () { profiler.stop() }, } diff --git a/packages/dd-trace/src/profiling/ssi-heuristics.js b/packages/dd-trace/src/profiling/ssi-heuristics.js index 994cf7d6a46..e83ba71b18f 100644 --- a/packages/dd-trace/src/profiling/ssi-heuristics.js +++ b/packages/dd-trace/src/profiling/ssi-heuristics.js @@ -1,6 +1,6 @@ 'use strict' -const dc = require('dc-polyfill') +const dc = /** @type {typeof import('diagnostics_channel')} */ (require('dc-polyfill')) const log = require('../log') // If the process lives for at least 30 seconds, it's considered long-lived @@ -10,6 +10,9 @@ const DEFAULT_LONG_LIVED_THRESHOLD = 30_000 * This class embodies the SSI profiler-triggering heuristics under SSI. */ class SSIHeuristics { + /** + * @param {import('../config/config-base')} config - Tracer configuration + */ constructor (config) { const longLivedThreshold = config.profiling.longLivedThreshold || DEFAULT_LONG_LIVED_THRESHOLD if (typeof longLivedThreshold !== 'number' || longLivedThreshold <= 0) { diff --git a/packages/dd-trace/src/propagation-hash/index.js b/packages/dd-trace/src/propagation-hash/index.js index 74d61f3938b..29cb6069809 100644 --- a/packages/dd-trace/src/propagation-hash/index.js +++ b/packages/dd-trace/src/propagation-hash/index.js @@ -17,11 +17,12 @@ class PropagationHashManager { _cachedHash = null _cachedHashString = null _cachedHashBase64 = null + /** @type {import('../config/config-base') | null} */ _config = null /** * Configure the propagation hash manager with tracer config - * @param {object} config - Tracer configuration + * @param {import('../config/config-base')} config - Tracer configuration */ configure (config) { this._config = config diff --git a/packages/dd-trace/src/proxy.js b/packages/dd-trace/src/proxy.js index 015e91f78ca..6ac2b7dc4f9 100644 --- a/packages/dd-trace/src/proxy.js +++ b/packages/dd-trace/src/proxy.js @@ -27,9 +27,12 @@ class LazyModule { this.provider = provider } - enable (...args) { + /** + * @param {import('./config/config-base')} config - Tracer configuration + */ + enable (config, ...args) { this.module = this.provider() - this.module.enable(...args) + this.module.enable(config, ...args) } disable () { @@ -237,12 +240,16 @@ class Tracer extends NoopProxy { getDynamicInstrumentationClient(config) } } catch (e) { - log.error('Error initialising tracer', e) + log.error('Error initializing tracer', e) + // TODO: Should we stop everything started so far? } return this } + /** + * @param {import('./config/config-base')} config - Tracer configuration + */ _startProfiler (config) { // do not stop tracer initialization if the profiler fails to be imported try { @@ -256,6 +263,9 @@ class Tracer extends NoopProxy { } } + /** + * @param {import('./config/config-base')} config - Tracer configuration + */ #updateTracing (config) { if (config.tracing !== false) { if (config.appsec.enabled) { diff --git a/packages/dd-trace/src/remote_config/index.js b/packages/dd-trace/src/remote_config/index.js index d4451234938..83a3b016e15 100644 --- a/packages/dd-trace/src/remote_config/index.js +++ b/packages/dd-trace/src/remote_config/index.js @@ -25,6 +25,9 @@ class RemoteConfig { #products = new Set() #batchHandlers = new Map() + /** + * @param {import('../config/config-base')} config - Tracer configuration + */ constructor (config) { const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000) diff --git a/packages/dd-trace/src/runtime_metrics/index.js b/packages/dd-trace/src/runtime_metrics/index.js index 9b2602844e7..72f51dae1fb 100644 --- a/packages/dd-trace/src/runtime_metrics/index.js +++ b/packages/dd-trace/src/runtime_metrics/index.js @@ -14,6 +14,9 @@ const noop = runtimeMetrics = { } module.exports = { + /** + * @param {import('../config/config-base')} config - Tracer configuration + */ start (config) { if (!config?.runtimeMetrics.enabled) return diff --git a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js index 5e042b8484b..7fd0ccdd7c1 100644 --- a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js @@ -35,6 +35,9 @@ let eventLoopDelayObserver = null // https://github.com/DataDog/dogweb/blob/prod/integration/node/node_metadata.csv module.exports = { + /** + * @param {import('../config/config-base')} config - Tracer configuration + */ start (config) { this.stop() const clientConfig = DogStatsDClient.generateClientConfig(config) diff --git a/packages/dd-trace/src/sampler.js b/packages/dd-trace/src/sampler.js index b023c55b6de..df9eadb1dec 100644 --- a/packages/dd-trace/src/sampler.js +++ b/packages/dd-trace/src/sampler.js @@ -42,7 +42,7 @@ class Sampler { /** * Determines whether a trace/span should be sampled based on the configured sampling rate. * - * @param {Span|SpanContext} span - The span or span context to evaluate. + * @param {import("../../..").Span|import("../../..").SpanContext} span - The span or span context to evaluate. * @returns {boolean} `true` if the trace/span should be sampled, otherwise `false`. */ isSampled (span) { diff --git a/packages/dd-trace/src/standalone/index.js b/packages/dd-trace/src/standalone/index.js index eb43ee87d4d..699e48c220c 100644 --- a/packages/dd-trace/src/standalone/index.js +++ b/packages/dd-trace/src/standalone/index.js @@ -11,6 +11,9 @@ const startCh = channel('dd-trace:span:start') const injectCh = channel('dd-trace:span:inject') const extractCh = channel('dd-trace:span:extract') +/** + * @param {import('../config/config-base')} config - Tracer configuration + */ function configure (config) { if (startCh.hasSubscribers) startCh.unsubscribe(onSpanStart) if (injectCh.hasSubscribers) injectCh.unsubscribe(onSpanInject) diff --git a/packages/dd-trace/test/config/generated-config-types.spec.js b/packages/dd-trace/test/config/generated-config-types.spec.js new file mode 100644 index 00000000000..20ce9b08b36 --- /dev/null +++ b/packages/dd-trace/test/config/generated-config-types.spec.js @@ -0,0 +1,20 @@ +'use strict' + +const assert = require('node:assert/strict') +const { readFileSync } = require('node:fs') + +const { describe, it } = require('mocha') + +const { + generateConfigTypes, + OUTPUT_PATH, +} = require('../../../../scripts/generate-config-types') + +describe('generated config types', () => { + it('should stay in sync with supported-configurations.json', () => { + assert.strictEqual( + readFileSync(OUTPUT_PATH, 'utf8').replaceAll('\r\n', '\n'), + generateConfigTypes() + ) + }) +}) diff --git a/packages/dd-trace/test/plugin_manager.spec.js b/packages/dd-trace/test/plugin_manager.spec.js index 9a2f9e1c884..9ce71da9ef5 100644 --- a/packages/dd-trace/test/plugin_manager.spec.js +++ b/packages/dd-trace/test/plugin_manager.spec.js @@ -23,6 +23,15 @@ describe('Plugin Manager', () => { let Eight let pm + function makeTracerConfig (overrides = {}) { + return { + plugins: true, + spanAttributeSchema: 'v0', + spanRemoveIntegrationFromService: false, + ...overrides, + } + } + beforeEach(() => { tracer = { _nomenclature: nomenclature, @@ -31,8 +40,10 @@ describe('Plugin Manager', () => { class FakePlugin { constructor (aTracer) { assert.strictEqual(aTracer, tracer) - instantiated.push(this.constructor.id) + instantiated.push(/** @type {{ id: string }} */ (/** @type {unknown} */ (this.constructor)).id) } + + configure () {} } const plugins = { @@ -108,7 +119,7 @@ describe('Plugin Manager', () => { it('should keep the config for future configure calls', () => { pm.configurePlugin('two', { foo: 'bar' }) - pm.configure() + pm.configure(makeTracerConfig()) loadChannel.publish({ name: 'two' }) sinon.assert.calledWithMatch(Two.prototype.configure, { enabled: true, @@ -118,7 +129,7 @@ describe('Plugin Manager', () => { }) describe('without env vars', () => { - beforeEach(() => pm.configure()) + beforeEach(() => pm.configure(makeTracerConfig())) it('works with no config param', () => { pm.configurePlugin('two') @@ -158,7 +169,7 @@ describe('Plugin Manager', () => { }) describe('with disabled plugins', () => { - beforeEach(() => pm.configure()) + beforeEach(() => pm.configure(makeTracerConfig())) it('should not call configure on individual enable override', () => { pm.configurePlugin('five', { enabled: true }) @@ -167,7 +178,7 @@ describe('Plugin Manager', () => { }) it('should not configure all disabled plugins', () => { - pm.configure({}) + pm.configure(makeTracerConfig()) loadChannel.publish({ name: 'five' }) sinon.assert.notCalled(Five.prototype.configure) sinon.assert.notCalled(Six.prototype.configure) @@ -175,7 +186,7 @@ describe('Plugin Manager', () => { }) describe('with env var true', () => { - beforeEach(() => pm.configure()) + beforeEach(() => pm.configure(makeTracerConfig())) beforeEach(() => { process.env.DD_TRACE_TWO_ENABLED = '1' @@ -223,7 +234,7 @@ describe('Plugin Manager', () => { }) describe('with env var false', () => { - beforeEach(() => pm.configure()) + beforeEach(() => pm.configure(makeTracerConfig())) beforeEach(() => { process.env.DD_TRACE_TWO_ENABLED = '0' @@ -274,7 +285,7 @@ describe('Plugin Manager', () => { describe('configure', () => { describe('without the load event', () => { it('should not instantiate plugins', () => { - pm.configure() + pm.configure(makeTracerConfig()) pm.configurePlugin('two') assert.strictEqual(instantiated.length, 0) sinon.assert.notCalled(Two.prototype.configure) @@ -283,13 +294,13 @@ describe('Plugin Manager', () => { describe('with an experimental plugin', () => { it('should disable the plugin by default', () => { - pm.configure() + pm.configure(makeTracerConfig()) loadChannel.publish({ name: 'eight' }) sinon.assert.calledWithMatch(Eight.prototype.configure, { enabled: false }) }) it('should enable the plugin when configured programmatically', () => { - pm.configure() + pm.configure(makeTracerConfig()) pm.configurePlugin('eight') loadChannel.publish({ name: 'eight' }) sinon.assert.calledWithMatch(Eight.prototype.configure, { enabled: true }) @@ -297,24 +308,24 @@ describe('Plugin Manager', () => { it('should enable the plugin when configured with an environment variable', () => { process.env.DD_TRACE_EIGHT_ENABLED = 'true' - pm.configure() + pm.configure(makeTracerConfig()) loadChannel.publish({ name: 'eight' }) sinon.assert.calledWithMatch(Eight.prototype.configure, { enabled: true }) }) }) it('instantiates plugin classes', () => { - pm.configure() + pm.configure(makeTracerConfig()) loadChannel.publish({ name: 'two' }) loadChannel.publish({ name: 'four' }) assert.deepStrictEqual(instantiated, ['two', 'four']) }) describe('service naming schema manager', () => { - const config = { + const config = makeTracerConfig({ foo: { bar: 1 }, baz: 2, - } + }) let configureSpy beforeEach(() => { @@ -331,19 +342,19 @@ describe('Plugin Manager', () => { }) }) - it('skips configuring plugins entirely when plugins is false', () => { - pm.configurePlugin = sinon.spy() - pm.configure({ plugins: false }) - sinon.assert.notCalled(pm.configurePlugin) + it('disables plugins globally when plugins is false', () => { + pm.configure(makeTracerConfig({ plugins: false })) + loadChannel.publish({ name: 'two' }) + sinon.assert.calledWithMatch(Two.prototype.configure, { enabled: false }) }) it('observes configuration options', () => { - pm.configure({ + pm.configure(makeTracerConfig({ serviceMapping: { two: 'deux' }, logInjection: true, queryStringObfuscation: '.*', clientIpEnabled: true, - }) + })) loadChannel.publish({ name: 'two' }) loadChannel.publish({ name: 'four' }) sinon.assert.calledWithMatch(Two.prototype.configure, { @@ -363,7 +374,7 @@ describe('Plugin Manager', () => { }) describe('destroy', () => { - beforeEach(() => pm.configure()) + beforeEach(() => pm.configure(makeTracerConfig())) it('should disable the plugins', () => { loadChannel.publish({ name: 'two' }) diff --git a/scripts/generate-config-types.js b/scripts/generate-config-types.js new file mode 100644 index 00000000000..66aecd34a78 --- /dev/null +++ b/scripts/generate-config-types.js @@ -0,0 +1,217 @@ +'use strict' + +const { readFileSync, writeFileSync } = require('node:fs') +const path = require('node:path') + +const CHECK_FLAG = '--check' +const OUTPUT_PATH_IN_REPOSITORY = 'packages/dd-trace/src/config/generated-config-types.d.ts' +const SUPPORTED_CONFIGURATIONS_PATH = path.join( + __dirname, + '..', + 'packages/dd-trace/src/config/supported-configurations.json' +) +const OUTPUT_PATH = path.join( + __dirname, + '..', + 'packages/dd-trace/src/config/generated-config-types.d.ts' +) + +const BASE_TYPES = { + array: 'string[]', + boolean: 'boolean', + decimal: 'number', + int: 'number', + json: 'unknown', + map: 'Record', + string: 'string', +} + +const SIMPLE_ALLOWED_VALUE = /^[A-Za-z0-9 _./:-]+$/ +const PROPERTY_TYPE_OVERRIDES = { + 'dogstatsd.port': 'string | number', + port: 'string | number', + samplingRules: "import('../../../../index').SamplingRule[]", + spanSamplingRules: "import('../../../../index').SpanSamplingRule[]", + url: 'string | URL', +} +const TRANSFORM_TYPE_OVERRIDES = { + normalizeProfilingEnabled: "'true' | 'false' | 'auto'", + parseOtelTags: 'Record', + sampleRate: 'number', + setIntegerRangeSet: 'number[]', + splitJSONPathRules: 'string[]', +} + +function createTreeNode () { + return { + children: new Map(), + type: undefined, + } +} + +function getPropertyName (canonicalName, entry) { + const configurationNames = entry.internalPropertyName ? [entry.internalPropertyName] : entry.configurationNames + return configurationNames?.[0] ?? canonicalName +} + +function withUndefined (type, entry) { + return entry.default === null ? `${type} | undefined` : type +} + +function getAllowedType (entry) { + if (!entry.allowed) { + return + } + + const values = entry.allowed.split('|') + if (values.length === 0 || values.some(value => !SIMPLE_ALLOWED_VALUE.test(value))) { + return + } + + const normalizedValues = values.map(value => { + if (entry.transform === 'toLowerCase') { + return value.toLowerCase() + } + if (entry.transform === 'toUpperCase') { + return value.toUpperCase() + } + return value + }) + + return normalizedValues + .map(value => JSON.stringify(value)) + .join(' | ') +} + +function getTypeForEntry (propertyName, entry) { + const override = PROPERTY_TYPE_OVERRIDES[propertyName] ?? + TRANSFORM_TYPE_OVERRIDES[entry.transform] ?? + getAllowedType(entry) ?? + BASE_TYPES[entry.type] + + if (!override) { + throw new Error(`Unsupported configuration type for ${propertyName}: ${entry.type}`) + } + + return withUndefined(override, entry) +} + +function addProperty (root, propertyName, type) { + const parts = propertyName.split('.') + let node = root + + for (const part of parts) { + node.children.set(part, node.children.get(part) ?? createTreeNode()) + node = node.children.get(part) + } + + if (node.type && node.type !== type) { + throw new Error(`Conflicting generated types for ${propertyName}: ${node.type} !== ${type}`) + } + + node.type = type +} + +function renderPropertyName (name) { + return /^[$A-Z_a-z][$\w]*$/.test(name) ? name : JSON.stringify(name) +} + +function renderNode (node, indentLevel = 0) { + const indent = ' '.repeat(indentLevel) + + if (node.children.size === 0) { + return /** @type {string} */ (node.type) + } + + const objectBody = [...node.children.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => { + return `${indent} ${renderPropertyName(key)}: ${renderNode(child, indentLevel + 1)};` + }) + .join('\n') + const objectType = `{\n${objectBody}\n${indent}}` + + if (!node.type) { + return objectType + } + + return `${node.type} | ${objectType}` +} + +function generateConfigTypes () { + const { supportedConfigurations } = JSON.parse(readFileSync(SUPPORTED_CONFIGURATIONS_PATH, 'utf8')) + const root = createTreeNode() + + for (const [canonicalName, entries] of Object.entries(supportedConfigurations)) { + if (entries.length !== 1) { + throw new Error( + `Multiple entries found for canonical name: ${canonicalName}. ` + + 'This is currently not supported and must be implemented, if needed.' + ) + } + + const [entry] = entries + const propertyName = getPropertyName(canonicalName, entry) + const type = getTypeForEntry(propertyName, entry) + + addProperty(root, propertyName, type) + } + + return ( + '// This file is generated from packages/dd-trace/src/config/supported-configurations.json\n' + + '// by scripts/generate-config-types.js. Do not edit this file directly.\n\n' + + 'export interface GeneratedConfig ' + + renderNode(root) + + '\n' + ) +} + +function normalizeLineEndings (value) { + return value.replaceAll('\r\n', '\n') +} + +function writeGeneratedConfigTypes () { + const output = generateConfigTypes() + writeFileSync(OUTPUT_PATH, output) + return output +} + +function checkGeneratedConfigTypes () { + const generated = generateConfigTypes() + + const current = normalizeLineEndings(readFileSync(OUTPUT_PATH, 'utf8')) + if (current === generated) { + return true + } + + // eslint-disable-next-line no-console + console.error(`❌ Generated config types are out of date. + +The checked-in generated file does not match the current source-of-truth inputs: +- packages/dd-trace/src/config/supported-configurations.json +- index.d.ts + +To regenerate it locally, run: + npm run generate:config:types + +Then commit the updated file: + ${OUTPUT_PATH_IN_REPOSITORY} +`) + return false +} + +if (require.main === module) { + if (process.argv.includes(CHECK_FLAG)) { + process.exitCode = checkGeneratedConfigTypes() ? 0 : 1 + } else { + writeGeneratedConfigTypes() + } +} + +module.exports = { + checkGeneratedConfigTypes, + generateConfigTypes, + OUTPUT_PATH, + SUPPORTED_CONFIGURATIONS_PATH, + writeGeneratedConfigTypes, +} From aee7c5e1e69186a06432f985349008219deb1b83 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 1 Apr 2026 17:44:55 +0200 Subject: [PATCH 2/2] fixup! --- packages/dd-trace/src/config/index.js | 7 +++---- packages/dd-trace/src/config/supported-configurations.json | 4 ++-- packages/dd-trace/src/profiling/exporter_cli.js | 5 +---- packages/dd-trace/src/profiling/profiler.js | 2 +- packages/dd-trace/test/config/index.spec.js | 6 +++--- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/dd-trace/src/config/index.js b/packages/dd-trace/src/config/index.js index 797d983e644..15d52a52fab 100644 --- a/packages/dd-trace/src/config/index.js +++ b/packages/dd-trace/src/config/index.js @@ -654,10 +654,9 @@ class Config { setString(target, 'installSignature.id', DD_INSTRUMENTATION_INSTALL_ID) setString(target, 'installSignature.time', DD_INSTRUMENTATION_INSTALL_TIME) setString(target, 'installSignature.type', DD_INSTRUMENTATION_INSTALL_TYPE) - // TODO: Why is DD_INJECTION_ENABLED a comma separated list? - setArray(target, 'injectionEnabled', DD_INJECTION_ENABLED) - if (DD_INJECTION_ENABLED !== undefined) { - setString(target, 'instrumentationSource', DD_INJECTION_ENABLED ? 'ssi' : 'manual') + setString(target, 'injectionEnabled', DD_INJECTION_ENABLED) + if (DD_INJECTION_ENABLED) { + setString(target, 'instrumentationSource', 'ssi') } setBoolean(target, 'injectForce', DD_INJECT_FORCE) setBoolean(target, 'isAzureFunction', getIsAzureFunction()) diff --git a/packages/dd-trace/src/config/supported-configurations.json b/packages/dd-trace/src/config/supported-configurations.json index 5bca7736dc9..8971520a45b 100644 --- a/packages/dd-trace/src/config/supported-configurations.json +++ b/packages/dd-trace/src/config/supported-configurations.json @@ -1066,8 +1066,8 @@ "DD_INJECTION_ENABLED": [ { "implementation": "A", - "type": "array", - "default": "", + "type": "string", + "default": null, "configurationNames": [ "injectionEnabled" ] diff --git a/packages/dd-trace/src/profiling/exporter_cli.js b/packages/dd-trace/src/profiling/exporter_cli.js index cba3d6349b1..a122a334664 100644 --- a/packages/dd-trace/src/profiling/exporter_cli.js +++ b/packages/dd-trace/src/profiling/exporter_cli.js @@ -17,9 +17,6 @@ function exporterFromURL (url) { if (url.protocol === 'file:') { return new FileExporter({ pprofPrefix: fileURLToPath(url) }) } - // TODO: Why is DD_INJECTION_ENABLED a comma separated list? - const injectionEnabled = (getValueFromEnvSources('DD_INJECTION_ENABLED') ?? '').split(',') - const libraryInjected = injectionEnabled.length > 0 const profilingEnabled = (getValueFromEnvSources('DD_PROFILING_ENABLED') ?? '').toLowerCase() const activation = ['true', '1'].includes(profilingEnabled) ? 'manual' @@ -30,7 +27,7 @@ function exporterFromURL (url) { url, logger, uploadTimeout: timeoutMs, - libraryInjected, + libraryInjected: !!getValueFromEnvSources('DD_INJECTION_ENABLED'), activation, }) } diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index c107fc82750..e8b06719e54 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -95,7 +95,7 @@ class Profiler extends EventEmitter { error (...args) { log.error(...args) }, } - const libraryInjected = injectionEnabled.length > 0 + const libraryInjected = !!injectionEnabled let activation if (enabled === 'auto') { activation = 'auto' diff --git a/packages/dd-trace/test/config/index.spec.js b/packages/dd-trace/test/config/index.spec.js index 16e8d53275e..cc6a8189d20 100644 --- a/packages/dd-trace/test/config/index.spec.js +++ b/packages/dd-trace/test/config/index.spec.js @@ -445,7 +445,7 @@ describe('Config', () => { assert.deepStrictEqual(config.dynamicInstrumentation?.redactionExcludedIdentifiers, []) assert.deepStrictEqual(config.grpc.client.error.statuses, GRPC_CLIENT_ERROR_STATUSES) assert.deepStrictEqual(config.grpc.server.error.statuses, GRPC_SERVER_ERROR_STATUSES) - assert.deepStrictEqual(config.injectionEnabled, []) + assert.deepStrictEqual(config.injectionEnabled, undefined) assert.deepStrictEqual(config.serviceMapping, {}) assert.deepStrictEqual(config.tracePropagationStyle?.extract, ['datadog', 'tracecontext', 'baggage']) assert.deepStrictEqual(config.tracePropagationStyle?.inject, ['datadog', 'tracecontext', 'baggage']) @@ -533,7 +533,7 @@ describe('Config', () => { { name: 'iast.stackTrace.enabled', value: true, origin: 'default' }, { name: 'iast.telemetryVerbosity', value: 'INFORMATION', origin: 'default' }, { name: 'injectForce', value: false, origin: 'default' }, - { name: 'injectionEnabled', value: [], origin: 'default' }, + { name: 'injectionEnabled', value: undefined, origin: 'default' }, { name: 'instrumentationSource', value: 'manual', origin: 'default' }, { name: 'isCiVisibility', value: false, origin: 'default' }, { name: 'isEarlyFlakeDetectionEnabled', value: true, origin: 'default' }, @@ -996,7 +996,7 @@ describe('Config', () => { { name: 'iast.stackTrace.enabled', value: false, origin: 'env_var' }, { name: 'iast.telemetryVerbosity', value: 'DEBUG', origin: 'env_var' }, { name: 'injectForce', value: false, origin: 'env_var' }, - { name: 'injectionEnabled', value: ['tracer'], origin: 'env_var' }, + { name: 'injectionEnabled', value: 'tracer', origin: 'env_var' }, { name: 'instrumentation_config_id', value: 'abcdef123', origin: 'env_var' }, { name: 'instrumentationSource', value: 'ssi', origin: 'env_var' }, { name: 'isGCPFunction', value: false, origin: 'env_var' },