Skip to content
Merged
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
108 changes: 92 additions & 16 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.
- [`logLevel`](#loglevel)
- [`metadata.name`](#metadataname)
- [`metadata.version`](#metadataversion)
- [`sourcemaps`](#sourcemaps)
- [Features](#features)
- [Error Tracking](#error-tracking-----)
- [Metrics](#metrics-----)
Expand Down Expand Up @@ -104,16 +105,36 @@ Follow the specific documentation for each bundler:
name?: string;
version?: string;
};
sourcemaps?:
| {
debugId: true;
upload?: false;
}
| {
bailOnError?: boolean;
debugId: true;
dryRun?: boolean;
maxConcurrency?: number;
upload: true;
};
errorTracking?: {
enable?: boolean;
sourcemaps?: {
bailOnError?: boolean;
dryRun?: boolean;
maxConcurrency?: number;
minifiedPathPrefix: string;
releaseVersion: string;
service: string;
};
sourcemaps?:
| {
bailOnError?: boolean;
debugId: true;
dryRun?: boolean;
maxConcurrency?: number;
}
| {
bailOnError?: boolean;
debugId?: false;
dryRun?: boolean;
maxConcurrency?: number;
minifiedPathPrefix: string;
releaseVersion?: string;
service: string;
};
};
metrics?: {
enable?: boolean;
Expand Down Expand Up @@ -145,6 +166,17 @@ Follow the specific documentation for each bundler:
clientToken?: string;
// [...] See https://docs.datadoghq.com/real_user_monitoring/browser/setup/client?tab=rum#configuration for all options.
};
sourceCodeContext?:
| {
debugId: true;
service?: string;
version?: string;
}
| {
debugId?: false;
service: string;
version?: string;
};
};
}
```
Expand Down Expand Up @@ -297,6 +329,31 @@ This is used to identify the build in logs, metrics and spans.
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.

### `sourcemaps`

> default: `null`

Inject a debug ID into each JavaScript bundle and, optionally, upload the corresponding source maps during the build.

```typescript
{
auth: {
apiKey: process.env.DATADOG_API_KEY,
site: 'datadoghq.com',
},
sourcemaps: {
debugId: true,
upload: true,
},
}
```

Setting `debugId: true` enables injection. Set `upload: true` to upload the source maps directly from the build plugin. Uploading requires `auth.apiKey` or the `DATADOG_API_KEY` environment variable.

The `bailOnError`, `dryRun`, and `maxConcurrency` upload options are available when `upload` is `true`.

Existing `rum.sourceCodeContext` and `errorTracking.sourcemaps` configurations remain supported. The top-level `sourcemaps` option can be combined with `rum.sourceCodeContext.service` and `rum.sourceCodeContext.version`; this metadata is independent of debug ID matching. Do not combine the top-level option with `errorTracking.sourcemaps` or with `rum.sourceCodeContext.debugId: false`.

## Features

<!-- #list-of-packages -->
Expand All @@ -314,14 +371,22 @@ This is the canonical place to declare the version once. Plugins that need a bui
datadogWebpackPlugin({
errorTracking?: {
enable?: boolean,
sourcemaps?: {
bailOnError?: boolean,
dryRun?: boolean,
maxConcurrency?: number,
minifiedPathPrefix: string,
releaseVersion: string,
service: string,
},
sourcemaps?:
| {
bailOnError?: boolean,
debugId: true,
dryRun?: boolean,
maxConcurrency?: number,
}
| {
bailOnError?: boolean,
debugId?: false,
dryRun?: boolean,
maxConcurrency?: number,
minifiedPathPrefix: string,
releaseVersion?: string,
service: string,
},
}
});
```
Expand Down Expand Up @@ -407,6 +472,17 @@ datadogWebpackPlugin({
clientToken?: string,
// [...] See https://docs.datadoghq.com/real_user_monitoring/browser/setup/client?tab=rum#configuration for all options.
},
sourceCodeContext?:
| {
debugId: true,
service?: string,
version?: string,
}
| {
debugId?: false,
service: string,
version?: string,
},
}
});
```
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,24 @@ export interface BaseOptions {
logLevel?: LogLevel;
}

type SourcemapsUploadEnabledOptions = {
upload: true;
bailOnError?: boolean;
dryRun?: boolean;
maxConcurrency?: number;
};

type SourcemapsInjectionOnlyOptions = {
upload?: false;
bailOnError?: never;
dryRun?: never;
maxConcurrency?: never;
};

export type SourcemapsOptions = {
debugId: true;
} & (SourcemapsUploadEnabledOptions | SourcemapsInjectionOnlyOptions);

export interface Options extends BaseOptions {
// Each product should have a unique entry.
// #types-injection-marker
Expand All @@ -290,6 +308,7 @@ export interface Options extends BaseOptions {
[output.CONFIG_KEY]?: OutputOptions;
[rum.CONFIG_KEY]?: RumOptions;
// #types-injection-marker
sourcemaps?: SourcemapsOptions;
customPlugins?: GetCustomPlugins;
}

Expand Down
28 changes: 28 additions & 0 deletions packages/factory/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Copyright 2019-Present Datadog, Inc.

import type { PluginOptions, Options } from '@dd/core/types';
import { PLUGIN_NAME as ERROR_TRACKING_PLUGIN_NAME } from '@dd/error-tracking-plugin';
import { buildPluginFactory } from '@dd/factory';

const invokeFactory = (opts: Options): PluginOptions[] => {
Expand Down Expand Up @@ -56,6 +57,33 @@ describe('Factory', () => {
expect(hasPlugin(plugins, 'output')).toBe(true);
});

test('Should include error tracking for RUM debug ID uploads', () => {
const plugins = invokeFactory({
auth: { apiKey: '123' },
errorTracking: { sourcemaps: { debugId: true } },
logLevel: 'none',
rum: { sourceCodeContext: { debugId: true } },
});
expect(hasPlugin(plugins, ERROR_TRACKING_PLUGIN_NAME)).toBe(true);
});

test('Should not upload from the top-level sourcemaps option by default', () => {
const plugins = invokeFactory({
logLevel: 'none',
sourcemaps: { debugId: true },
});
expect(hasPlugin(plugins, ERROR_TRACKING_PLUGIN_NAME)).toBe(false);
});

test('Should inject and upload debug ID source maps from the top-level sourcemaps option', () => {
const plugins = invokeFactory({
auth: { apiKey: '123' },
logLevel: 'none',
sourcemaps: { debugId: true, upload: true },
});
expect(hasPlugin(plugins, ERROR_TRACKING_PLUGIN_NAME)).toBe(true);
});

test('Should coerce a non-boolean enable value and still include the plugin', () => {
const plugins = invokeFactory({
logLevel: 'none',
Expand Down
95 changes: 95 additions & 0 deletions packages/factory/src/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,99 @@ describe('factory validateOptions', () => {
).not.toThrow();
});
});

describe('sourcemaps', () => {
it('should normalize debug ID injection without upload', () => {
expect(validateOptions({ sourcemaps: { debugId: true } })).toEqual(
expect.objectContaining({
rum: { sourceCodeContext: { debugId: true } },
}),
);
expect(
validateOptions({ sourcemaps: { debugId: true } }).errorTracking,
).toBeUndefined();
});

it('should normalize debug ID injection and upload options', () => {
expect(
validateOptions({
sourcemaps: {
bailOnError: true,
debugId: true,
dryRun: true,
maxConcurrency: 5,
upload: true,
},
}),
).toEqual(
expect.objectContaining({
errorTracking: {
sourcemaps: {
bailOnError: true,
debugId: true,
dryRun: true,
maxConcurrency: 5,
},
},
rum: { sourceCodeContext: { debugId: true } },
}),
);
});

it('should leave omitted upload options unset for downstream defaults', () => {
expect(
validateOptions({ sourcemaps: { debugId: true, upload: true } }).errorTracking,
).toEqual({ sourcemaps: { debugId: true } });
});

it('should preserve source code context metadata while enabling debug IDs', () => {
expect(
validateOptions({
metadata: { version: '1.2.3' },
rum: { sourceCodeContext: { service: 'checkout' } },
sourcemaps: { debugId: true, upload: true },
}),
).toEqual(
expect.objectContaining({
rum: {
sourceCodeContext: {
debugId: true,
service: 'checkout',
},
},
}),
);
});

it.each([
{
input: { sourcemaps: { debugId: false } },
error: /sourcemaps\.debugId must be true/,
},
{
input: { sourcemaps: { debugId: true, upload: 'yes' } },
error: /sourcemaps\.upload must be a boolean/,
},
{
input: { sourcemaps: { bailOnError: true, debugId: true } },
error: /require sourcemaps\.upload to be true/,
},
{
input: {
rum: { sourceCodeContext: { debugId: false, service: 'checkout' } },
sourcemaps: { debugId: true },
},
error: /rum\.sourceCodeContext\.debugId cannot be false/,
},
{
input: {
errorTracking: { sourcemaps: { debugId: true } },
sourcemaps: { debugId: true, upload: true },
},
error: /cannot be combined with errorTracking\.sourcemaps/,
},
])('should reject invalid or conflicting configuration', ({ input, error }) => {
expect(() => validateOptions(input as unknown as Options)).toThrow(error);
});
});
});
Loading
Loading