Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ To interact with Datadog directly from your builds.
- [`enableGit`](#enablegit)
- [`logLevel`](#loglevel)
- [`metadata.name`](#metadataname)
- [`metadata.version`](#metadataversion)
- [Features](#features)
- [Error Tracking](#error-tracking-----)
- [Metrics](#metrics-----)
Expand Down Expand Up @@ -100,6 +101,7 @@ Follow the specific documentation for each bundler:
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'none',
metadata?: {
name?: string;
version?: string;
};
errorTracking?: {
enable?: boolean;
Expand Down Expand Up @@ -271,6 +273,12 @@ Which level of log do you want to show.
The name of the build.<br/>
This is used to identify the build in logs, metrics and spans.

### `metadata.version`
> default: `null`

An immutable identifier for the deployed build (typically a release tag, a git commit SHA, or a CI build ID).<br/>
This is the canonical place to declare the version once. Plugins that need a build version (for sourcemap upload, source-code resolution, runtime SDK initialization, etc.) read it from here unless they're given a more specific override.

## Features

<!-- #list-of-packages -->
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type Timer = {

export type BuildMetadata = {
name?: string;
version?: string;
};

export type BuildReport = {
Expand Down
71 changes: 71 additions & 0 deletions packages/factory/src/validate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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 { Options } from '@dd/core/types';

import { validateOptions } from './validate';

describe('factory validateOptions', () => {
describe('defaults', () => {
it('should return defaults when no options are provided', () => {
expect(validateOptions()).toEqual(
expect.objectContaining({
enableGit: true,
logLevel: 'warn',
metadata: {},
}),
);
});

it('should preserve user-provided metadata', () => {
const result = validateOptions({
metadata: { name: 'my-build', version: '1.0.0' },
});
expect(result.metadata).toEqual({ name: 'my-build', version: '1.0.0' });
});

it('should accept metadata with only name set', () => {
const result = validateOptions({ metadata: { name: 'my-build' } });
expect(result.metadata).toEqual({ name: 'my-build' });
});

it('should accept metadata with only version set', () => {
const result = validateOptions({ metadata: { version: '1.0.0' } });
expect(result.metadata).toEqual({ version: '1.0.0' });
});

it('should accept an empty metadata block', () => {
const result = validateOptions({ metadata: {} });
expect(result.metadata).toEqual({});
});
});

describe('metadata validation', () => {
const cases = [
{
description: 'reject metadata.version when not a string',
input: { metadata: { version: 123 } },
errorPattern: /metadata\.version.*must be a string/,
},
{
description: 'reject metadata.version when null',
input: { metadata: { version: null } },
errorPattern: /metadata\.version.*must be a string/,
},
];

test.each(cases)('should $description', ({ input, errorPattern }) => {
expect(() => validateOptions(input as unknown as Options)).toThrow(errorPattern);
});

it('should accept non-string metadata.name for backwards compatibility', () => {
expect(() =>
validateOptions({ metadata: { name: 123 } } as unknown as Options),
).not.toThrow();
expect(() =>
validateOptions({ metadata: { name: null } } as unknown as Options),
).not.toThrow();
});
});
});
28 changes: 27 additions & 1 deletion packages/factory/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,35 @@
// Copyright 2019-Present Datadog, Inc.

import { getDDEnvValue } from '@dd/core/helpers/env';
import type { AuthOptionsWithDefaults, Options, OptionsWithDefaults } from '@dd/core/types';
import type {
AuthOptionsWithDefaults,
BuildMetadata,
Options,
OptionsWithDefaults,
} from '@dd/core/types';

const validateMetadata = (metadata: BuildMetadata | undefined): string[] => {
const errors: string[] = [];
if (metadata === undefined) {
return errors;
}
// TODO(next-major): also reject non-string `metadata.name`. Skipped today
// because `metadata.name` has historically been unvalidated (and the root
// README documents its default as `null`), so adding a type-check here
// would be a breaking change for users who took the docs literally.
if (metadata.version !== undefined && typeof metadata.version !== 'string') {
errors.push('metadata.version must be a string');
}
return errors;
};

export const validateOptions = (options: Options = {}): OptionsWithDefaults => {
const errors: string[] = [...validateMetadata(options.metadata)];

if (errors.length) {
throw new Error(`Invalid Datadog plugin configuration:\n - ${errors.join('\n - ')}`);
}

const auth: AuthOptionsWithDefaults = {
// DATADOG_SITE env var takes precedence over configuration
site: getDDEnvValue('SITE') || options.auth?.site || 'datadoghq.com',
Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/error-tracking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,12 @@ Example: if you're uploading `dist/file.js` to `https://example.com/static/file.

### errorTracking.sourcemaps.releaseVersion

> required
> required (or set via [`metadata.version`](/README.md#metadataversion))

Is similar and will be used to match the `version` tag set on the RUM SDK.

If omitted, the plugin falls back to the shared top-level `metadata.version`. At least one of the two must be set.

### errorTracking.sourcemaps.service

> required
Expand Down
4 changes: 3 additions & 1 deletion packages/plugins/error-tracking/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export type SourcemapsOptions = {
dryRun?: boolean;
maxConcurrency?: number;
minifiedPathPrefix: MinifiedPathPrefix;
releaseVersion: string;
// Optional: when omitted, the validator falls back to the shared
// top-level `metadata.version`. At least one of the two must be set.
releaseVersion?: string;
service: string;
};

Expand Down
44 changes: 43 additions & 1 deletion packages/plugins/error-tracking/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('Error Tracking Plugins validate', () => {

expect(errors).toHaveLength(3);
const expectedErrors = [
'sourcemaps.releaseVersion is required.',
'sourcemaps.releaseVersion is required (set it directly or via metadata.version).',
'sourcemaps.service is required.',
'sourcemaps.minifiedPathPrefix is required.',
];
Expand Down Expand Up @@ -83,6 +83,48 @@ describe('Error Tracking Plugins validate', () => {
});
});

test('Should fall back to metadata.version when sourcemaps.releaseVersion is unset', () => {
const { config, errors } = validateSourcemapsOptions({
metadata: { version: '2.0.0' },
errorTracking: {
sourcemaps: getMinimalSourcemapsConfiguration({
releaseVersion: undefined,
}),
},
});

expect(errors).toHaveLength(0);
expect(config).toEqual(expect.objectContaining({ releaseVersion: '2.0.0' }));
});

test('Should prefer an explicit sourcemaps.releaseVersion over metadata.version', () => {
const { config, errors } = validateSourcemapsOptions({
metadata: { version: '2.0.0' },
errorTracking: {
sourcemaps: getMinimalSourcemapsConfiguration({
releaseVersion: '1.0.0',
}),
},
});

expect(errors).toHaveLength(0);
expect(config).toEqual(expect.objectContaining({ releaseVersion: '1.0.0' }));
});

test('Should error when neither sourcemaps.releaseVersion nor metadata.version is set', () => {
const { errors } = validateSourcemapsOptions({
errorTracking: {
sourcemaps: getMinimalSourcemapsConfiguration({
releaseVersion: undefined,
}),
},
});

expect(stripAnsi(errors[0])).toBe(
'sourcemaps.releaseVersion is required (set it directly or via metadata.version).',
);
});

test('Should return an error with a bad minifiedPathPrefix', () => {
const { errors } = validateSourcemapsOptions({
errorTracking: {
Expand Down
53 changes: 33 additions & 20 deletions packages/plugins/error-tracking/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,36 +72,49 @@ export const validateSourcemapsOptions = (
};

if (validatedOptions.sourcemaps) {
const sourcemapsCfg = validatedOptions.sourcemaps;

// Resolve `releaseVersion`: prefer the plugin-specific option, then
// fall back to the shared top-level `metadata.version`. Letting users
// configure one canonical build version at the top level keeps every
// consumer (live-debugger, sourcemaps, …) reading from the same place.
const releaseVersion = sourcemapsCfg.releaseVersion || config.metadata?.version;

// Validate the configuration.
if (!validatedOptions.sourcemaps.releaseVersion) {
toReturn.errors.push(`${red('sourcemaps.releaseVersion')} is required.`);
if (!releaseVersion) {
toReturn.errors.push(
`${red('sourcemaps.releaseVersion')} is required (set it directly or via ${red('metadata.version')}).`,
);
}
if (!validatedOptions.sourcemaps.service) {
if (!sourcemapsCfg.service) {
toReturn.errors.push(`${red('sourcemaps.service')} is required.`);
}
if (!validatedOptions.sourcemaps.minifiedPathPrefix) {
if (!sourcemapsCfg.minifiedPathPrefix) {
toReturn.errors.push(`${red('sourcemaps.minifiedPathPrefix')} is required.`);
}

// Validate the minifiedPathPrefix.
if (validatedOptions.sourcemaps.minifiedPathPrefix) {
if (!validateMinifiedPathPrefix(validatedOptions.sourcemaps.minifiedPathPrefix)) {
toReturn.errors.push(
`${red('sourcemaps.minifiedPathPrefix')} must be a valid URL or start with '/'.`,
);
}
if (
sourcemapsCfg.minifiedPathPrefix &&
!validateMinifiedPathPrefix(sourcemapsCfg.minifiedPathPrefix)
) {
toReturn.errors.push(
`${red('sourcemaps.minifiedPathPrefix')} must be a valid URL or start with '/'.`,
);
}

// Add the defaults.
const sourcemapsWithDefaults: SourcemapsOptionsWithDefaults = {
bailOnError: false,
dryRun: false,
maxConcurrency: 20,
...validatedOptions.sourcemaps,
};

// Save the config.
toReturn.config = sourcemapsWithDefaults;
// Build the resolved config only when `releaseVersion` actually
// resolves; otherwise an error has been recorded and the caller will
// throw before the config is read.
if (releaseVersion) {
Comment on lines +106 to +109
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this if () {} wrapping is really necessary.
It brings more confusion when reading the code than anything.

Even the comment tends to explain that it's actually unnecessary since it would throw anyways.

toReturn.config = {
bailOnError: false,
dryRun: false,
maxConcurrency: 20,
...sourcemapsCfg,
releaseVersion,
};
}
}

return toReturn;
Expand Down
25 changes: 18 additions & 7 deletions packages/plugins/live-debugger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Automatically instrument JavaScript functions at build time to enable Live Debug
- [Configuration](#configuration)
- [How it works](#how-it-works)
- [liveDebugger.enable](#livedebuggerenable)
- [liveDebugger.version](#livedebuggerversion)
- [metadata.version](#metadataversion)
- [liveDebugger.include](#livedebuggerinclude)
- [liveDebugger.exclude](#livedebuggerexclude)
- [liveDebugger.honorSkipComments](#livedebuggerhonorskipcomments)
Expand Down Expand Up @@ -51,7 +51,6 @@ the plugin throws an error with the exact install command above.
```ts
liveDebugger?: {
enable?: boolean;
version?: string;
include?: (string | RegExp)[];
exclude?: (string | RegExp)[];
honorSkipComments?: boolean;
Expand All @@ -60,6 +59,8 @@ liveDebugger?: {
}
```

Live Debugger also reads the build version from the top-level [`metadata.version`](#metadataversion) option.

## How it works

The Live Debugger plugin automatically instruments all JavaScript functions in your application at build time. It adds lightweight checks that can be activated at runtime without rebuilding your code.
Expand All @@ -74,7 +75,7 @@ Each instrumented function gets:

The instrumentation checks whether probes are active by calling `$dd_probes(functionId)`. When no probes are active, the function returns `undefined` and all instrumentation is skipped — only the `$dd_probes` call and a conditional check remain on the hot path.

When `liveDebugger.version` is set, it should match the immutable deployed build identifier used by your Browser Debugger SDK initialization. If you also upload sourcemaps through the Error Tracking plugin, use the same value for `errorTracking.sourcemaps.releaseVersion`.
When `metadata.version` is set, it should match the immutable deployed build identifier used by your Browser Debugger SDK initialization. If you also upload sourcemaps through the Error Tracking plugin, use the same value for `errorTracking.sourcemaps.releaseVersion`.

**Example transformation (block body):**

Expand Down Expand Up @@ -126,12 +127,22 @@ const double = (x) => {

Enable or disable the plugin without removing its configuration.

### liveDebugger.version
### metadata.version

> default: `undefined`

An immutable identifier for the deployed browser build. Set it at the top level of your Datadog plugin configuration:

```ts
datadogBuildPlugins({
metadata: { version: '1.0.0' },
liveDebugger: {},
});
```

Optional. When set, use an immutable deployed browser build identifier. This value should match:
If both `metadata.version` and an explicit `errorTracking.sourcemaps.releaseVersion` are configured and disagree, this plugin surfaces the mismatch as a build error.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is super relevant for the Live Debugger plugin's documentation.


- the `version` passed to `@datadog/browser-debugger`
- `errorTracking.sourcemaps.releaseVersion` when sourcemap upload is enabled
When set, Live Debugger injects the value into runtime-visible build metadata so the Browser Debugger SDK uses it as the default `version` during `init()`.

If omitted, Live Debugger instrumentation still works, but browser build lookup and source-code-aware resolution will gracefully degrade.

Expand Down
8 changes: 0 additions & 8 deletions packages/plugins/live-debugger/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,3 @@ export const PLUGIN_NAME: PluginName = 'datadog-live-debugger-plugin' as const;

// Skip instrumentation comment
export const SKIP_INSTRUMENTATION_COMMENT = '@dd-no-instrumentation';

// Minimal no-op stub injected into all chunks as a banner.
// $dd_probes is called unconditionally by every instrumented function;
// $dd_entry, $dd_return, and $dd_throw are guarded by `if (probe)` so
// they only need to exist once the SDK activates probes.
// When the Datadog Browser Debugger SDK loads, its init() overwrites
// $dd_probes with the real implementation.
export const RUNTIME_STUBS = `if(typeof globalThis.$dd_probes==='undefined'){globalThis.$dd_probes=function(){}}`;
Loading
Loading