Skip to content
Draft
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
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ To interact with Datadog directly from your builds.
- [`gitRepositoryUrl`](#gitrepositoryurl)
- [`logLevel`](#loglevel)
- [`metadata.name`](#metadataname)
- [`metadata.service`](#metadataservice)
- [`metadata.version`](#metadataversion)
- [`sourcemaps`](#sourcemaps)
- [Features](#features)
Expand Down Expand Up @@ -105,6 +106,7 @@ Follow the specific documentation for each bundler:
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'none',
metadata?: {
name?: string;
service?: string;
version?: string;
};
sourcemaps?:
Expand Down Expand Up @@ -178,7 +180,7 @@ Follow the specific documentation for each bundler:
}
| {
debugId?: false;
service: string;
service?: string;
version?: string;
};
};
Expand Down Expand Up @@ -344,6 +346,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.service`
> default: `null`

The service of the deployed application, as set in the Datadog browser SDKs.<br/>
This is the canonical place to declare the service once. Plugins that need a service (Live Debugger's symbol table upload, RUM source code context, source map upload, etc.) read it from here unless they're given a more specific override.

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

Expand Down Expand Up @@ -502,7 +510,7 @@ datadogWebpackPlugin({
}
| {
debugId?: false,
service: string,
service?: string,
version?: string,
},
}
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/helpers/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ describe('plugins', () => {
input: { ...pluginOptions, enableGit: false },
result: false,
},
{
description: 'true with Live Debugger and an API key',
input: { ...pluginOptions, liveDebugger: {} },
result: true,
},
{
description: 'false with Live Debugger and no API key',
input: { ...pluginOptions, auth: { site: 'datadoghq.com' }, liveDebugger: {} },
result: false,
},
{
description: 'false with Live Debugger disabled',
input: { ...pluginOptions, liveDebugger: { enable: false } },
result: false,
},
{
description: 'false if disabled globaly with Live Debugger',
input: { ...pluginOptions, enableGit: false, liveDebugger: {} },
result: false,
},
{
description: 'false if disabled globaly with sourcemaps',
input: {
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/helpers/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,15 @@ export const unserializeBuildReport = (report: SerializedBuildReport): BuildRepo
};

// Verify that we should get the git information based on the options.
// Only get git information if sourcemaps are enabled and git is enabled.
// Only get git information if something uploads it and git is enabled.
export const shouldGetGitInfo = (options: Options): boolean => {
// If we don't have sourcemaps enabled, we don't need git.
// Source map uploads need git.
const gitEnabledFromSourcemaps = !!options.errorTracking?.sourcemaps;
// Live Debugger symbol table uploads need git, and only happen with an API key.
const liveDebuggerConfig = options.liveDebugger;
const isLiveDebuggerEnabled = !!liveDebuggerConfig && !!(liveDebuggerConfig.enable ?? true);
const gitEnabledFromLiveDebugger = isLiveDebuggerEnabled && !!options.auth?.apiKey;
// If we have the 'enableGit' configuration at the root, use it and default to `true`.
const gitEnabledFromRoot = options.enableGit ?? true;
return gitEnabledFromSourcemaps && gitEnabledFromRoot;
return (gitEnabledFromSourcemaps || gitEnabledFromLiveDebugger) && gitEnabledFromRoot;
};
6 changes: 6 additions & 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;
service?: string;
version?: string;
};

Expand Down Expand Up @@ -198,10 +199,15 @@ export type GlobalContext = {
env: GlobalData['env'];
getLogger: GetLogger;
git?: RepositoryData;
// Pending while the git plugin collects git data. Resolved once it's done, with or without data.
gitPending: boolean;
gitReady: Promise<void>;
hook: TriggerHook<void>;
inject: (item: ToInjectItem) => void;
markArtifactsPending: () => void;
markArtifactsReady: (error?: unknown) => void;
markGitPending: () => void;
markGitReady: () => void;
pluginNames: string[];
plugins: (PluginOptions | CustomPluginOptions)[];
queue: (promise: Promise<any>) => void;
Expand Down
24 changes: 24 additions & 0 deletions packages/factory/src/helpers/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export const getContext = ({
buildRoot,
env: data.env,
getLogger: getLoggerFactory(data, stores, options.logLevel),
gitPending: false,
gitReady: Promise.resolve(),
// This will be updated in the injection plugin on initialization.
asyncHook: () => {
throw new Error('AsyncHook function called before it was initialized.');
Expand All @@ -65,6 +67,8 @@ export const getContext = ({
},
markArtifactsPending: () => {},
markArtifactsReady: () => {},
markGitPending: () => {},
markGitReady: () => {},
plugins: [],
// This will be updated in the async-queue plugin on initialization.
queue: () => {
Expand Down Expand Up @@ -105,5 +109,25 @@ export const getContext = ({
}
};

let resolveGit: (() => void) | undefined;

context.markGitPending = () => {
if (resolveGit) {
return;
}

context.gitReady = new Promise<void>((resolve) => {
resolveGit = resolve;
});
context.gitPending = true;
};

context.markGitReady = () => {
const resolve = resolveGit;
resolveGit = undefined;
context.gitPending = false;
resolve?.();
};

return context;
};
10 changes: 10 additions & 0 deletions packages/factory/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ describe('factory validateOptions', () => {
expect(result.metadata).toEqual({ version: '1.0.0' });
});

it('should accept metadata with a service', () => {
const result = validateOptions({ metadata: { service: 'checkout' } });
expect(result.metadata).toEqual({ service: 'checkout' });
});

it('should accept an empty metadata block', () => {
const result = validateOptions({ metadata: {} });
expect(result.metadata).toEqual({});
Expand Down Expand Up @@ -284,6 +289,11 @@ describe('factory validateOptions', () => {
input: { metadata: { version: null } },
errorPattern: /metadata\.version.*must be a string/,
},
{
description: 'reject metadata.service when not a string',
input: { metadata: { service: 123 } },
errorPattern: /metadata\.service.*must be a string/,
},
];

test.each(cases)('should $description', ({ input, errorPattern }) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/factory/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const validateMetadata = (metadata: BuildMetadata | undefined): string[] => {
if (metadata.version !== undefined && typeof metadata.version !== 'string') {
errors.push('metadata.version must be a string');
}
if (metadata.service !== undefined && typeof metadata.service !== 'string') {
errors.push('metadata.service must be a string');
}
return errors;
};

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 @@ -132,10 +132,12 @@ If omitted, the plugin falls back to the shared top-level `metadata.version`. At

### errorTracking.sourcemaps.service

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

Should be set as the name of the service you're uploading sourcemaps for, and Datadog will use this service name to find the corresponding sourcemaps based on the `service` tag set on the RUM SDK.

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

> [!NOTE]
>
> If you get the error `Error: No git remotes available`,
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 @@ -24,7 +24,9 @@ type ServiceVersionSourcemapsOptions = SourcemapsUploadOptions & {
// 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;
// Optional: when omitted, the validator falls back to the shared
// top-level `metadata.service`. At least one of the two must be set.
service?: string;
};

export type SourcemapsOptions = DebugIdSourcemapsOptions | ServiceVersionSourcemapsOptions;
Expand Down
30 changes: 29 additions & 1 deletion packages/plugins/error-tracking/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('Error Tracking Plugins validate', () => {
expect(errors).toHaveLength(3);
const expectedErrors = [
'sourcemaps.releaseVersion is required (set it directly or via metadata.version).',
'sourcemaps.service is required.',
'sourcemaps.service is required (set it directly or via metadata.service).',
'sourcemaps.minifiedPathPrefix is required.',
];
expect(errors.map(stripAnsi)).toEqual(expectedErrors);
Expand Down Expand Up @@ -247,6 +247,34 @@ describe('Error Tracking Plugins validate', () => {
);
});

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

expect(errors).toHaveLength(0);
expect(config).toEqual(expect.objectContaining({ service: 'checkout' }));
});

test('Should prefer an explicit sourcemaps.service over metadata.service', () => {
const { config, errors } = validateSourcemapsOptions({
metadata: { service: 'checkout' },
errorTracking: {
sourcemaps: getMinimalSourcemapsConfiguration({
service: 'cart',
}),
},
});

expect(errors).toHaveLength(0);
expect(config).toEqual(expect.objectContaining({ service: 'cart' }));
});

test('Should return an error with a bad minifiedPathPrefix', () => {
const { errors } = validateSourcemapsOptions({
errorTracking: {
Expand Down
17 changes: 11 additions & 6 deletions packages/plugins/error-tracking/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export const validateSourcemapsOptions = (
// 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;
// Same for `service`, with the shared top-level `metadata.service`.
const service = sourcemapsCfg.service || config.metadata?.service;

// Validate the configuration.
if (!releaseVersion) {
Expand All @@ -128,8 +130,10 @@ export const validateSourcemapsOptions = (
`${red('sourcemaps.releaseVersion')} must match ${red('metadata.version')} when both are configured.`,
);
}
if (!sourcemapsCfg.service) {
toReturn.errors.push(`${red('sourcemaps.service')} is required.`);
if (!service) {
toReturn.errors.push(
`${red('sourcemaps.service')} is required (set it directly or via ${red('metadata.service')}).`,
);
}
if (!sourcemapsCfg.minifiedPathPrefix) {
toReturn.errors.push(`${red('sourcemaps.minifiedPathPrefix')} is required.`);
Expand All @@ -145,17 +149,18 @@ export const validateSourcemapsOptions = (
);
}

// 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) {
// Build the resolved config only when `releaseVersion` and `service`
// actually resolve; otherwise an error has been recorded and the caller
// will throw before the config is read.
if (releaseVersion && service) {
toReturn.config = {
bailOnError: false,
dryRun: false,
maxConcurrency: 20,
...sourcemapsCfg,
debugId: false,
releaseVersion,
service,
};
}

Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/git/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ export const getGitPlugins: GetInternalPlugins = (arg: GetPluginsArg) => {

const repositoryData = await getRepositoryData(git, repositoryUrl);
context.git = repositoryData;
// Let plugins that wait for git data continue without waiting for the hook's handlers.
context.markGitReady();

timeGit.end();
await context.asyncHook('git', context.git);
} catch (e: any) {
log.error(`Could not get git information: ${e.message}`);
} finally {
context.markGitReady();
}
};

Expand All @@ -59,6 +63,7 @@ export const getGitPlugins: GetInternalPlugins = (arg: GetPluginsArg) => {

// buildRoot hook is sync because xpack can't make it async.
// So we queue the async part of the plugin.
context.markGitPending();
context.queue(processGit(gitDir));
} catch (e: any) {
// We don't want to have the build fail for this.
Expand Down
6 changes: 3 additions & 3 deletions packages/plugins/live-debugger/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ When adding or changing `liveDebugger` configuration, update [`src/types.ts`](./

## Symbol table

The plugin can list the instrumented functions that ship in a build, for upload to Datadog's Symbol Database. Each entry has the function ID, a display name, the path relative to the git repository root, and the original line range. The table lives in [`src/symbol-table`](./src/symbol-table).
The plugin lists the instrumented functions that ship in a build and uploads them to Datadog's Symbol Database. Each entry has the function ID, a display name, the path relative to the git repository root, and the original line range. The code lives in [`src/symbol-table`](./src/symbol-table).

For now, the only consumer is an opt-in debug dump. Set `DD_LIVE_DEBUGGER_SYMBOL_TABLE_PATH` to a file path to generate the table and write it there:
The upload is on by default, and the README lists [what it needs](./README.md#symbol-upload). When it can't happen, the table isn't generated either, so the build does no extra work. To inspect the table, set `DD_LIVE_DEBUGGER_SYMBOL_TABLE_PATH` to a file path: the plugin then also writes the table there, in the format of the upload's file part.

```bash
DD_LIVE_DEBUGGER_SYMBOL_TABLE_PATH=/tmp/symbol-table.json yarn build
```

Without that variable, the plugin doesn't add any hook or build time. The dump is never written inside the bundler's output directory, and generation never fails the build: problems are logged as warnings.
The dump is never written inside the bundler's output directory, and neither generation nor upload ever fails the build: problems are logged as warnings.

How it works:

Expand Down
Loading
Loading