From 5d29c9af89ca2312427b754e9208491dd1671567 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Sun, 19 Apr 2026 07:55:56 +0200 Subject: [PATCH 1/2] Standardize `enable` option handling across all plugins Introduce a shared `resolveEnable` helper in @dd/core that all six user-facing plugins now use to resolve their `enable` config flag. This replaces the two divergent patterns (explicit `??` vs spread- override) with a single greppable call site per plugin. Non-boolean values continue to be coerced for backwards compatibility but now emit a deprecation warning so strict validation can land in the next major. live-debugger retains its existing hard rejection via the companion `validateEnableStrict` helper. Also fixes the misleading `default: true` wording in the output and metrics READMEs, adds the missing `errorTracking.enable` docs section, and authors a full README for the rum plugin (lost in the sourcemaps extraction refactor of 804f917e). --- packages/core/src/helpers/options.test.ts | 132 +++++++++ packages/core/src/helpers/options.ts | 64 +++++ packages/plugins/apps/README.md | 4 +- packages/plugins/apps/src/index.ts | 2 +- packages/plugins/apps/src/validate.test.ts | 73 ++++- packages/plugins/apps/src/validate.ts | 8 +- packages/plugins/error-tracking/README.md | 9 + .../error-tracking/src/validate.test.ts | 75 +++++- .../plugins/error-tracking/src/validate.ts | 3 +- packages/plugins/live-debugger/README.md | 4 +- .../plugins/live-debugger/src/validate.ts | 8 +- packages/plugins/metrics/README.md | 6 +- .../metrics/src/common/helpers.test.ts | 36 ++- .../plugins/metrics/src/common/helpers.ts | 12 +- packages/plugins/metrics/src/index.ts | 2 +- packages/plugins/output/README.md | 4 +- packages/plugins/output/src/index.ts | 4 +- packages/plugins/output/src/validate.test.ts | 67 ++++- packages/plugins/output/src/validate.ts | 8 +- packages/plugins/rum/README.md | 254 ++++++++++++++++++ packages/plugins/rum/src/validate.test.ts | 67 ++++- packages/plugins/rum/src/validate.ts | 3 +- 22 files changed, 788 insertions(+), 57 deletions(-) create mode 100644 packages/core/src/helpers/options.test.ts create mode 100644 packages/core/src/helpers/options.ts create mode 100644 packages/plugins/rum/README.md diff --git a/packages/core/src/helpers/options.test.ts b/packages/core/src/helpers/options.test.ts new file mode 100644 index 000000000..d62ee69b0 --- /dev/null +++ b/packages/core/src/helpers/options.test.ts @@ -0,0 +1,132 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import type { Logger } from '@dd/core/types'; + +import { resetEnableWarnings, resolveEnable, validateEnableStrict } from './options'; + +const mockLogger: Logger = { + getLogger: jest.fn(), + time: jest.fn() as unknown as Logger['time'], + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetEnableWarnings(); +}); + +describe('resolveEnable', () => { + describe('standard boolean / omitted values', () => { + const cases = [ + { + description: 'return false when the config key is undefined', + options: {}, + expected: false, + }, + { + description: 'return false when the config key is null', + options: { myPlugin: null }, + expected: false, + }, + { + description: 'return true when the config key is a truthy object without enable', + options: { myPlugin: { someOther: 'val' } }, + expected: true, + }, + { + description: 'return true when enable is true', + options: { myPlugin: { enable: true } }, + expected: true, + }, + { + description: 'return false when enable is false', + options: { myPlugin: { enable: false } }, + expected: false, + }, + { + description: 'return true when enable is undefined (object present)', + options: { myPlugin: { enable: undefined } }, + expected: true, + }, + ]; + + test.each(cases)('should $description', ({ options, expected }) => { + expect(resolveEnable(options, 'myPlugin', mockLogger)).toBe(expected); + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); + }); + + describe('non-boolean coercion with deprecation warning', () => { + const cases = [ + { + description: 'coerce enable: 1 to true and warn', + options: { myPlugin: { enable: 1 } }, + expected: true, + }, + { + description: 'coerce enable: 0 to false and warn', + options: { myPlugin: { enable: 0 } }, + expected: false, + }, + { + description: 'coerce enable: "true" to true and warn', + options: { myPlugin: { enable: 'true' } }, + expected: true, + }, + { + description: 'coerce enable: "" to false and warn', + options: { myPlugin: { enable: '' } }, + expected: false, + }, + ]; + + test.each(cases)('should $description', ({ options, expected }) => { + expect(resolveEnable(options, 'myPlugin', mockLogger)).toBe(expected); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('myPlugin.enable'), + ); + }); + }); + + describe('warn-once behavior', () => { + test('should only warn once per config key across multiple calls', () => { + resolveEnable({ myPlugin: { enable: 1 } }, 'myPlugin', mockLogger); + resolveEnable({ myPlugin: { enable: 'yes' } }, 'myPlugin', mockLogger); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + }); + + test('should warn separately for different config keys', () => { + resolveEnable({ pluginA: { enable: 1 } }, 'pluginA', mockLogger); + resolveEnable({ pluginB: { enable: 1 } }, 'pluginB', mockLogger); + expect(mockLogger.warn).toHaveBeenCalledTimes(2); + }); + }); +}); + +describe('validateEnableStrict', () => { + test('should not push an error when enable is a boolean', () => { + const errors: string[] = []; + validateEnableStrict({ enable: true }, errors); + expect(errors).toHaveLength(0); + }); + + test('should not push an error when enable is undefined', () => { + const errors: string[] = []; + validateEnableStrict({ enable: undefined }, errors); + expect(errors).toHaveLength(0); + }); + + test('should push an error when enable is a non-boolean', () => { + const errors: string[] = []; + validateEnableStrict({ enable: 'yes' }, errors); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('enable'); + expect(errors[0]).toContain('boolean'); + }); +}); diff --git a/packages/core/src/helpers/options.ts b/packages/core/src/helpers/options.ts new file mode 100644 index 000000000..fc864bb73 --- /dev/null +++ b/packages/core/src/helpers/options.ts @@ -0,0 +1,64 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import type { Logger } from '@dd/core/types'; +import chalk from 'chalk'; + +const warnedKeys = new Set(); + +/** + * Resolve the `enable` value for a plugin config key, emitting a deprecation + * warning when the caller passes a non-boolean truthy/falsy value. + * + * Semantics: + * - Config key absent / undefined / falsy → false (plugin disabled). + * - Config key is a truthy object without an `enable` property → true. + * - Config key is a truthy object with `enable` set → coerce to boolean, + * warning once per key if it isn't already a boolean. + */ +export const resolveEnable = ( + options: T, + configKey: C, + log: Logger, +): boolean => { + const pluginConfig = options[configKey]; + + if (pluginConfig && typeof pluginConfig === 'object' && 'enable' in pluginConfig) { + const value = (pluginConfig as Record).enable; + + if (typeof value !== 'boolean' && value !== undefined) { + if (!warnedKeys.has(configKey)) { + warnedKeys.add(configKey); + log.warn( + `\`${configKey}.enable\` should be a boolean, got ${typeof value}. ` + + `Non-boolean values are coerced today but will be rejected in the next major.`, + ); + } + } + + if (value !== undefined) { + return !!value; + } + } + + return !!pluginConfig; +}; + +/** + * Push a strict validation error when `enable` is present but not a boolean. + * Used by plugins that have always rejected non-boolean values (e.g. live-debugger). + */ +export const validateEnableStrict = ( + pluginConfig: { enable?: unknown }, + errors: string[], +): void => { + if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') { + errors.push(`${chalk.bold.red('enable')} must be a boolean`); + } +}; + +/** @internal Exposed only for tests to reset the warn-once set between cases. */ +export const resetEnableWarnings = (): void => { + warnedKeys.clear(); +}; diff --git a/packages/plugins/apps/README.md b/packages/plugins/apps/README.md index 03cdb074a..4490ca1ad 100644 --- a/packages/plugins/apps/README.md +++ b/packages/plugins/apps/README.md @@ -54,10 +54,12 @@ Setting the `apps.dryRun` configuration will override any value set in the envir ### apps.enable -> default: `true` when an `apps` config block is present +> default: `true` when an `apps` config block is present, `false` otherwise. Enable or disable the plugin without removing its configuration. +Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. + ### apps.include > default: `[]` diff --git a/packages/plugins/apps/src/index.ts b/packages/plugins/apps/src/index.ts index 86bdec673..ad87870bc 100644 --- a/packages/plugins/apps/src/index.ts +++ b/packages/plugins/apps/src/index.ts @@ -18,7 +18,7 @@ export type types = { export const getPlugins: GetPlugins = ({ options, context, bundler }) => { const log = context.getLogger(PLUGIN_NAME); - const validatedOptions = validateOptions(options); + const validatedOptions = validateOptions(options, log); if (!validatedOptions.enable) { return []; } diff --git a/packages/plugins/apps/src/validate.test.ts b/packages/plugins/apps/src/validate.test.ts index f9fb6af69..a99639dd9 100644 --- a/packages/plugins/apps/src/validate.test.ts +++ b/packages/plugins/apps/src/validate.test.ts @@ -3,6 +3,22 @@ // Copyright 2019-Present Datadog, Inc. import { validateOptions } from '@dd/apps-plugin/validate'; +import { resetEnableWarnings } from '@dd/core/helpers/options'; +import type { Logger } from '@dd/core/types'; + +const mockLogger: Logger = { + getLogger: jest.fn(() => mockLogger), + time: jest.fn() as unknown as Logger['time'], + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetEnableWarnings(); +}); describe('Apps Plugin - validateOptions', () => { describe('enable flag', () => { @@ -30,14 +46,44 @@ describe('Apps Plugin - validateOptions', () => { ]; test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input); + const result = validateOptions(input, mockLogger); expect(result.enable).toBe(expected); }); }); + describe('enable deprecation warning for non-boolean values', () => { + const cases = [ + { + description: 'coerce enable: 1 to true and warn', + input: { apps: { enable: 1 } }, + expected: true, + }, + { + description: 'coerce enable: 0 to false and warn', + input: { apps: { enable: 0 } }, + expected: false, + }, + { + description: 'coerce enable: "true" to true and warn', + input: { apps: { enable: 'true' } }, + expected: true, + }, + ]; + + test.each(cases)('Should $description', ({ input, expected }) => { + const result = validateOptions( + input as unknown as Parameters[0], + mockLogger, + ); + expect(result.enable).toBe(expected); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('apps.enable')); + }); + }); + describe('defaults', () => { test('Should set defaults when nothing is provided', () => { - const result = validateOptions({}); + const result = validateOptions({}, mockLogger); expect(result).toEqual({ dryRun: true, enable: false, @@ -50,7 +96,7 @@ describe('Apps Plugin - validateOptions', () => { test('Should set dryRun to false when DATADOG_APPS_UPLOAD_ASSETS is set', () => { process.env.DATADOG_APPS_UPLOAD_ASSETS = '1'; try { - const result = validateOptions({ apps: {} }); + const result = validateOptions({ apps: {} }, mockLogger); expect(result.dryRun).toBe(false); } finally { delete process.env.DATADOG_APPS_UPLOAD_ASSETS; @@ -60,7 +106,7 @@ describe('Apps Plugin - validateOptions', () => { test('Should set dryRun to false when DD_APPS_UPLOAD_ASSETS is set', () => { process.env.DD_APPS_UPLOAD_ASSETS = '1'; try { - const result = validateOptions({ apps: {} }); + const result = validateOptions({ apps: {} }, mockLogger); expect(result.dryRun).toBe(false); } finally { delete process.env.DD_APPS_UPLOAD_ASSETS; @@ -70,7 +116,7 @@ describe('Apps Plugin - validateOptions', () => { test('Should respect explicit dryRun over env var', () => { process.env.DATADOG_APPS_UPLOAD_ASSETS = '1'; try { - const result = validateOptions({ apps: { dryRun: true } }); + const result = validateOptions({ apps: { dryRun: true } }, mockLogger); expect(result.dryRun).toBe(true); } finally { delete process.env.DATADOG_APPS_UPLOAD_ASSETS; @@ -80,14 +126,17 @@ describe('Apps Plugin - validateOptions', () => { describe('overrides', () => { test('Should keep provided options and trim identifier', () => { - const result = validateOptions({ - apps: { - dryRun: true, - enable: true, - include: ['public/**/*', 'dist/**/*'], - identifier: ' my-app ', + const result = validateOptions( + { + apps: { + dryRun: true, + enable: true, + include: ['public/**/*', 'dist/**/*'], + identifier: ' my-app ', + }, }, - }); + mockLogger, + ); expect(result).toEqual({ dryRun: true, diff --git a/packages/plugins/apps/src/validate.ts b/packages/plugins/apps/src/validate.ts index bd1509065..ba53de16b 100644 --- a/packages/plugins/apps/src/validate.ts +++ b/packages/plugins/apps/src/validate.ts @@ -3,17 +3,17 @@ // Copyright 2019-Present Datadog, Inc. import { getDDEnvValue } from '@dd/core/helpers/env'; -import type { Options } from '@dd/core/types'; +import { resolveEnable } from '@dd/core/helpers/options'; +import type { Logger, Options } from '@dd/core/types'; import { CONFIG_KEY } from './constants'; import type { AppsOptions, AppsOptionsWithDefaults } from './types'; -export const validateOptions = (options: Options): AppsOptionsWithDefaults => { +export const validateOptions = (options: Options, log: Logger): AppsOptionsWithDefaults => { const resolvedOptions = (options[CONFIG_KEY] || {}) as AppsOptions; - const enable = resolvedOptions.enable ?? !!options[CONFIG_KEY]; const validatedOptions: AppsOptionsWithDefaults = { - enable, + enable: resolveEnable(options, CONFIG_KEY, log), include: resolvedOptions.include || [], dryRun: resolvedOptions.dryRun ?? !getDDEnvValue('APPS_UPLOAD_ASSETS'), identifier: resolvedOptions.identifier?.trim(), diff --git a/packages/plugins/error-tracking/README.md b/packages/plugins/error-tracking/README.md index 3b2ef7786..3e32cb7de 100644 --- a/packages/plugins/error-tracking/README.md +++ b/packages/plugins/error-tracking/README.md @@ -10,6 +10,7 @@ Interact with Error Tracking directly from your build system. - [Configuration](#configuration) + - [errorTracking.enable](#errortrackingenable) - [Sourcemaps Upload](#sourcemaps-upload) - [errorTracking.sourcemaps.bailOnError](#errortrackingsourcemapsbailonerror) - [errorTracking.sourcemaps.dryRun](#errortrackingsourcemapsdryrun) @@ -35,6 +36,14 @@ errorTracking?: { } ``` +### errorTracking.enable + +> default: `true` when an `errorTracking` config block is present, `false` otherwise. + +Enable or disable the plugin without removing its configuration. + +Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. + ## Sourcemaps Upload Upload JavaScript sourcemaps to Datadog to un-minify your errors. diff --git a/packages/plugins/error-tracking/src/validate.test.ts b/packages/plugins/error-tracking/src/validate.test.ts index a657672d7..522023b25 100644 --- a/packages/plugins/error-tracking/src/validate.test.ts +++ b/packages/plugins/error-tracking/src/validate.test.ts @@ -2,11 +2,27 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { resetEnableWarnings } from '@dd/core/helpers/options'; +import type { Logger } from '@dd/core/types'; import type { SourcemapsOptions } from '@dd/error-tracking-plugin/types'; import { validateOptions, validateSourcemapsOptions } from '@dd/error-tracking-plugin/validate'; -import { mockLogger, getMinimalSourcemapsConfiguration } from '@dd/tests/_jest/helpers/mocks'; +import { getMinimalSourcemapsConfiguration } from '@dd/tests/_jest/helpers/mocks'; import stripAnsi from 'strip-ansi'; +const mockLogger: Logger = { + getLogger: jest.fn(() => mockLogger), + time: jest.fn() as unknown as Logger['time'], + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetEnableWarnings(); +}); + describe('Error Tracking Plugins validate', () => { describe('validateOptions', () => { test('Should return the validated configuration', () => { @@ -44,6 +60,63 @@ describe('Error Tracking Plugins validate', () => { }).toThrow(); }); }); + + describe('enable flag', () => { + const cases = [ + { + description: 'return false when no errorTracking config is provided', + input: {}, + expected: false, + }, + { + description: 'return true when errorTracking config is an empty object', + input: { errorTracking: {} }, + expected: true, + }, + { + description: 'respect explicit enable true', + input: { errorTracking: { enable: true } }, + expected: true, + }, + { + description: 'respect explicit enable false', + input: { errorTracking: { enable: false } }, + expected: false, + }, + ]; + + test.each(cases)('Should $description', ({ input, expected }) => { + const result = validateOptions(input, mockLogger); + expect(result.enable).toBe(expected); + }); + }); + + describe('enable deprecation warning for non-boolean values', () => { + test('Should coerce enable: 1 to true and warn', () => { + const result = validateOptions( + { errorTracking: { enable: 1 } } as unknown as Parameters< + typeof validateOptions + >[0], + mockLogger, + ); + expect(result.enable).toBe(true); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('errorTracking.enable'), + ); + }); + + test('Should coerce enable: 0 to false and warn', () => { + const result = validateOptions( + { errorTracking: { enable: 0 } } as unknown as Parameters< + typeof validateOptions + >[0], + mockLogger, + ); + expect(result.enable).toBe(false); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + }); + }); describe('validateSourcemapsOptions', () => { test('Should return errors for each missing required field', () => { const { errors } = validateSourcemapsOptions({ diff --git a/packages/plugins/error-tracking/src/validate.ts b/packages/plugins/error-tracking/src/validate.ts index 8afffe494..5b0c7e77c 100644 --- a/packages/plugins/error-tracking/src/validate.ts +++ b/packages/plugins/error-tracking/src/validate.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { resolveEnable } from '@dd/core/helpers/options'; import type { Logger, Options } from '@dd/core/types'; import chalk from 'chalk'; @@ -28,8 +29,8 @@ export const validateOptions = (config: Options, log: Logger): ErrorTrackingOpti // Build the final configuration. const toReturn: ErrorTrackingOptionsWithDefaults = { - enable: !!config[CONFIG_KEY], ...config[CONFIG_KEY], + enable: resolveEnable(config, CONFIG_KEY, log), sourcemaps: undefined, }; diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index e7a246bd6..557404293 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -123,9 +123,9 @@ const double = (x) => { ### liveDebugger.enable -> default: `true` when a `liveDebugger` config block is present +> default: `true` when a `liveDebugger` config block is present, `false` otherwise. -Enable or disable the plugin without removing its configuration. +Enable or disable the plugin without removing its configuration. Must be a boolean. ### metadata.version diff --git a/packages/plugins/live-debugger/src/validate.ts b/packages/plugins/live-debugger/src/validate.ts index ede13beee..11f0b34d4 100644 --- a/packages/plugins/live-debugger/src/validate.ts +++ b/packages/plugins/live-debugger/src/validate.ts @@ -2,6 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { resolveEnable, validateEnableStrict } from '@dd/core/helpers/options'; import type { Logger, Options } from '@dd/core/types'; import chalk from 'chalk'; @@ -16,10 +17,7 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio const metadataVersion = config.metadata?.version; const errors: string[] = []; - // Validate enable option - if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') { - errors.push(`${red('enable')} must be a boolean`); - } + validateEnableStrict(pluginConfig, errors); // Validate include option if (pluginConfig.include !== undefined) { @@ -86,7 +84,7 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio // Build the final configuration with defaults return { - enable: pluginConfig.enable ?? !!config[CONFIG_KEY], + enable: resolveEnable(config, CONFIG_KEY, log), version: metadataVersion, include: pluginConfig.include || [/\.[jt]sx?$/], // .js, .jsx, .ts, .tsx exclude: pluginConfig.exclude || [ diff --git a/packages/plugins/metrics/README.md b/packages/plugins/metrics/README.md index cef665120..50781de50 100644 --- a/packages/plugins/metrics/README.md +++ b/packages/plugins/metrics/README.md @@ -47,9 +47,11 @@ metrics?: { ### `enable` -> default: `true` +> default: `true` when a `metrics` config block is present, `false` otherwise. + +Enable or disable the plugin without removing its configuration. -Plugin will be enabled and track metrics when set to `true`. +Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. ### `enableDefaultPrefix` diff --git a/packages/plugins/metrics/src/common/helpers.test.ts b/packages/plugins/metrics/src/common/helpers.test.ts index 1ce0e9418..0b6a8d6eb 100644 --- a/packages/plugins/metrics/src/common/helpers.test.ts +++ b/packages/plugins/metrics/src/common/helpers.test.ts @@ -2,7 +2,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { Metric } from '@dd/core/types'; +import { resetEnableWarnings } from '@dd/core/helpers/options'; +import type { Logger, Metric } from '@dd/core/types'; import { defaultFilters } from '@dd/metrics-plugin/common/filters'; import { getMetricsToSend, @@ -18,11 +19,25 @@ import { getMockModule, } from '@dd/tests/_jest/helpers/mocks'; +const mockLogger: Logger = { + getLogger: jest.fn(() => mockLogger), + time: jest.fn() as unknown as Logger['time'], + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetEnableWarnings(); +}); + describe('Metrics Helpers', () => { describe('validateOptions', () => { test('Should return the default options', () => { const options = { ...defaultPluginOptions, [CONFIG_KEY]: {} }; - expect(validateOptions(options, 'webpack')).toEqual({ + expect(validateOptions(options, 'webpack', mockLogger)).toEqual({ enable: true, enableDefaultPrefix: true, enableTracing: false, @@ -45,7 +60,7 @@ describe('Metrics Helpers', () => { tags: ['tag1'], }, }; - expect(validateOptions(options, 'webpack')).toEqual({ + expect(validateOptions(options, 'webpack', mockLogger)).toEqual({ enable: false, enableDefaultPrefix: true, enableTracing: true, @@ -55,6 +70,21 @@ describe('Metrics Helpers', () => { timestamp: expect.any(Number), }); }); + + test('Should coerce non-boolean enable and warn', () => { + const options = { + ...defaultPluginOptions, + [CONFIG_KEY]: { enable: 1 }, + }; + const result = validateOptions( + options as unknown as Parameters[0], + 'webpack', + mockLogger, + ); + expect(result.enable).toBe(true); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('metrics.enable')); + }); }); describe('getModuleName', () => { diff --git a/packages/plugins/metrics/src/common/helpers.ts b/packages/plugins/metrics/src/common/helpers.ts index d7d136250..3e208e290 100644 --- a/packages/plugins/metrics/src/common/helpers.ts +++ b/packages/plugins/metrics/src/common/helpers.ts @@ -2,7 +2,14 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { OptionsWithDefaults, Metric, ValueContext, MetricToSend } from '@dd/core/types'; +import { resolveEnable } from '@dd/core/helpers/options'; +import type { + Logger, + OptionsWithDefaults, + Metric, + ValueContext, + MetricToSend, +} from '@dd/core/types'; import { CONFIG_KEY } from '@dd/metrics-plugin/constants'; import type { Module, @@ -20,6 +27,7 @@ export const getTimestamp = (timestamp?: number): number => { export const validateOptions = ( opts: OptionsWithDefaults, bundlerName: string, + log: Logger, ): MetricsOptionsWithDefaults => { const options = opts[CONFIG_KEY]; @@ -31,12 +39,12 @@ export const validateOptions = ( } return { - enable: !!opts[CONFIG_KEY], enableDefaultPrefix: true, enableTracing: false, filters: defaultFilters, tags: [], ...opts[CONFIG_KEY], + enable: resolveEnable(opts, CONFIG_KEY, log), timestamp, // Make it lowercase and remove any leading/closing dots. prefix: prefix.toLowerCase().replace(/(^\.*|\.*$)/g, ''), diff --git a/packages/plugins/metrics/src/index.ts b/packages/plugins/metrics/src/index.ts index 8eeb3bc12..67eabdf2f 100644 --- a/packages/plugins/metrics/src/index.ts +++ b/packages/plugins/metrics/src/index.ts @@ -30,7 +30,7 @@ export const getPlugins: GetPlugins = ({ options, context }) => { const log = context.getLogger(PLUGIN_NAME); let realBuildEnd: number = 0; - const validatedOptions = validateOptions(options, context.bundler.name); + const validatedOptions = validateOptions(options, context.bundler.name, log); const plugins: PluginOptions[] = []; // If the plugin is not enabled, return an empty array. diff --git a/packages/plugins/output/README.md b/packages/plugins/output/README.md index 4efa1f7ba..68b4fe83c 100644 --- a/packages/plugins/output/README.md +++ b/packages/plugins/output/README.md @@ -37,10 +37,12 @@ output?: { ### `enable` -> default: `true` +> default: `true` when an `output` config block is present, `false` otherwise. Enable or disable the output plugin. +Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. + ### `path` > default: `'./'` diff --git a/packages/plugins/output/src/index.ts b/packages/plugins/output/src/index.ts index e366494c4..d3b18dc19 100644 --- a/packages/plugins/output/src/index.ts +++ b/packages/plugins/output/src/index.ts @@ -87,9 +87,9 @@ export const getFilePath = (outDir: string, pathOption: string, filename: string }; export const getPlugins: GetPlugins = ({ options, context }) => { - // Verify configuration. - const validatedOptions = validateOptions(options); const log = context.getLogger(PLUGIN_NAME); + // Verify configuration. + const validatedOptions = validateOptions(options, log); // If the plugin is not enabled, return an empty array. if (!validatedOptions.enable) { diff --git a/packages/plugins/output/src/validate.test.ts b/packages/plugins/output/src/validate.test.ts index e1b8710bf..254a57bc6 100644 --- a/packages/plugins/output/src/validate.test.ts +++ b/packages/plugins/output/src/validate.test.ts @@ -2,8 +2,25 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { resetEnableWarnings } from '@dd/core/helpers/options'; +import type { Logger } from '@dd/core/types'; + import { validateOptions } from './validate'; +const mockLogger: Logger = { + getLogger: jest.fn(() => mockLogger), + time: jest.fn() as unknown as Logger['time'], + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetEnableWarnings(); +}); + describe('validateOptions', () => { describe('enable', () => { const cases = [ @@ -30,11 +47,32 @@ describe('validateOptions', () => { ]; test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input); + const result = validateOptions(input, mockLogger); expect(result.enable).toBe(expected); }); }); + describe('enable deprecation warning for non-boolean values', () => { + test('Should coerce enable: 1 to true and warn', () => { + const result = validateOptions( + { output: { enable: 1 } } as unknown as Parameters[0], + mockLogger, + ); + expect(result.enable).toBe(true); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('output.enable')); + }); + + test('Should coerce enable: 0 to false and warn', () => { + const result = validateOptions( + { output: { enable: 0 } } as unknown as Parameters[0], + mockLogger, + ); + expect(result.enable).toBe(false); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + }); + }); + describe('path', () => { const cases = [ { @@ -55,14 +93,14 @@ describe('validateOptions', () => { ]; test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input); + const result = validateOptions(input, mockLogger); expect(result.path).toBe(expected); }); }); describe('files', () => { test('Should have all files enabled by default when files is undefined', () => { - const result = validateOptions({ output: {} }); + const result = validateOptions({ output: {} }, mockLogger); expect(result.files).toEqual({ build: 'build.json', bundler: 'bundler.json', @@ -76,7 +114,7 @@ describe('validateOptions', () => { }); test('Should have all files disabled by default when files is empty object', () => { - const result = validateOptions({ output: { files: {} } }); + const result = validateOptions({ output: { files: {} } }, mockLogger); expect(result.files).toEqual({ build: false, bundler: false, @@ -90,17 +128,20 @@ describe('validateOptions', () => { }); test('Should handle mixed file configuration', () => { - const result = validateOptions({ - output: { - files: { - build: false, - timings: 'some-other-name-without-extension', - logs: './logs/some-name-with-extension.txt', - errors: 'error-log.json', - warnings: true, + const result = validateOptions( + { + output: { + files: { + build: false, + timings: 'some-other-name-without-extension', + logs: './logs/some-name-with-extension.txt', + errors: 'error-log.json', + warnings: true, + }, }, }, - }); + mockLogger, + ); expect(result.files).toEqual({ build: false, diff --git a/packages/plugins/output/src/validate.ts b/packages/plugins/output/src/validate.ts index c99390989..747a39653 100644 --- a/packages/plugins/output/src/validate.ts +++ b/packages/plugins/output/src/validate.ts @@ -2,7 +2,8 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import type { Options } from '@dd/core/types'; +import { resolveEnable } from '@dd/core/helpers/options'; +import type { Logger, Options } from '@dd/core/types'; import { CONFIG_KEY } from './constants'; import type { FileKey, OutputOptions, OutputOptionsWithDefaults } from './types'; @@ -40,12 +41,11 @@ const validateFilesOptions = ( }; // Deal with validation and defaults here. -export const validateOptions = (options: Options): OutputOptionsWithDefaults => { +export const validateOptions = (options: Options, log: Logger): OutputOptionsWithDefaults => { const validatedOptions: OutputOptionsWithDefaults = { - // By using an empty object, we consider the plugin as enabled. - enable: !!options[CONFIG_KEY], path: './', ...options[CONFIG_KEY], + enable: resolveEnable(options, CONFIG_KEY, log), files: validateFilesOptions(options[CONFIG_KEY]?.files), }; diff --git a/packages/plugins/rum/README.md b/packages/plugins/rum/README.md new file mode 100644 index 000000000..7c65f2e7e --- /dev/null +++ b/packages/plugins/rum/README.md @@ -0,0 +1,254 @@ +# Rum Plugin + +Interact with Real User Monitoring (RUM) directly from your build system. + + + +## Table of content + + + + +- [Configuration](#configuration) + - [rum.enable](#rumenable) +- [RUM SDK Injection](#rum-sdk-injection) + - [rum.sdk.applicationId](#rumsdkapplicationid) + - [rum.sdk.clientToken](#rumsdkclienttoken) + - [rum.sdk.site](#rumsdksite) + - [rum.sdk.sessionSampleRate](#rumsdksessionsamplerate) + - [rum.sdk.sessionReplaySampleRate](#rumsdksessionreplaysamplerate) + - [rum.sdk.defaultPrivacyLevel](#rumsdkdefaultprivacylevel) + - [rum.sdk.trackUserInteractions](#rumsdktrackuserinteractions) + - [rum.sdk.trackResources](#rumsdktrackresources) + - [rum.sdk.trackLongTasks](#rumsdktracklongtasks) + - [rum.sdk.trackViewsManually](#rumsdktrackviewsmanually) + - [rum.sdk.trackingConsent](#rumsdktrackingconsent) + - [rum.sdk.traceSampleRate](#rumsdktracesamplerate) + - [rum.sdk.telemetrySampleRate](#rumsdktelemetrysamplerate) + - [rum.sdk.allowUntrustedEvents](#rumsdkallowuntrustedevents) + - [rum.sdk.compressIntakeRequests](#rumsdkcompressintakerequests) + - [rum.sdk.enablePrivacyForActionName](#rumsdkenableprivacyforactionname) + - [rum.sdk.silentMultipleInit](#rumsdksilentmultipleinit) + - [rum.sdk.startSessionReplayRecordingManually](#rumsdkstartsessionreplayrecordingmanually) + - [rum.sdk.storeContextsAcrossPages](#rumsdkstorecontextsacrosspages) +- [Privacy Transforms](#privacy-transforms) + - [rum.privacy.include](#rumprivacyinclude) + - [rum.privacy.exclude](#rumprivacyexclude) + - [rum.privacy.addToDictionaryFunctionName](#rumprivacyaddtodictionaryfunctionname) + - [rum.privacy.helperCodeExpression](#rumprivacyhelpercodeexpression) +- [Source Code Context](#source-code-context) + - [rum.sourceCodeContext.service](#rumsourcecodecontextservice) + - [rum.sourceCodeContext.version](#rumsourcecodecontextversion) + + +## Configuration + +```ts +rum?: { + enable?: boolean; + sdk?: { + applicationId: string; + clientToken?: string; + site?: string; + sessionSampleRate?: number; + sessionReplaySampleRate?: number; + defaultPrivacyLevel?: string; + trackUserInteractions?: boolean; + trackResources?: boolean; + trackLongTasks?: boolean; + trackViewsManually?: boolean; + trackingConsent?: string; + traceSampleRate?: number; + telemetrySampleRate?: number; + allowUntrustedEvents?: boolean; + compressIntakeRequests?: boolean; + enablePrivacyForActionName?: boolean; + silentMultipleInit?: boolean; + startSessionReplayRecordingManually?: boolean; + storeContextsAcrossPages?: boolean; + }; + privacy?: { + include?: (string | RegExp)[]; + exclude?: (string | RegExp)[]; + addToDictionaryFunctionName?: string; + helperCodeExpression?: string; + }; + sourceCodeContext?: { + service: string; + version?: string; + }; +} +``` + +### rum.enable + +> default: `true` when a `rum` config block is present, `false` otherwise. + +Enable or disable the plugin without removing its configuration. + +Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. + +## RUM SDK Injection + +Automatically inject the Datadog RUM Browser SDK into your application at build time. When the `rum.sdk` block is provided, the plugin injects initialization code so you don't need to add the SDK script tag or call `datadogRum.init()` manually. + +> [!NOTE] +> If `clientToken` is not provided, the plugin will attempt to fetch it automatically using `auth.apiKey` and `auth.appKey`. + +### rum.sdk.applicationId + +> required + +The RUM application ID from Datadog. + +### rum.sdk.clientToken + +> optional — fetched automatically when `auth.apiKey` and `auth.appKey` are set. + +The client token used by the RUM SDK to send data to Datadog. + +### rum.sdk.site + +> default: value of `auth.site` or `'datadoghq.com'` + +The Datadog site to send RUM data to. + +### rum.sdk.sessionSampleRate + +> default: `100` + +Percentage of sessions to track (0–100). + +### rum.sdk.sessionReplaySampleRate + +> default: `0` + +Percentage of tracked sessions that include Session Replay recordings (0–100). + +### rum.sdk.defaultPrivacyLevel + +> default: `'mask'` + +Default privacy level for Session Replay. Controls how content is masked in recordings. + +### rum.sdk.trackUserInteractions + +> default: `false` + +Automatically collect user actions (clicks). + +### rum.sdk.trackResources + +> default: `false` + +Automatically collect resource events. + +### rum.sdk.trackLongTasks + +> default: `false` + +Automatically collect long task events. + +### rum.sdk.trackViewsManually + +> default: `false` + +When `true`, RUM views must be started manually via the SDK API. + +### rum.sdk.trackingConsent + +> default: `'granted'` + +Initial tracking consent. Use `'not-granted'` to defer collection until consent is given. + +### rum.sdk.traceSampleRate + +> default: `100` + +Percentage of requests to trace (0–100). Controls APM trace correlation. + +### rum.sdk.telemetrySampleRate + +> default: `20` + +Percentage of telemetry events sent to Datadog for SDK health monitoring. + +### rum.sdk.allowUntrustedEvents + +> default: `false` + +Allow the SDK to capture programmatically dispatched (non-user) events. + +### rum.sdk.compressIntakeRequests + +> default: `false` + +Compress data sent to the Datadog intake to reduce network bandwidth. + +### rum.sdk.enablePrivacyForActionName + +> default: `false` + +When `true`, action names in Session Replay are masked for privacy. + +### rum.sdk.silentMultipleInit + +> default: `false` + +Suppress console warnings when `datadogRum.init()` is called more than once. + +### rum.sdk.startSessionReplayRecordingManually + +> default: `false` + +When `true`, Session Replay recording must be started manually via the SDK API. + +### rum.sdk.storeContextsAcrossPages + +> default: `false` + +Persist global and user contexts across page navigations using `localStorage`. + +## Privacy Transforms + +Build-time code transforms that prepare your application for Session Replay privacy controls. When the `rum.privacy` block is provided, the plugin transforms source files to support action name masking and other privacy features. + +### rum.privacy.include + +> default: `[/\.(?:c|m)?(?:j|t)sx?$/]` + +Array of file patterns (strings or RegExp) to include for privacy transforms. By default, all JavaScript and TypeScript files are included. + +### rum.privacy.exclude + +> default: `[/\/node_modules\//, /\.preval\./, /^[!@#$%^&*()=+~` + "`" + `-]/]` + +Array of file patterns (strings or RegExp) to exclude from privacy transforms. By default, `node_modules`, `.preval.` files, and files starting with special characters are excluded. + +### rum.privacy.addToDictionaryFunctionName + +> default: `'$'` + +The function name injected into transformed code to register strings with the privacy dictionary. + +### rum.privacy.helperCodeExpression + +> default: an IIFE that creates and manages the privacy dictionary queue on `globalThis`. + +Custom JavaScript expression for the privacy helper code. Override this only if you need to customize how the privacy dictionary queue is initialized. + +## Source Code Context + +Inject source code context metadata so Datadog can link RUM errors to the correct source version. + +### rum.sourceCodeContext.service + +> required + +The service name to associate with the source code context. Used by Datadog to link RUM data to the correct service. + +### rum.sourceCodeContext.version + +> optional + +The version string to associate with this build. When omitted, Datadog uses other available version information. diff --git a/packages/plugins/rum/src/validate.test.ts b/packages/plugins/rum/src/validate.test.ts index 9da4a92c6..533c9467d 100644 --- a/packages/plugins/rum/src/validate.test.ts +++ b/packages/plugins/rum/src/validate.test.ts @@ -2,10 +2,75 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import { resetEnableWarnings } from '@dd/core/helpers/options'; +import type { Logger } from '@dd/core/types'; import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; import { createFilter } from '@rollup/pluginutils'; -import { validatePrivacyOptions, validateSourceCodeContextOptions } from './validate'; +import { + validateOptions, + validatePrivacyOptions, + validateSourceCodeContextOptions, +} from './validate'; + +const mockLogger: Logger = { + getLogger: jest.fn(() => mockLogger), + time: jest.fn() as unknown as Logger['time'], + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); + resetEnableWarnings(); +}); + +describe('validateOptions', () => { + describe('enable flag', () => { + const cases = [ + { + description: 'return false when no rum config is provided', + input: { ...defaultPluginOptions }, + expected: false, + }, + { + description: 'return true when rum config is an empty object', + input: { ...defaultPluginOptions, rum: {} }, + expected: true, + }, + { + description: 'respect explicit enable true', + input: { ...defaultPluginOptions, rum: { enable: true } }, + expected: true, + }, + { + description: 'respect explicit enable false', + input: { ...defaultPluginOptions, rum: { enable: false } }, + expected: false, + }, + ]; + + test.each(cases)('Should $description', ({ input, expected }) => { + const result = validateOptions(input, mockLogger); + expect(result.enable).toBe(expected); + }); + }); + + describe('enable deprecation warning for non-boolean values', () => { + test('Should coerce non-boolean enable and warn', () => { + const input = { ...defaultPluginOptions, rum: { enable: 1 } }; + const result = validateOptions( + input as unknown as Parameters[0], + mockLogger, + ); + expect(result.enable).toBe(true); + expect(mockLogger.warn).toHaveBeenCalledTimes(1); + expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('rum.enable')); + }); + }); +}); describe('Test privacy plugin option exclude regex', () => { let filter: (path: string) => boolean; diff --git a/packages/plugins/rum/src/validate.ts b/packages/plugins/rum/src/validate.ts index 3fe8a8603..b96b2e992 100644 --- a/packages/plugins/rum/src/validate.ts +++ b/packages/plugins/rum/src/validate.ts @@ -3,6 +3,7 @@ // Copyright 2019-Present Datadog, Inc. import type { Site } from '@datadog/browser-rum'; +import { resolveEnable } from '@dd/core/helpers/options'; import type { Logger, Options, OptionsWithDefaults } from '@dd/core/types'; import chalk from 'chalk'; @@ -38,8 +39,8 @@ export const validateOptions = ( // Build the final configuration. const toReturn: RumOptionsWithDefaults = { - enable: !!options[CONFIG_KEY], ...options[CONFIG_KEY], + enable: resolveEnable(options, CONFIG_KEY, log), sdk: undefined, privacy: undefined, sourceCodeContext: undefined, From f847e1a020bee3273d3fd6fcedef3e5ced946d4a Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 13 May 2026 07:35:33 +0200 Subject: [PATCH 2/2] Address review comments --- packages/core/src/helpers/options.test.ts | 24 +- packages/core/src/helpers/options.ts | 16 +- packages/factory/src/index.test.ts | 54 ++++ packages/factory/src/index.ts | 29 +- packages/plugins/apps/src/index.ts | 5 +- packages/plugins/apps/src/types.ts | 5 +- packages/plugins/apps/src/validate.test.ts | 103 +------ packages/plugins/apps/src/validate.ts | 10 +- .../plugins/error-tracking/src/index.test.ts | 13 +- packages/plugins/error-tracking/src/index.ts | 6 - packages/plugins/error-tracking/src/types.ts | 2 - .../error-tracking/src/validate.test.ts | 76 +----- .../plugins/error-tracking/src/validate.ts | 14 +- packages/plugins/live-debugger/README.md | 4 +- .../plugins/live-debugger/src/index.test.ts | 19 -- packages/plugins/live-debugger/src/index.ts | 4 - packages/plugins/live-debugger/src/types.ts | 1 - .../live-debugger/src/validate.test.ts | 52 +--- .../plugins/live-debugger/src/validate.ts | 4 - .../metrics/src/common/helpers.test.ts | 39 +-- .../plugins/metrics/src/common/helpers.ts | 11 +- packages/plugins/metrics/src/index.test.ts | 11 +- packages/plugins/metrics/src/index.ts | 7 +- packages/plugins/metrics/src/types.ts | 2 +- packages/plugins/output/README.md | 2 +- packages/plugins/output/src/index.test.ts | 9 +- packages/plugins/output/src/index.ts | 8 +- packages/plugins/output/src/types.ts | 2 +- packages/plugins/output/src/validate.test.ts | 95 +------ packages/plugins/output/src/validate.ts | 10 +- packages/plugins/rum/README.md | 254 ------------------ packages/plugins/rum/src/index.test.ts | 19 +- packages/plugins/rum/src/index.ts | 6 - packages/plugins/rum/src/types.ts | 1 - packages/plugins/rum/src/validate.test.ts | 67 +---- packages/plugins/rum/src/validate.ts | 2 - .../src/commands/create-plugin/templates.ts | 20 +- .../tools/src/commands/integrity/files.ts | 2 +- 38 files changed, 145 insertions(+), 863 deletions(-) delete mode 100644 packages/plugins/rum/README.md diff --git a/packages/core/src/helpers/options.test.ts b/packages/core/src/helpers/options.test.ts index d62ee69b0..6c91de2bf 100644 --- a/packages/core/src/helpers/options.test.ts +++ b/packages/core/src/helpers/options.test.ts @@ -4,7 +4,7 @@ import type { Logger } from '@dd/core/types'; -import { resetEnableWarnings, resolveEnable, validateEnableStrict } from './options'; +import { resetEnableWarnings, resolveEnable } from './options'; const mockLogger: Logger = { getLogger: jest.fn(), @@ -108,25 +108,3 @@ describe('resolveEnable', () => { }); }); }); - -describe('validateEnableStrict', () => { - test('should not push an error when enable is a boolean', () => { - const errors: string[] = []; - validateEnableStrict({ enable: true }, errors); - expect(errors).toHaveLength(0); - }); - - test('should not push an error when enable is undefined', () => { - const errors: string[] = []; - validateEnableStrict({ enable: undefined }, errors); - expect(errors).toHaveLength(0); - }); - - test('should push an error when enable is a non-boolean', () => { - const errors: string[] = []; - validateEnableStrict({ enable: 'yes' }, errors); - expect(errors).toHaveLength(1); - expect(errors[0]).toContain('enable'); - expect(errors[0]).toContain('boolean'); - }); -}); diff --git a/packages/core/src/helpers/options.ts b/packages/core/src/helpers/options.ts index fc864bb73..3a0d35768 100644 --- a/packages/core/src/helpers/options.ts +++ b/packages/core/src/helpers/options.ts @@ -3,7 +3,6 @@ // Copyright 2019-Present Datadog, Inc. import type { Logger } from '@dd/core/types'; -import chalk from 'chalk'; const warnedKeys = new Set(); @@ -38,6 +37,8 @@ export const resolveEnable = { - if (pluginConfig.enable !== undefined && typeof pluginConfig.enable !== 'boolean') { - errors.push(`${chalk.bold.red('enable')} must be a boolean`); - } -}; - /** @internal Exposed only for tests to reset the warn-once set between cases. */ export const resetEnableWarnings = (): void => { warnedKeys.clear(); diff --git a/packages/factory/src/index.test.ts b/packages/factory/src/index.test.ts index 2ca19cce1..f7055f2b5 100644 --- a/packages/factory/src/index.test.ts +++ b/packages/factory/src/index.test.ts @@ -2,6 +2,17 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. +import type { PluginOptions, Options } from '@dd/core/types'; + +const invokeFactory = async (opts: Options): Promise => { + const { buildPluginFactory } = await import('@dd/factory'); + const factory = buildPluginFactory({ bundler: {}, version: '1.0.0' }); + return factory.raw(opts, { framework: 'esbuild' }) as PluginOptions[]; +}; + +const hasPlugin = (plugins: PluginOptions[], name: string) => + plugins.some((plugin) => plugin.name.includes(name)); + describe('Factory', () => { test('Should not throw with no options', async () => { const { buildPluginFactory } = await import('@dd/factory'); @@ -12,4 +23,47 @@ describe('Factory', () => { factory.vite(); }).not.toThrow(); }); + + describe('enable gating for user-facing plugins', () => { + // The factory is the single source of truth for `.enable`. + // Each user-facing plugin is skipped when its config key is absent or + // explicitly disabled, and included when the config key is present. + + test('Should skip a plugin when its config key is absent', async () => { + const plugins = await invokeFactory({ logLevel: 'none' }); + expect(hasPlugin(plugins, 'output')).toBe(false); + expect(hasPlugin(plugins, 'metrics')).toBe(false); + expect(hasPlugin(plugins, 'rum')).toBe(false); + }); + + test('Should include a plugin when its config key is present', async () => { + const plugins = await invokeFactory({ logLevel: 'none', output: {} }); + expect(hasPlugin(plugins, 'output')).toBe(true); + }); + + test('Should skip a plugin when enable: false', async () => { + const plugins = await invokeFactory({ + logLevel: 'none', + output: { enable: false }, + }); + expect(hasPlugin(plugins, 'output')).toBe(false); + }); + + test('Should include a plugin when enable: true', async () => { + const plugins = await invokeFactory({ + logLevel: 'none', + output: { enable: true }, + }); + expect(hasPlugin(plugins, 'output')).toBe(true); + }); + + test('Should coerce a non-boolean enable value and still include the plugin', async () => { + const plugins = await invokeFactory({ + logLevel: 'none', + // @ts-expect-error - intentional non-boolean to exercise coercion + output: { enable: 1 }, + }); + expect(hasPlugin(plugins, 'output')).toBe(true); + }); + }); }); diff --git a/packages/factory/src/index.ts b/packages/factory/src/index.ts index a1a866a2e..cd3ba8c2b 100644 --- a/packages/factory/src/index.ts +++ b/packages/factory/src/index.ts @@ -34,6 +34,7 @@ import { getContext } from './helpers/context'; import { wrapGetPlugins } from './helpers/wrapPlugins'; import { ALL_ENVS, HOST_NAME } from '@dd/core/constants'; import { notifyOnEnvOverrides } from '@dd/core/helpers/env'; +import { resolveEnable } from '@dd/core/helpers/options'; // #imports-injection-marker import * as apps from '@dd/apps-plugin'; import * as errorTracking from '@dd/error-tracking-plugin'; @@ -160,17 +161,27 @@ export const buildPluginFactory = ({ pluginsToAdd.push(['custom', options.customPlugins]); } - // Add the customer facing plugins. - pluginsToAdd.push( + // Customer-facing plugins are gated by their `.enable` flag. + // Resolving here lets every plugin share the same semantics: + // - config key absent → disabled + // - config key present without `enable` → enabled + // - non-boolean `enable` → coerced, with a single deprecation warning + const userFacingPlugins: [name: string, configKey: string, GetPlugins][] = [ // #configs-injection-marker - ['apps', apps.getPlugins], - ['error-tracking', errorTracking.getPlugins], - ['live-debugger', liveDebugger.getPlugins], - ['metrics', metrics.getPlugins], - ['output', output.getPlugins], - ['rum', rum.getPlugins], + ['apps', apps.CONFIG_KEY, apps.getPlugins], + ['error-tracking', errorTracking.CONFIG_KEY, errorTracking.getPlugins], + ['live-debugger', liveDebugger.CONFIG_KEY, liveDebugger.getPlugins], + ['metrics', metrics.CONFIG_KEY, metrics.getPlugins], + ['output', output.CONFIG_KEY, output.getPlugins], + ['rum', rum.CONFIG_KEY, rum.getPlugins], // #configs-injection-marker - ); + ]; + + for (const [name, configKey, getPlugins] of userFacingPlugins) { + if (resolveEnable(options, configKey, log)) { + pluginsToAdd.push([name, getPlugins]); + } + } // Initialize all our plugins. for (const [name, getPlugins] of pluginsToAdd) { diff --git a/packages/plugins/apps/src/index.ts b/packages/plugins/apps/src/index.ts index ad87870bc..0144727cf 100644 --- a/packages/plugins/apps/src/index.ts +++ b/packages/plugins/apps/src/index.ts @@ -18,10 +18,7 @@ export type types = { export const getPlugins: GetPlugins = ({ options, context, bundler }) => { const log = context.getLogger(PLUGIN_NAME); - const validatedOptions = validateOptions(options, log); - if (!validatedOptions.enable) { - return []; - } + const validatedOptions = validateOptions(options); if (context.bundler.name !== 'vite') { log.warn(`The apps plugin only supports Vite; skipping under '${context.bundler.name}'.`); diff --git a/packages/plugins/apps/src/types.ts b/packages/plugins/apps/src/types.ts index f53ac1a6a..c640c6e2c 100644 --- a/packages/plugins/apps/src/types.ts +++ b/packages/plugins/apps/src/types.ts @@ -25,4 +25,7 @@ export type AppsManifest = { }; // We don't enforce identifier, as it needs to be dynamically computed if absent. -export type AppsOptionsWithDefaults = WithRequired; +export type AppsOptionsWithDefaults = Omit< + WithRequired, + 'enable' +>; diff --git a/packages/plugins/apps/src/validate.test.ts b/packages/plugins/apps/src/validate.test.ts index a99639dd9..a660f1662 100644 --- a/packages/plugins/apps/src/validate.test.ts +++ b/packages/plugins/apps/src/validate.test.ts @@ -3,90 +3,13 @@ // Copyright 2019-Present Datadog, Inc. import { validateOptions } from '@dd/apps-plugin/validate'; -import { resetEnableWarnings } from '@dd/core/helpers/options'; -import type { Logger } from '@dd/core/types'; - -const mockLogger: Logger = { - getLogger: jest.fn(() => mockLogger), - time: jest.fn() as unknown as Logger['time'], - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), -}; - -beforeEach(() => { - jest.clearAllMocks(); - resetEnableWarnings(); -}); describe('Apps Plugin - validateOptions', () => { - describe('enable flag', () => { - const cases = [ - { - description: 'return false when no apps config is provided', - input: {}, - expected: false, - }, - { - description: 'return true when apps config is an empty object', - input: { apps: {} }, - expected: true, - }, - { - description: 'respect explicit enable true', - input: { apps: { enable: true } }, - expected: true, - }, - { - description: 'respect explicit enable false', - input: { apps: { enable: false } }, - expected: false, - }, - ]; - - test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input, mockLogger); - expect(result.enable).toBe(expected); - }); - }); - - describe('enable deprecation warning for non-boolean values', () => { - const cases = [ - { - description: 'coerce enable: 1 to true and warn', - input: { apps: { enable: 1 } }, - expected: true, - }, - { - description: 'coerce enable: 0 to false and warn', - input: { apps: { enable: 0 } }, - expected: false, - }, - { - description: 'coerce enable: "true" to true and warn', - input: { apps: { enable: 'true' } }, - expected: true, - }, - ]; - - test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions( - input as unknown as Parameters[0], - mockLogger, - ); - expect(result.enable).toBe(expected); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('apps.enable')); - }); - }); - describe('defaults', () => { test('Should set defaults when nothing is provided', () => { - const result = validateOptions({}, mockLogger); + const result = validateOptions({}); expect(result).toEqual({ dryRun: true, - enable: false, include: [], identifier: undefined, name: undefined, @@ -96,7 +19,7 @@ describe('Apps Plugin - validateOptions', () => { test('Should set dryRun to false when DATADOG_APPS_UPLOAD_ASSETS is set', () => { process.env.DATADOG_APPS_UPLOAD_ASSETS = '1'; try { - const result = validateOptions({ apps: {} }, mockLogger); + const result = validateOptions({ apps: {} }); expect(result.dryRun).toBe(false); } finally { delete process.env.DATADOG_APPS_UPLOAD_ASSETS; @@ -106,7 +29,7 @@ describe('Apps Plugin - validateOptions', () => { test('Should set dryRun to false when DD_APPS_UPLOAD_ASSETS is set', () => { process.env.DD_APPS_UPLOAD_ASSETS = '1'; try { - const result = validateOptions({ apps: {} }, mockLogger); + const result = validateOptions({ apps: {} }); expect(result.dryRun).toBe(false); } finally { delete process.env.DD_APPS_UPLOAD_ASSETS; @@ -116,7 +39,7 @@ describe('Apps Plugin - validateOptions', () => { test('Should respect explicit dryRun over env var', () => { process.env.DATADOG_APPS_UPLOAD_ASSETS = '1'; try { - const result = validateOptions({ apps: { dryRun: true } }, mockLogger); + const result = validateOptions({ apps: { dryRun: true } }); expect(result.dryRun).toBe(true); } finally { delete process.env.DATADOG_APPS_UPLOAD_ASSETS; @@ -126,21 +49,17 @@ describe('Apps Plugin - validateOptions', () => { describe('overrides', () => { test('Should keep provided options and trim identifier', () => { - const result = validateOptions( - { - apps: { - dryRun: true, - enable: true, - include: ['public/**/*', 'dist/**/*'], - identifier: ' my-app ', - }, + const result = validateOptions({ + apps: { + dryRun: true, + enable: true, + include: ['public/**/*', 'dist/**/*'], + identifier: ' my-app ', }, - mockLogger, - ); + }); expect(result).toEqual({ dryRun: true, - enable: true, include: ['public/**/*', 'dist/**/*'], identifier: 'my-app', name: undefined, diff --git a/packages/plugins/apps/src/validate.ts b/packages/plugins/apps/src/validate.ts index ba53de16b..38e75a61e 100644 --- a/packages/plugins/apps/src/validate.ts +++ b/packages/plugins/apps/src/validate.ts @@ -3,22 +3,18 @@ // Copyright 2019-Present Datadog, Inc. import { getDDEnvValue } from '@dd/core/helpers/env'; -import { resolveEnable } from '@dd/core/helpers/options'; -import type { Logger, Options } from '@dd/core/types'; +import type { Options } from '@dd/core/types'; import { CONFIG_KEY } from './constants'; import type { AppsOptions, AppsOptionsWithDefaults } from './types'; -export const validateOptions = (options: Options, log: Logger): AppsOptionsWithDefaults => { +export const validateOptions = (options: Options): AppsOptionsWithDefaults => { const resolvedOptions = (options[CONFIG_KEY] || {}) as AppsOptions; - const validatedOptions: AppsOptionsWithDefaults = { - enable: resolveEnable(options, CONFIG_KEY, log), + return { include: resolvedOptions.include || [], dryRun: resolvedOptions.dryRun ?? !getDDEnvValue('APPS_UPLOAD_ASSETS'), identifier: resolvedOptions.identifier?.trim(), name: resolvedOptions.name?.trim() || options.metadata?.name?.trim(), }; - - return validatedOptions; }; diff --git a/packages/plugins/error-tracking/src/index.test.ts b/packages/plugins/error-tracking/src/index.test.ts index 507ee846f..5573868d4 100644 --- a/packages/plugins/error-tracking/src/index.test.ts +++ b/packages/plugins/error-tracking/src/index.test.ts @@ -17,17 +17,8 @@ const uploadSourcemapsMock = jest.mocked(uploadSourcemaps); describe('Error Tracking Plugin', () => { describe('getPlugins', () => { - test('Should not initialize the plugin if not enabled', async () => { - expect(getPlugins(getGetPluginsArg({ errorTracking: { enable: false } }))).toHaveLength( - 0, - ); - expect(getPlugins(getGetPluginsArg())).toHaveLength(0); - }); - - test('Should initialize the plugin if enabled', async () => { - expect( - getPlugins(getGetPluginsArg({ errorTracking: { enable: true } })).length, - ).toBeGreaterThan(0); + test('Should initialize the plugin', async () => { + expect(getPlugins(getGetPluginsArg({ errorTracking: {} })).length).toBeGreaterThan(0); }); }); diff --git a/packages/plugins/error-tracking/src/index.ts b/packages/plugins/error-tracking/src/index.ts index 2b5b2b4c1..5d8d353b3 100644 --- a/packages/plugins/error-tracking/src/index.ts +++ b/packages/plugins/error-tracking/src/index.ts @@ -19,16 +19,10 @@ export type types = { export const getPlugins: GetPlugins = ({ options, context }) => { const log = context.getLogger(PLUGIN_NAME); - // Verify configuration. const timeOptions = log.time('validate options'); const validatedOptions = validateOptions(options, log); timeOptions.end(); - // If the plugin is not enabled, return an empty array. - if (!validatedOptions.enable) { - return []; - } - let gitInfo: RepositoryData | undefined; let buildReport: BuildReport | undefined; let sourcemapsHandled: boolean = false; diff --git a/packages/plugins/error-tracking/src/types.ts b/packages/plugins/error-tracking/src/types.ts index 018350600..87e8f5e73 100644 --- a/packages/plugins/error-tracking/src/types.ts +++ b/packages/plugins/error-tracking/src/types.ts @@ -23,12 +23,10 @@ export type ErrorTrackingOptions = { }; export type ErrorTrackingOptionsWithDefaults = { - enable?: boolean; sourcemaps?: SourcemapsOptionsWithDefaults; }; export type ErrorTrackingOptionsWithSourcemaps = { - enable?: boolean; sourcemaps: SourcemapsOptionsWithDefaults; }; diff --git a/packages/plugins/error-tracking/src/validate.test.ts b/packages/plugins/error-tracking/src/validate.test.ts index 522023b25..ce8203709 100644 --- a/packages/plugins/error-tracking/src/validate.test.ts +++ b/packages/plugins/error-tracking/src/validate.test.ts @@ -2,25 +2,13 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resetEnableWarnings } from '@dd/core/helpers/options'; -import type { Logger } from '@dd/core/types'; import type { SourcemapsOptions } from '@dd/error-tracking-plugin/types'; import { validateOptions, validateSourcemapsOptions } from '@dd/error-tracking-plugin/validate'; -import { getMinimalSourcemapsConfiguration } from '@dd/tests/_jest/helpers/mocks'; +import { getMinimalSourcemapsConfiguration, mockLogger } from '@dd/tests/_jest/helpers/mocks'; import stripAnsi from 'strip-ansi'; -const mockLogger: Logger = { - getLogger: jest.fn(() => mockLogger), - time: jest.fn() as unknown as Logger['time'], - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), -}; - beforeEach(() => { jest.clearAllMocks(); - resetEnableWarnings(); }); describe('Error Tracking Plugins validate', () => { @@ -31,15 +19,13 @@ describe('Error Tracking Plugins validate', () => { auth: { apiKey: '123', }, - errorTracking: { - enable: true, - }, + errorTracking: {}, }, mockLogger, ); expect(config).toEqual({ - enable: true, + sourcemaps: undefined, }); }); @@ -61,62 +47,6 @@ describe('Error Tracking Plugins validate', () => { }); }); - describe('enable flag', () => { - const cases = [ - { - description: 'return false when no errorTracking config is provided', - input: {}, - expected: false, - }, - { - description: 'return true when errorTracking config is an empty object', - input: { errorTracking: {} }, - expected: true, - }, - { - description: 'respect explicit enable true', - input: { errorTracking: { enable: true } }, - expected: true, - }, - { - description: 'respect explicit enable false', - input: { errorTracking: { enable: false } }, - expected: false, - }, - ]; - - test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input, mockLogger); - expect(result.enable).toBe(expected); - }); - }); - - describe('enable deprecation warning for non-boolean values', () => { - test('Should coerce enable: 1 to true and warn', () => { - const result = validateOptions( - { errorTracking: { enable: 1 } } as unknown as Parameters< - typeof validateOptions - >[0], - mockLogger, - ); - expect(result.enable).toBe(true); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('errorTracking.enable'), - ); - }); - - test('Should coerce enable: 0 to false and warn', () => { - const result = validateOptions( - { errorTracking: { enable: 0 } } as unknown as Parameters< - typeof validateOptions - >[0], - mockLogger, - ); - expect(result.enable).toBe(false); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - }); - }); describe('validateSourcemapsOptions', () => { test('Should return errors for each missing required field', () => { const { errors } = validateSourcemapsOptions({ diff --git a/packages/plugins/error-tracking/src/validate.ts b/packages/plugins/error-tracking/src/validate.ts index 5b0c7e77c..f96b9241d 100644 --- a/packages/plugins/error-tracking/src/validate.ts +++ b/packages/plugins/error-tracking/src/validate.ts @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resolveEnable } from '@dd/core/helpers/options'; import type { Logger, Options } from '@dd/core/types'; import chalk from 'chalk'; @@ -27,19 +26,10 @@ export const validateOptions = (config: Options, log: Logger): ErrorTrackingOpti throw new Error(`Invalid configuration for ${PLUGIN_NAME}.`); } - // Build the final configuration. - const toReturn: ErrorTrackingOptionsWithDefaults = { + return { ...config[CONFIG_KEY], - enable: resolveEnable(config, CONFIG_KEY, log), - sourcemaps: undefined, + sourcemaps: sourcemapsResults.config, }; - - // Fill in the defaults. - if (sourcemapsResults.config) { - toReturn.sourcemaps = sourcemapsResults.config; - } - - return toReturn; }; type ToReturn = { diff --git a/packages/plugins/live-debugger/README.md b/packages/plugins/live-debugger/README.md index 557404293..90a517333 100644 --- a/packages/plugins/live-debugger/README.md +++ b/packages/plugins/live-debugger/README.md @@ -125,7 +125,9 @@ const double = (x) => { > default: `true` when a `liveDebugger` config block is present, `false` otherwise. -Enable or disable the plugin without removing its configuration. Must be a boolean. +Enable or disable the plugin without removing its configuration. + +Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. ### metadata.version diff --git a/packages/plugins/live-debugger/src/index.test.ts b/packages/plugins/live-debugger/src/index.test.ts index 6c42934db..2bbd7f5fa 100644 --- a/packages/plugins/live-debugger/src/index.test.ts +++ b/packages/plugins/live-debugger/src/index.test.ts @@ -14,7 +14,6 @@ import type { LiveDebuggerOptionsWithDefaults } from './types'; const makeOptions = ( overrides: Partial = {}, ): LiveDebuggerOptionsWithDefaults => ({ - enable: true, version: '1.0.0', include: [/\.[jt]sx?$/], exclude: [/\/node_modules\//], @@ -409,24 +408,6 @@ describe('getLiveDebuggerPlugin', () => { }); describe('getPlugins', () => { - it('should return an empty array when enable is false', () => { - const arg = getGetPluginsArg({ liveDebugger: { enable: false } }); - - const plugins = getPlugins(arg); - - expect(plugins).toEqual([]); - expect(arg.context.inject).not.toHaveBeenCalled(); - }); - - it('should return an empty array when liveDebugger config is omitted', () => { - const arg = getGetPluginsArg(); - - const plugins = getPlugins(arg); - - expect(plugins).toEqual([]); - expect(arg.context.inject).not.toHaveBeenCalled(); - }); - it('should inject runtime stubs and return a plugin when an empty config is provided', () => { const arg = getGetPluginsArg({ liveDebugger: {} }); diff --git a/packages/plugins/live-debugger/src/index.ts b/packages/plugins/live-debugger/src/index.ts index fd93e5f6b..88f90a3e4 100644 --- a/packages/plugins/live-debugger/src/index.ts +++ b/packages/plugins/live-debugger/src/index.ts @@ -152,10 +152,6 @@ export const getPlugins: GetPlugins = ({ options, context }) => { const log = context.getLogger(PLUGIN_NAME); const validatedOptions = validateOptions(options, log); - if (!validatedOptions.enable) { - return []; - } - // Inject no-op stubs for the runtime globals so instrumented code // doesn't crash when the Datadog Browser Debugger SDK is absent. // The SDK's init() overwrites these with the real implementations. diff --git a/packages/plugins/live-debugger/src/types.ts b/packages/plugins/live-debugger/src/types.ts index 98afc0bc9..f6d69074a 100644 --- a/packages/plugins/live-debugger/src/types.ts +++ b/packages/plugins/live-debugger/src/types.ts @@ -23,7 +23,6 @@ export type LiveDebuggerOptions = { }; export type LiveDebuggerOptionsWithDefaults = { - enable: boolean; version: string | undefined; include: (string | RegExp)[]; exclude: (string | RegExp)[]; diff --git a/packages/plugins/live-debugger/src/validate.test.ts b/packages/plugins/live-debugger/src/validate.test.ts index d08219a28..09a282a58 100644 --- a/packages/plugins/live-debugger/src/validate.test.ts +++ b/packages/plugins/live-debugger/src/validate.test.ts @@ -30,10 +30,9 @@ describe('validateOptions', () => { describe('defaults', () => { const cases = [ { - description: 'disable when no options are provided', + description: 'return defaults when no options are provided', input: makeConfig(undefined), expected: { - enable: false, version: undefined, include: [/\.[jt]sx?$/], exclude: expect.arrayContaining([/\/node_modules\//]), @@ -43,20 +42,9 @@ describe('validateOptions', () => { } satisfies LiveDebuggerOptionsWithDefaults, }, { - description: 'honor enable: false even when the config key is present', - input: makeConfig({ enable: false }), - expected: expect.objectContaining({ enable: false, version: undefined }), - }, - { - description: 'honor enable: false even when metadata.version is provided', - input: makeConfig({ enable: false }, { version: '1.0.0' }), - expected: expect.objectContaining({ enable: false, version: '1.0.0' }), - }, - { - description: 'enable and return defaults when an empty object is provided', + description: 'return defaults when an empty object is provided', input: makeConfig({}), expected: { - enable: true, version: undefined, include: [/\.[jt]sx?$/], exclude: expect.arrayContaining([/\/node_modules\//]), @@ -66,15 +54,9 @@ describe('validateOptions', () => { } satisfies LiveDebuggerOptionsWithDefaults, }, { - description: 'honor enable: true and forward metadata.version', - input: makeConfig({ enable: true }, { version: '1.0.0' }), - expected: expect.objectContaining({ enable: true, version: '1.0.0' }), - }, - { - description: 'enable and forward metadata.version when liveDebugger is empty', + description: 'forward metadata.version when liveDebugger is empty', input: makeConfig({}, { version: '1.0.0' }), expected: { - enable: true, version: '1.0.0', include: [/\.[jt]sx?$/], exclude: expect.arrayContaining([/\/node_modules\//]), @@ -86,12 +68,12 @@ describe('validateOptions', () => { { description: 'leave version undefined when metadata is omitted', input: makeConfig({}), - expected: expect.objectContaining({ enable: true, version: undefined }), + expected: expect.objectContaining({ version: undefined }), }, { description: 'leave version undefined when only metadata.name is set', input: makeConfig({}, { name: 'my-build' }), - expected: expect.objectContaining({ enable: true, version: undefined }), + expected: expect.objectContaining({ version: undefined }), }, ]; @@ -258,28 +240,6 @@ describe('validateOptions', () => { }); }); - describe('invalid enable', () => { - const cases = [ - { - description: 'reject enable when a string', - input: makeInvalidConfig({ enable: 'yes' }), - }, - { - description: 'reject enable when a number', - input: makeInvalidConfig({ enable: 1 }), - }, - ]; - - test.each(cases)('should $description', ({ input }) => { - expect(() => validateOptions(input, mockLogger)).toThrow( - `Invalid configuration for ${PLUGIN_NAME}.`, - ); - expect(mockError).toHaveBeenCalledWith( - expect.stringMatching(/enable.*must be a boolean/), - ); - }); - }); - describe('invalid honorSkipComments', () => { const cases = [ { @@ -349,7 +309,6 @@ describe('validateOptions', () => { describe('multiple errors', () => { it('should aggregate all validation errors before throwing', () => { const input = makeInvalidConfig({ - enable: 'yes', include: 'bad', exclude: 'bad', honorSkipComments: 42, @@ -362,7 +321,6 @@ describe('validateOptions', () => { ); const errorMessage = mockError.mock.calls[0][0]; - expect(errorMessage).toMatch(/enable/); expect(errorMessage).toMatch(/include/); expect(errorMessage).toMatch(/exclude/); expect(errorMessage).toMatch(/honorSkipComments/); diff --git a/packages/plugins/live-debugger/src/validate.ts b/packages/plugins/live-debugger/src/validate.ts index 11f0b34d4..eb8020792 100644 --- a/packages/plugins/live-debugger/src/validate.ts +++ b/packages/plugins/live-debugger/src/validate.ts @@ -2,7 +2,6 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resolveEnable, validateEnableStrict } from '@dd/core/helpers/options'; import type { Logger, Options } from '@dd/core/types'; import chalk from 'chalk'; @@ -17,8 +16,6 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio const metadataVersion = config.metadata?.version; const errors: string[] = []; - validateEnableStrict(pluginConfig, errors); - // Validate include option if (pluginConfig.include !== undefined) { if (!Array.isArray(pluginConfig.include)) { @@ -84,7 +81,6 @@ export const validateOptions = (config: Options, log: Logger): LiveDebuggerOptio // Build the final configuration with defaults return { - enable: resolveEnable(config, CONFIG_KEY, log), version: metadataVersion, include: pluginConfig.include || [/\.[jt]sx?$/], // .js, .jsx, .ts, .tsx exclude: pluginConfig.exclude || [ diff --git a/packages/plugins/metrics/src/common/helpers.test.ts b/packages/plugins/metrics/src/common/helpers.test.ts index 0b6a8d6eb..325a35769 100644 --- a/packages/plugins/metrics/src/common/helpers.test.ts +++ b/packages/plugins/metrics/src/common/helpers.test.ts @@ -2,8 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resetEnableWarnings } from '@dd/core/helpers/options'; -import type { Logger, Metric } from '@dd/core/types'; +import type { Metric } from '@dd/core/types'; import { defaultFilters } from '@dd/metrics-plugin/common/filters'; import { getMetricsToSend, @@ -19,26 +18,11 @@ import { getMockModule, } from '@dd/tests/_jest/helpers/mocks'; -const mockLogger: Logger = { - getLogger: jest.fn(() => mockLogger), - time: jest.fn() as unknown as Logger['time'], - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), -}; - -beforeEach(() => { - jest.clearAllMocks(); - resetEnableWarnings(); -}); - describe('Metrics Helpers', () => { describe('validateOptions', () => { test('Should return the default options', () => { const options = { ...defaultPluginOptions, [CONFIG_KEY]: {} }; - expect(validateOptions(options, 'webpack', mockLogger)).toEqual({ - enable: true, + expect(validateOptions(options, 'webpack')).toEqual({ enableDefaultPrefix: true, enableTracing: false, filters: defaultFilters, @@ -53,15 +37,13 @@ describe('Metrics Helpers', () => { const options = { ...defaultPluginOptions, [CONFIG_KEY]: { - enable: false, enableTracing: true, filters: [fakeFilter], prefix: 'prefix', tags: ['tag1'], }, }; - expect(validateOptions(options, 'webpack', mockLogger)).toEqual({ - enable: false, + expect(validateOptions(options, 'webpack')).toEqual({ enableDefaultPrefix: true, enableTracing: true, filters: [fakeFilter], @@ -70,21 +52,6 @@ describe('Metrics Helpers', () => { timestamp: expect.any(Number), }); }); - - test('Should coerce non-boolean enable and warn', () => { - const options = { - ...defaultPluginOptions, - [CONFIG_KEY]: { enable: 1 }, - }; - const result = validateOptions( - options as unknown as Parameters[0], - 'webpack', - mockLogger, - ); - expect(result.enable).toBe(true); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('metrics.enable')); - }); }); describe('getModuleName', () => { diff --git a/packages/plugins/metrics/src/common/helpers.ts b/packages/plugins/metrics/src/common/helpers.ts index 3e208e290..9df625178 100644 --- a/packages/plugins/metrics/src/common/helpers.ts +++ b/packages/plugins/metrics/src/common/helpers.ts @@ -2,14 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resolveEnable } from '@dd/core/helpers/options'; -import type { - Logger, - OptionsWithDefaults, - Metric, - ValueContext, - MetricToSend, -} from '@dd/core/types'; +import type { OptionsWithDefaults, Metric, ValueContext, MetricToSend } from '@dd/core/types'; import { CONFIG_KEY } from '@dd/metrics-plugin/constants'; import type { Module, @@ -27,7 +20,6 @@ export const getTimestamp = (timestamp?: number): number => { export const validateOptions = ( opts: OptionsWithDefaults, bundlerName: string, - log: Logger, ): MetricsOptionsWithDefaults => { const options = opts[CONFIG_KEY]; @@ -44,7 +36,6 @@ export const validateOptions = ( filters: defaultFilters, tags: [], ...opts[CONFIG_KEY], - enable: resolveEnable(opts, CONFIG_KEY, log), timestamp, // Make it lowercase and remove any leading/closing dots. prefix: prefix.toLowerCase().replace(/(^\.*|\.*$)/g, ''), diff --git a/packages/plugins/metrics/src/index.test.ts b/packages/plugins/metrics/src/index.test.ts index ce32b8619..3859b6f7b 100644 --- a/packages/plugins/metrics/src/index.test.ts +++ b/packages/plugins/metrics/src/index.test.ts @@ -93,15 +93,8 @@ describe('Metrics Universal Plugin', () => { }); describe('getPlugins', () => { - test('Should not initialize the plugin if not enabled', async () => { - expect(getPlugins(getGetPluginsArg({ metrics: { enable: false } }))).toHaveLength(0); - expect(getPlugins(getGetPluginsArg())).toHaveLength(0); - }); - - test('Should initialize the plugin if enabled', async () => { - expect( - getPlugins(getGetPluginsArg({ metrics: { enable: true } })).length, - ).toBeGreaterThan(0); + test('Should initialize the plugin', async () => { + expect(getPlugins(getGetPluginsArg({ metrics: {} })).length).toBeGreaterThan(0); }); }); diff --git a/packages/plugins/metrics/src/index.ts b/packages/plugins/metrics/src/index.ts index 67eabdf2f..209ae79c5 100644 --- a/packages/plugins/metrics/src/index.ts +++ b/packages/plugins/metrics/src/index.ts @@ -30,14 +30,9 @@ export const getPlugins: GetPlugins = ({ options, context }) => { const log = context.getLogger(PLUGIN_NAME); let realBuildEnd: number = 0; - const validatedOptions = validateOptions(options, context.bundler.name, log); + const validatedOptions = validateOptions(options, context.bundler.name); const plugins: PluginOptions[] = []; - // If the plugin is not enabled, return an empty array. - if (!validatedOptions.enable) { - return plugins; - } - // Webpack and Esbuild specific plugins. // LEGACY const legacyPlugin: PluginOptions = { diff --git a/packages/plugins/metrics/src/types.ts b/packages/plugins/metrics/src/types.ts index 4ef873f06..2fa413e7d 100644 --- a/packages/plugins/metrics/src/types.ts +++ b/packages/plugins/metrics/src/types.ts @@ -17,7 +17,7 @@ export type MetricsOptions = { timestamp?: number; }; -export type MetricsOptionsWithDefaults = Required; +export type MetricsOptionsWithDefaults = Required>; export interface ModuleGraph { getModule(dependency: Dependency): Module; diff --git a/packages/plugins/output/README.md b/packages/plugins/output/README.md index 68b4fe83c..f8ba2cc34 100644 --- a/packages/plugins/output/README.md +++ b/packages/plugins/output/README.md @@ -39,7 +39,7 @@ output?: { > default: `true` when an `output` config block is present, `false` otherwise. -Enable or disable the output plugin. +Enable or disable the plugin without removing its configuration. Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. diff --git a/packages/plugins/output/src/index.test.ts b/packages/plugins/output/src/index.test.ts index de9c6be2d..ce687f861 100644 --- a/packages/plugins/output/src/index.test.ts +++ b/packages/plugins/output/src/index.test.ts @@ -17,13 +17,8 @@ const mockedOutputJson = jest.mocked(outputJson); describe('Output Plugin', () => { describe('getPlugins', () => { - test('Should not initialize the plugin if not enabled', async () => { - expect(getPlugins(getGetPluginsArg({ output: { enable: false } }))).toHaveLength(0); - expect(getPlugins(getGetPluginsArg())).toHaveLength(0); - }); - - test('Should initialize the plugin if enabled', async () => { - expect(getPlugins(getGetPluginsArg({ output: { enable: true } }))).toHaveLength(1); + test('Should initialize the plugin', async () => { + expect(getPlugins(getGetPluginsArg({ output: {} }))).toHaveLength(1); }); }); diff --git a/packages/plugins/output/src/index.ts b/packages/plugins/output/src/index.ts index d3b18dc19..26343a312 100644 --- a/packages/plugins/output/src/index.ts +++ b/packages/plugins/output/src/index.ts @@ -88,13 +88,7 @@ export const getFilePath = (outDir: string, pathOption: string, filename: string export const getPlugins: GetPlugins = ({ options, context }) => { const log = context.getLogger(PLUGIN_NAME); - // Verify configuration. - const validatedOptions = validateOptions(options, log); - - // If the plugin is not enabled, return an empty array. - if (!validatedOptions.enable) { - return []; - } + const validatedOptions = validateOptions(options); const writeFile = (name: FileKey, data: any) => { const fileValue: FileValue = validatedOptions.files[name]; diff --git a/packages/plugins/output/src/types.ts b/packages/plugins/output/src/types.ts index acdb5b698..4683f53c2 100644 --- a/packages/plugins/output/src/types.ts +++ b/packages/plugins/output/src/types.ts @@ -18,7 +18,7 @@ export type OutputOptions = { }; export type OutputOptionsWithDefaults = Assign< - Required, + Required>, { files: { [K in FileKey]: DefaultFileValue; diff --git a/packages/plugins/output/src/validate.test.ts b/packages/plugins/output/src/validate.test.ts index 254a57bc6..27171230c 100644 --- a/packages/plugins/output/src/validate.test.ts +++ b/packages/plugins/output/src/validate.test.ts @@ -2,77 +2,9 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resetEnableWarnings } from '@dd/core/helpers/options'; -import type { Logger } from '@dd/core/types'; - import { validateOptions } from './validate'; -const mockLogger: Logger = { - getLogger: jest.fn(() => mockLogger), - time: jest.fn() as unknown as Logger['time'], - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), -}; - -beforeEach(() => { - jest.clearAllMocks(); - resetEnableWarnings(); -}); - describe('validateOptions', () => { - describe('enable', () => { - const cases = [ - { - description: 'return false when no output config provided', - input: {}, - expected: false, - }, - { - description: 'return true when output config is an empty object', - input: { output: {} }, - expected: true, - }, - { - description: 'return true when output config has enable: true', - input: { output: { enable: true } }, - expected: true, - }, - { - description: 'return false when output config has enable: false', - input: { output: { enable: false } }, - expected: false, - }, - ]; - - test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input, mockLogger); - expect(result.enable).toBe(expected); - }); - }); - - describe('enable deprecation warning for non-boolean values', () => { - test('Should coerce enable: 1 to true and warn', () => { - const result = validateOptions( - { output: { enable: 1 } } as unknown as Parameters[0], - mockLogger, - ); - expect(result.enable).toBe(true); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('output.enable')); - }); - - test('Should coerce enable: 0 to false and warn', () => { - const result = validateOptions( - { output: { enable: 0 } } as unknown as Parameters[0], - mockLogger, - ); - expect(result.enable).toBe(false); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - }); - }); - describe('path', () => { const cases = [ { @@ -93,14 +25,14 @@ describe('validateOptions', () => { ]; test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input, mockLogger); + const result = validateOptions(input); expect(result.path).toBe(expected); }); }); describe('files', () => { test('Should have all files enabled by default when files is undefined', () => { - const result = validateOptions({ output: {} }, mockLogger); + const result = validateOptions({ output: {} }); expect(result.files).toEqual({ build: 'build.json', bundler: 'bundler.json', @@ -114,7 +46,7 @@ describe('validateOptions', () => { }); test('Should have all files disabled by default when files is empty object', () => { - const result = validateOptions({ output: { files: {} } }, mockLogger); + const result = validateOptions({ output: { files: {} } }); expect(result.files).toEqual({ build: false, bundler: false, @@ -128,20 +60,17 @@ describe('validateOptions', () => { }); test('Should handle mixed file configuration', () => { - const result = validateOptions( - { - output: { - files: { - build: false, - timings: 'some-other-name-without-extension', - logs: './logs/some-name-with-extension.txt', - errors: 'error-log.json', - warnings: true, - }, + const result = validateOptions({ + output: { + files: { + build: false, + timings: 'some-other-name-without-extension', + logs: './logs/some-name-with-extension.txt', + errors: 'error-log.json', + warnings: true, }, }, - mockLogger, - ); + }); expect(result.files).toEqual({ build: false, diff --git a/packages/plugins/output/src/validate.ts b/packages/plugins/output/src/validate.ts index 747a39653..81bc824e5 100644 --- a/packages/plugins/output/src/validate.ts +++ b/packages/plugins/output/src/validate.ts @@ -2,8 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resolveEnable } from '@dd/core/helpers/options'; -import type { Logger, Options } from '@dd/core/types'; +import type { Options } from '@dd/core/types'; import { CONFIG_KEY } from './constants'; import type { FileKey, OutputOptions, OutputOptionsWithDefaults } from './types'; @@ -41,13 +40,10 @@ const validateFilesOptions = ( }; // Deal with validation and defaults here. -export const validateOptions = (options: Options, log: Logger): OutputOptionsWithDefaults => { - const validatedOptions: OutputOptionsWithDefaults = { +export const validateOptions = (options: Options): OutputOptionsWithDefaults => { + return { path: './', ...options[CONFIG_KEY], - enable: resolveEnable(options, CONFIG_KEY, log), files: validateFilesOptions(options[CONFIG_KEY]?.files), }; - - return validatedOptions; }; diff --git a/packages/plugins/rum/README.md b/packages/plugins/rum/README.md deleted file mode 100644 index 7c65f2e7e..000000000 --- a/packages/plugins/rum/README.md +++ /dev/null @@ -1,254 +0,0 @@ -# Rum Plugin - -Interact with Real User Monitoring (RUM) directly from your build system. - - - -## Table of content - - - - -- [Configuration](#configuration) - - [rum.enable](#rumenable) -- [RUM SDK Injection](#rum-sdk-injection) - - [rum.sdk.applicationId](#rumsdkapplicationid) - - [rum.sdk.clientToken](#rumsdkclienttoken) - - [rum.sdk.site](#rumsdksite) - - [rum.sdk.sessionSampleRate](#rumsdksessionsamplerate) - - [rum.sdk.sessionReplaySampleRate](#rumsdksessionreplaysamplerate) - - [rum.sdk.defaultPrivacyLevel](#rumsdkdefaultprivacylevel) - - [rum.sdk.trackUserInteractions](#rumsdktrackuserinteractions) - - [rum.sdk.trackResources](#rumsdktrackresources) - - [rum.sdk.trackLongTasks](#rumsdktracklongtasks) - - [rum.sdk.trackViewsManually](#rumsdktrackviewsmanually) - - [rum.sdk.trackingConsent](#rumsdktrackingconsent) - - [rum.sdk.traceSampleRate](#rumsdktracesamplerate) - - [rum.sdk.telemetrySampleRate](#rumsdktelemetrysamplerate) - - [rum.sdk.allowUntrustedEvents](#rumsdkallowuntrustedevents) - - [rum.sdk.compressIntakeRequests](#rumsdkcompressintakerequests) - - [rum.sdk.enablePrivacyForActionName](#rumsdkenableprivacyforactionname) - - [rum.sdk.silentMultipleInit](#rumsdksilentmultipleinit) - - [rum.sdk.startSessionReplayRecordingManually](#rumsdkstartsessionreplayrecordingmanually) - - [rum.sdk.storeContextsAcrossPages](#rumsdkstorecontextsacrosspages) -- [Privacy Transforms](#privacy-transforms) - - [rum.privacy.include](#rumprivacyinclude) - - [rum.privacy.exclude](#rumprivacyexclude) - - [rum.privacy.addToDictionaryFunctionName](#rumprivacyaddtodictionaryfunctionname) - - [rum.privacy.helperCodeExpression](#rumprivacyhelpercodeexpression) -- [Source Code Context](#source-code-context) - - [rum.sourceCodeContext.service](#rumsourcecodecontextservice) - - [rum.sourceCodeContext.version](#rumsourcecodecontextversion) - - -## Configuration - -```ts -rum?: { - enable?: boolean; - sdk?: { - applicationId: string; - clientToken?: string; - site?: string; - sessionSampleRate?: number; - sessionReplaySampleRate?: number; - defaultPrivacyLevel?: string; - trackUserInteractions?: boolean; - trackResources?: boolean; - trackLongTasks?: boolean; - trackViewsManually?: boolean; - trackingConsent?: string; - traceSampleRate?: number; - telemetrySampleRate?: number; - allowUntrustedEvents?: boolean; - compressIntakeRequests?: boolean; - enablePrivacyForActionName?: boolean; - silentMultipleInit?: boolean; - startSessionReplayRecordingManually?: boolean; - storeContextsAcrossPages?: boolean; - }; - privacy?: { - include?: (string | RegExp)[]; - exclude?: (string | RegExp)[]; - addToDictionaryFunctionName?: string; - helperCodeExpression?: string; - }; - sourceCodeContext?: { - service: string; - version?: string; - }; -} -``` - -### rum.enable - -> default: `true` when a `rum` config block is present, `false` otherwise. - -Enable or disable the plugin without removing its configuration. - -Must be a boolean. Non-boolean values are coerced today but will be rejected in a future major release. - -## RUM SDK Injection - -Automatically inject the Datadog RUM Browser SDK into your application at build time. When the `rum.sdk` block is provided, the plugin injects initialization code so you don't need to add the SDK script tag or call `datadogRum.init()` manually. - -> [!NOTE] -> If `clientToken` is not provided, the plugin will attempt to fetch it automatically using `auth.apiKey` and `auth.appKey`. - -### rum.sdk.applicationId - -> required - -The RUM application ID from Datadog. - -### rum.sdk.clientToken - -> optional — fetched automatically when `auth.apiKey` and `auth.appKey` are set. - -The client token used by the RUM SDK to send data to Datadog. - -### rum.sdk.site - -> default: value of `auth.site` or `'datadoghq.com'` - -The Datadog site to send RUM data to. - -### rum.sdk.sessionSampleRate - -> default: `100` - -Percentage of sessions to track (0–100). - -### rum.sdk.sessionReplaySampleRate - -> default: `0` - -Percentage of tracked sessions that include Session Replay recordings (0–100). - -### rum.sdk.defaultPrivacyLevel - -> default: `'mask'` - -Default privacy level for Session Replay. Controls how content is masked in recordings. - -### rum.sdk.trackUserInteractions - -> default: `false` - -Automatically collect user actions (clicks). - -### rum.sdk.trackResources - -> default: `false` - -Automatically collect resource events. - -### rum.sdk.trackLongTasks - -> default: `false` - -Automatically collect long task events. - -### rum.sdk.trackViewsManually - -> default: `false` - -When `true`, RUM views must be started manually via the SDK API. - -### rum.sdk.trackingConsent - -> default: `'granted'` - -Initial tracking consent. Use `'not-granted'` to defer collection until consent is given. - -### rum.sdk.traceSampleRate - -> default: `100` - -Percentage of requests to trace (0–100). Controls APM trace correlation. - -### rum.sdk.telemetrySampleRate - -> default: `20` - -Percentage of telemetry events sent to Datadog for SDK health monitoring. - -### rum.sdk.allowUntrustedEvents - -> default: `false` - -Allow the SDK to capture programmatically dispatched (non-user) events. - -### rum.sdk.compressIntakeRequests - -> default: `false` - -Compress data sent to the Datadog intake to reduce network bandwidth. - -### rum.sdk.enablePrivacyForActionName - -> default: `false` - -When `true`, action names in Session Replay are masked for privacy. - -### rum.sdk.silentMultipleInit - -> default: `false` - -Suppress console warnings when `datadogRum.init()` is called more than once. - -### rum.sdk.startSessionReplayRecordingManually - -> default: `false` - -When `true`, Session Replay recording must be started manually via the SDK API. - -### rum.sdk.storeContextsAcrossPages - -> default: `false` - -Persist global and user contexts across page navigations using `localStorage`. - -## Privacy Transforms - -Build-time code transforms that prepare your application for Session Replay privacy controls. When the `rum.privacy` block is provided, the plugin transforms source files to support action name masking and other privacy features. - -### rum.privacy.include - -> default: `[/\.(?:c|m)?(?:j|t)sx?$/]` - -Array of file patterns (strings or RegExp) to include for privacy transforms. By default, all JavaScript and TypeScript files are included. - -### rum.privacy.exclude - -> default: `[/\/node_modules\//, /\.preval\./, /^[!@#$%^&*()=+~` + "`" + `-]/]` - -Array of file patterns (strings or RegExp) to exclude from privacy transforms. By default, `node_modules`, `.preval.` files, and files starting with special characters are excluded. - -### rum.privacy.addToDictionaryFunctionName - -> default: `'$'` - -The function name injected into transformed code to register strings with the privacy dictionary. - -### rum.privacy.helperCodeExpression - -> default: an IIFE that creates and manages the privacy dictionary queue on `globalThis`. - -Custom JavaScript expression for the privacy helper code. Override this only if you need to customize how the privacy dictionary queue is initialized. - -## Source Code Context - -Inject source code context metadata so Datadog can link RUM errors to the correct source version. - -### rum.sourceCodeContext.service - -> required - -The service name to associate with the source code context. Used by Datadog to link RUM data to the correct service. - -### rum.sourceCodeContext.version - -> optional - -The version string to associate with this build. When omitted, Datadog uses other available version information. diff --git a/packages/plugins/rum/src/index.test.ts b/packages/plugins/rum/src/index.test.ts index 8fe18ad6d..35306fab8 100644 --- a/packages/plugins/rum/src/index.test.ts +++ b/packages/plugins/rum/src/index.test.ts @@ -53,28 +53,11 @@ describe('RUM Plugin', () => { ]; describe('getPlugins', () => { const injectMock = jest.fn(); - test('Should not initialize the plugin if disabled', async () => { + test('Should initialize the plugin', async () => { getPlugins( getGetPluginsArg( { rum: { - enable: false, - sdk: { applicationId: 'app-id', clientToken: '123' }, - }, - }, - { inject: injectMock }, - ), - ); - getPlugins(getGetPluginsArg({}, { inject: injectMock })); - expect(injectMock).not.toHaveBeenCalled(); - }); - - test('Should initialize the plugin if enabled', async () => { - getPlugins( - getGetPluginsArg( - { - rum: { - enable: true, sdk: { applicationId: 'app-id', clientToken: '123' }, }, }, diff --git a/packages/plugins/rum/src/index.ts b/packages/plugins/rum/src/index.ts index 0bf029795..fbeafdb2c 100644 --- a/packages/plugins/rum/src/index.ts +++ b/packages/plugins/rum/src/index.ts @@ -28,15 +28,9 @@ export type types = { export const getPlugins: GetPlugins = ({ options, context }) => { const log = context.getLogger(PLUGIN_NAME); - // Verify configuration. const validatedOptions = validateOptions(options, log); const plugins: PluginOptions[] = []; - // If the plugin is not enabled, return an empty array. - if (!validatedOptions.enable) { - return plugins; - } - if (validatedOptions.sourceCodeContext) { context.inject({ type: 'code', diff --git a/packages/plugins/rum/src/types.ts b/packages/plugins/rum/src/types.ts index 1a8e6bfd6..23986ca9c 100644 --- a/packages/plugins/rum/src/types.ts +++ b/packages/plugins/rum/src/types.ts @@ -60,7 +60,6 @@ export type SDKOptionsWithDefaults = Assign< >; export type RumOptionsWithDefaults = { - enable?: boolean; sdk?: SDKOptionsWithDefaults; privacy?: PrivacyOptionsWithDefaults; sourceCodeContext?: SourceCodeContextOptions; diff --git a/packages/plugins/rum/src/validate.test.ts b/packages/plugins/rum/src/validate.test.ts index 533c9467d..9da4a92c6 100644 --- a/packages/plugins/rum/src/validate.test.ts +++ b/packages/plugins/rum/src/validate.test.ts @@ -2,75 +2,10 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { resetEnableWarnings } from '@dd/core/helpers/options'; -import type { Logger } from '@dd/core/types'; import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks'; import { createFilter } from '@rollup/pluginutils'; -import { - validateOptions, - validatePrivacyOptions, - validateSourceCodeContextOptions, -} from './validate'; - -const mockLogger: Logger = { - getLogger: jest.fn(() => mockLogger), - time: jest.fn() as unknown as Logger['time'], - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - debug: jest.fn(), -}; - -beforeEach(() => { - jest.clearAllMocks(); - resetEnableWarnings(); -}); - -describe('validateOptions', () => { - describe('enable flag', () => { - const cases = [ - { - description: 'return false when no rum config is provided', - input: { ...defaultPluginOptions }, - expected: false, - }, - { - description: 'return true when rum config is an empty object', - input: { ...defaultPluginOptions, rum: {} }, - expected: true, - }, - { - description: 'respect explicit enable true', - input: { ...defaultPluginOptions, rum: { enable: true } }, - expected: true, - }, - { - description: 'respect explicit enable false', - input: { ...defaultPluginOptions, rum: { enable: false } }, - expected: false, - }, - ]; - - test.each(cases)('Should $description', ({ input, expected }) => { - const result = validateOptions(input, mockLogger); - expect(result.enable).toBe(expected); - }); - }); - - describe('enable deprecation warning for non-boolean values', () => { - test('Should coerce non-boolean enable and warn', () => { - const input = { ...defaultPluginOptions, rum: { enable: 1 } }; - const result = validateOptions( - input as unknown as Parameters[0], - mockLogger, - ); - expect(result.enable).toBe(true); - expect(mockLogger.warn).toHaveBeenCalledTimes(1); - expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('rum.enable')); - }); - }); -}); +import { validatePrivacyOptions, validateSourceCodeContextOptions } from './validate'; describe('Test privacy plugin option exclude regex', () => { let filter: (path: string) => boolean; diff --git a/packages/plugins/rum/src/validate.ts b/packages/plugins/rum/src/validate.ts index b96b2e992..ccaa994d5 100644 --- a/packages/plugins/rum/src/validate.ts +++ b/packages/plugins/rum/src/validate.ts @@ -3,7 +3,6 @@ // Copyright 2019-Present Datadog, Inc. import type { Site } from '@datadog/browser-rum'; -import { resolveEnable } from '@dd/core/helpers/options'; import type { Logger, Options, OptionsWithDefaults } from '@dd/core/types'; import chalk from 'chalk'; @@ -40,7 +39,6 @@ export const validateOptions = ( // Build the final configuration. const toReturn: RumOptionsWithDefaults = { ...options[CONFIG_KEY], - enable: resolveEnable(options, CONFIG_KEY, log), sdk: undefined, privacy: undefined, sourceCodeContext: undefined, diff --git a/packages/tools/src/commands/create-plugin/templates.ts b/packages/tools/src/commands/create-plugin/templates.ts index ba14c5621..737dcaa7c 100644 --- a/packages/tools/src/commands/create-plugin/templates.ts +++ b/packages/tools/src/commands/create-plugin/templates.ts @@ -45,23 +45,14 @@ export const getFiles = (context: Context): File[] => { // Deal with validation and defaults here. export const validateOptions = (options: Options): ${pascalCase}OptionsWithDefaults => { - const validatedOptions: ${pascalCase}OptionsWithDefaults = { - // By using an empty object, we consider the plugin as enabled. - enable: !!options[CONFIG_KEY], + return { ...options[CONFIG_KEY] }; - return validatedOptions; }; export const getPlugins: GetPlugins = ({ options, context }) => { - // Verify configuration. const validatedOptions = validateOptions(options); - // If the plugin is not enabled, return an empty array. - if (!validatedOptions.enable) { - return []; - } - const log = context.getLogger(PLUGIN_NAME); return [ @@ -82,7 +73,7 @@ export const getFiles = (context: Context): File[] => { enable?: boolean; }; - export type ${pascalCase}OptionsWithDefaults = Required<${pascalCase}Options>; + export type ${pascalCase}OptionsWithDefaults = Required>; `; }, }, @@ -95,12 +86,7 @@ export const getFiles = (context: Context): File[] => { describe('${title} Plugin', () => { describe('getPlugins', () => { - test('Should not initialize the plugin if not enabled', async () => { - expect(getPlugins(getGetPluginsArg({ ${camelCase}: { enable: false } }))).toHaveLength(0); - expect(getPlugins(getGetPluginsArg())).toHaveLength(0); - }); - - test('Should initialize the plugin if enabled', async () => { + test('Should initialize the plugin', async () => { expect(getPlugins(getGetPluginsArg({ ${camelCase}: { enable: true } }))).toHaveLength(1); }); }); diff --git a/packages/tools/src/commands/integrity/files.ts b/packages/tools/src/commands/integrity/files.ts index a682c1912..3269c5e0c 100644 --- a/packages/tools/src/commands/integrity/files.ts +++ b/packages/tools/src/commands/integrity/files.ts @@ -101,7 +101,7 @@ const updateFactory = async (plugins: Workspace[]) => { import * as ${camelCase} from '${plugin.name}'; `; typesExportContent += `export type { types as ${pascalCase}Types } from '${plugin.name}';`; - configContent += `['${cleanPluginName(plugin.name)}', ${camelCase}.getPlugins],`; + configContent += `['${cleanPluginName(plugin.name)}', ${configKeyVar}, ${camelCase}.getPlugins],`; // Only add helpers if they export them. if (pluginExports.helpers && Object.keys(pluginExports.helpers).length) {