diff --git a/.eslintrc.js b/.eslintrc.js
index eff08d41f..0b6142c0e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -230,7 +230,8 @@ module.exports = {
],
'no-delete-var': 'error',
'no-label-var': 'error',
- 'no-shadow': 'error',
+ 'no-shadow': 'off',
+ '@typescript-eslint/no-shadow': ['error'],
'no-shadow-restricted-names': 'error',
'no-undef': 'error',
'no-undef-init': 'error',
@@ -390,7 +391,12 @@ module.exports = {
},
},
{
- files: ['rollup.config.mjs', 'packages/core/**/*', 'packages/published/**/*'],
+ files: [
+ 'rollup.config.mjs',
+ 'packages/core/**/*',
+ 'packages/published/**/*',
+ 'packages/plugins/**/built/*',
+ ],
rules: {
'import/no-extraneous-dependencies': 'off',
},
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 27de7a7c1..a8d8f13f9 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -23,6 +23,10 @@ packages/tests/src/plugins/injection @yoannmoin
packages/plugins/telemetry @DataDog/frontend-devx @yoannmoinet
packages/tests/src/plugins/telemetry @DataDog/frontend-devx @yoannmoinet
+# Error Tracking
+packages/plugins/error-tracking @yoannmoinet
+packages/tests/src/plugins/error-tracking @yoannmoinet
+
# Rum
-packages/plugins/rum @DataDog/rum @yoannmoinet
-packages/tests/src/plugins/rum @DataDog/rum @yoannmoinet
+packages/plugins/rum @yoannmoinet
+packages/tests/src/plugins/rum @yoannmoinet
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 0ba48f334..507327191 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -21,6 +21,7 @@ jobs:
with:
node-version: ${{matrix.node}}.x
- run: yarn install
+ - run: yarn build:all
- run: yarn test
lint:
diff --git a/.node-version b/.node-version
index a9d087399..1117d417c 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-18.19.0
+18.20.5
diff --git a/.vscode/settings.json b/.vscode/settings.json
index be309b700..65d300429 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -25,5 +25,6 @@
"**/.yarn/": true,
"**/.yarnrc.yml": true,
"**/yarn.lock": true
- }
+ },
+ "typescript.tsdk": "node_modules/typescript/lib"
}
diff --git a/.yarn/cache/@datadog-browser-core-npm-6.0.0-6d2c41f4eb-283097b37b.zip b/.yarn/cache/@datadog-browser-core-npm-6.0.0-6d2c41f4eb-283097b37b.zip
new file mode 100644
index 000000000..64521c7f7
Binary files /dev/null and b/.yarn/cache/@datadog-browser-core-npm-6.0.0-6d2c41f4eb-283097b37b.zip differ
diff --git a/.yarn/cache/@datadog-browser-rum-core-npm-6.0.0-e328c70bce-92ff7c44a4.zip b/.yarn/cache/@datadog-browser-rum-core-npm-6.0.0-e328c70bce-92ff7c44a4.zip
new file mode 100644
index 000000000..d668a4096
Binary files /dev/null and b/.yarn/cache/@datadog-browser-rum-core-npm-6.0.0-e328c70bce-92ff7c44a4.zip differ
diff --git a/.yarn/cache/@datadog-browser-rum-npm-6.0.0-8404f49122-e50c3955af.zip b/.yarn/cache/@datadog-browser-rum-npm-6.0.0-8404f49122-e50c3955af.zip
new file mode 100644
index 000000000..db766566c
Binary files /dev/null and b/.yarn/cache/@datadog-browser-rum-npm-6.0.0-8404f49122-e50c3955af.zip differ
diff --git a/.yarn/cache/@datadog-browser-rum-react-npm-6.0.0-c26f101832-f13aec89de.zip b/.yarn/cache/@datadog-browser-rum-react-npm-6.0.0-c26f101832-f13aec89de.zip
new file mode 100644
index 000000000..03d6d5ef6
Binary files /dev/null and b/.yarn/cache/@datadog-browser-rum-react-npm-6.0.0-c26f101832-f13aec89de.zip differ
diff --git a/.yarn/cache/@rollup-plugin-terser-npm-0.4.4-c6896dd264-a5e066ddea.zip b/.yarn/cache/@rollup-plugin-terser-npm-0.4.4-c6896dd264-a5e066ddea.zip
new file mode 100644
index 000000000..4330a00fd
Binary files /dev/null and b/.yarn/cache/@rollup-plugin-terser-npm-0.4.4-c6896dd264-a5e066ddea.zip differ
diff --git a/.yarn/cache/smob-npm-1.5.0-acdaaf382d-a1ea453bce.zip b/.yarn/cache/smob-npm-1.5.0-acdaaf382d-a1ea453bce.zip
new file mode 100644
index 000000000..4a65fc40d
Binary files /dev/null and b/.yarn/cache/smob-npm-1.5.0-acdaaf382d-a1ea453bce.zip differ
diff --git a/.yarn/cache/terser-npm-5.37.0-7dbdc43c6e-3afacf7c38.zip b/.yarn/cache/terser-npm-5.37.0-7dbdc43c6e-3afacf7c38.zip
new file mode 100644
index 000000000..9ec2ef107
Binary files /dev/null and b/.yarn/cache/terser-npm-5.37.0-7dbdc43c6e-3afacf7c38.zip differ
diff --git a/LICENSE b/LICENSE
index 98d1c7c2f..6f944311d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2024 Datadog
+Copyright (c) 2025 Datadog
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/LICENSES-3rdparty.csv b/LICENSES-3rdparty.csv
index 8405fc40f..62fd35ff7 100644
--- a/LICENSES-3rdparty.csv
+++ b/LICENSES-3rdparty.csv
@@ -115,6 +115,10 @@ Component,Origin,Licence,Copyright
@babel/types,npm,MIT,The Babel Team (https://babel.dev/docs/en/next/babel-types)
@bcoe/v8-coverage,npm,MIT,Charles Samborski (https://demurgos.github.io/v8-coverage)
@cspotcode/source-map-support,npm,MIT,(https://www.npmjs.com/package/@cspotcode/source-map-support)
+@datadog/browser-core,npm,Apache-2.0,(https://www.npmjs.com/package/@datadog/browser-core)
+@datadog/browser-rum,virtual,Apache-2.0,(https://www.npmjs.com/package/@datadog/browser-rum)
+@datadog/browser-rum-core,npm,Apache-2.0,(https://www.npmjs.com/package/@datadog/browser-rum-core)
+@datadog/browser-rum-react,virtual,Apache-2.0,(https://www.npmjs.com/package/@datadog/browser-rum-react)
@esbuild/darwin-arm64,npm,MIT,(https://www.npmjs.com/package/@esbuild/darwin-arm64)
@esbuild/linux-x64,npm,MIT,(https://www.npmjs.com/package/@esbuild/linux-x64)
@eslint-community/eslint-utils,virtual,MIT,Toru Nagashima (https://github.com/eslint-community/eslint-utils#readme)
@@ -171,6 +175,7 @@ Component,Origin,Licence,Copyright
@rollup/plugin-commonjs,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/commonjs/#readme)
@rollup/plugin-json,virtual,MIT,rollup (https://github.com/rollup/plugins/tree/master/packages/json#readme)
@rollup/plugin-node-resolve,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/node-resolve/#readme)
+@rollup/plugin-terser,virtual,MIT,Peter Placzek (https://github.com/rollup/plugins/tree/master/packages/terser#readme)
@rollup/pluginutils,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/pluginutils#readme)
@rollup/rollup-darwin-arm64,npm,MIT,Lukas Taegert-Atkinson (https://rollupjs.org/)
@rspack/binding,npm,MIT,(https://rspack.dev)
@@ -822,6 +827,7 @@ simple-git,npm,MIT,Steve King (https://www.npmjs.com/package/simple-git)
sisteransi,npm,MIT,Terkel Gjervig (https://terkel.com)
slash,npm,MIT,Sindre Sorhus (sindresorhus.com)
slice-ansi,npm,MIT,(https://www.npmjs.com/package/slice-ansi)
+smob,npm,MIT,Peter Placzek (https://github.com/Tada5hi/smob#readme)
snapdragon,npm,MIT,Jon Schlinkert (https://github.com/jonschlinkert/snapdragon)
snapdragon-node,npm,MIT,Jon Schlinkert (https://github.com/jonschlinkert/snapdragon-node)
snapdragon-util,npm,MIT,Jon Schlinkert (https://github.com/jonschlinkert/snapdragon-util)
diff --git a/NOTICE b/NOTICE
index 982aa64b0..78aeaf14a 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,4 +1,4 @@
Datadog build-plugins
-Copyright 2024-present Datadog, Inc.
+Copyright 2025-present Datadog, Inc.
This product includes software developed at Datadog (https://www.datadoghq.com/).
diff --git a/README.md b/README.md
index 382fdbf6d..b03e84f2b 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
# Datadog Build Plugins
A set of bundler plugins for:
- -
Webpack
- -
Vite
- -
esbuild
- -
Rollup
- -
Rspack
+
+- [
esbuild `@datadog/esbuild-plugin`](/packages/published/esbuild-plugin#readme)
+- [
Rollup `@datadog/rollup-plugin`](/packages/published/rollup-plugin#readme)
+- [
Rspack `@datadog/rspack-plugin`](/packages/published/rspack-plugin#readme)
+- [
Vite `@datadog/vite-plugin`](/packages/published/vite-plugin#readme)
+- [
Webpack `@datadog/webpack-plugin`](/packages/published/webpack-plugin#readme)
+
To interact with Datadog directly from your builds.
@@ -17,208 +19,45 @@ To interact with Datadog directly from your builds.
-- [Bundler Plugins](#bundler-plugins)
- - [ESBuild](#-esbuild)
- - [Rollup](#-rollup)
- - [Rspack](#-rspack)
- - [Vite](#-vite)
- - [Webpack](#-webpack)
-- [Features](#features)
- - [RUM](#rum-----)
- - [Telemetry](#telemetry-----)
+- [Installation](#installation)
+- [Usage](#usage)
- [Configuration](#configuration)
- [`auth.apiKey`](#authapikey)
+ - [`auth.appKey`](#authappkey)
- [`logLevel`](#loglevel)
- [`customPlugins`](#customplugins)
+- [Features](#features)
+ - [Error Tracking](#error-tracking-----)
+ - [Rum](#rum-----)
+ - [Telemetry](#telemetry-----)
- [Contributing](#contributing)
- [License](#license)
-## Bundler Plugins
-
-
-###
ESBuild
-
-`@datadog/esbuild-plugin`
-
-#### Installation
-- Yarn
-
-```bash
-yarn add -D @datadog/esbuild-plugin
-```
-
-- NPM
-
-```bash
-npm install --save-dev @datadog/esbuild-plugin
-```
-
-
-#### Usage
-```js
-const { datadogEsbuildPlugin } = require('@datadog/esbuild-plugin');
-
-require('esbuild').build({
- plugins: [
- datadogEsbuildPlugin({
- // Configuration
- }),
- ],
-});
-```
-
-> [!TIP]
-> It is important to have the plugin in the first position in order to report every other plugins.
-
-
-[📝 More details ➡️](/packages/published/esbuild-plugin#readme)
-
-###
Rollup
-
-`@datadog/rollup-plugin`
-
-#### Installation
-- Yarn
-
-```bash
-yarn add -D @datadog/rollup-plugin
-```
-
-- NPM
-
-```bash
-npm install --save-dev @datadog/rollup-plugin
-```
-
-
-#### Usage
-Inside your `rollup.config.js`.
-
-```js
-import { datadogRollupPlugin } from '@datadog/rollup-plugin';
-
-export default {
- plugins: [
- datadogRollupPlugin({
- // Configuration
- }),
- ],
-};
-```
-
-> [!TIP]
-> It is important to have the plugin in the first position in order to report every other plugins.
-
-
-[📝 More details ➡️](/packages/published/rollup-plugin#readme)
-
-###
Rspack
-
-`@datadog/rspack-plugin`
-
-#### Installation
-- Yarn
-
-```bash
-yarn add -D @datadog/rspack-plugin
-```
-
-- NPM
-
-```bash
-npm install --save-dev @datadog/rspack-plugin
-```
-
-
-#### Usage
-Inside your `rspack.config.js`.
-
-```js
-const { datadogRspackPlugin } = require('@datadog/rspack-plugin');
-
-module.exports = {
- plugins: [
- datadogRspackPlugin({
- // Configuration
- }),
- ],
-};
-```
-
-> [!TIP]
-> It is important to have the plugin in the first position in order to report every other plugins.
-
-
-[📝 More details ➡️](/packages/published/rspack-plugin#readme)
-
-###
Vite
-
-`@datadog/vite-plugin`
-
-#### Installation
-- Yarn
-
-```bash
-yarn add -D @datadog/vite-plugin
-```
-
-- NPM
-
-```bash
-npm install --save-dev @datadog/vite-plugin
-```
-
-
-#### Usage
-Inside your `vite.config.js`.
-
-```js
-import { datadogVitePlugin } from '@datadog/vite-plugin';
-import { defineConfig } from 'vite'
-
-export default defineConfig({
- plugins: [
- datadogVitePlugin({
- // Configuration
- }),
- ],
-};
-```
-
-> [!TIP]
-> It is important to have the plugin in the first position in order to report every other plugins.
-
+## Installation
-[📝 More details ➡️](/packages/published/vite-plugin#readme)
-
-###
Webpack
-
-`@datadog/webpack-plugin`
-
-#### Installation
- Yarn
```bash
-yarn add -D @datadog/webpack-plugin
+yarn add -D @datadog/{{bundler}}-plugin
```
- NPM
```bash
-npm install --save-dev @datadog/webpack-plugin
+npm install --save-dev @datadog/{{bundler}}-plugin
```
+## Usage
-#### Usage
-Inside your `webpack.config.js`.
+In your bundler's configuration file:
```js
-const { datadogWebpackPlugin } = require('@datadog/webpack-plugin');
+const { datadog{{Bundler}}Plugin } = require('@datadog/{{bundler}}-plugin');
-module.exports = {
+export const config = {
plugins: [
- datadogWebpackPlugin({
+ datadog{{Bundler}}Plugin({
// Configuration
}),
],
@@ -226,67 +65,22 @@ module.exports = {
```
> [!TIP]
-> It is important to have the plugin in the first position in order to report every other plugins.
-
+> It is best to have the plugin in the first position in order to report every other plugins.
-[📝 More details ➡️](/packages/published/webpack-plugin#readme)
+Follow the specific documentation for each bundler:
+
+- [
esbuild `@datadog/esbuild-plugin`](/packages/published/esbuild-plugin#readme)
+- [
Rollup `@datadog/rollup-plugin`](/packages/published/rollup-plugin#readme)
+- [
Rspack `@datadog/rspack-plugin`](/packages/published/rspack-plugin#readme)
+- [
Vite `@datadog/vite-plugin`](/packages/published/vite-plugin#readme)
+- [
Webpack `@datadog/webpack-plugin`](/packages/published/webpack-plugin#readme)
-## Features
-
-
-### RUM
-
-> Interact with our Real User Monitoring product (RUM) in Datadog directly from your build system.
-
-```typescript
-datadogWebpackPlugin({
- rum?: {
- disabled?: boolean,
- sourcemaps?: {
- bailOnError?: boolean,
- dryRun?: boolean,
- intakeUrl?: string,
- maxConcurrency?: number,
- minifiedPathPrefix: string,
- releaseVersion: string,
- service: string,
- },
- }
-});
-```
-
-[📝 Full documentation ➡️](/packages/plugins/rum#readme)
-
-### Telemetry
-
-> Display and send telemetry data as metrics to Datadog.
-
-```typescript
-datadogWebpackPlugin({
- telemetry?: {
- disabled?: boolean,
- enableTracing?: boolean,
- endPoint?: string,
- output?: boolean
- | string
- | {
- destination: string,
- timings?: boolean,
- metrics?: boolean,
- },
- prefix?: string,
- tags?: string[],
- timestamp?: number,
- filters?: ((metric: Metric) => Metric | null)[],
- }
-});
-```
+## Configuration
-[📝 Full documentation ➡️](/packages/plugins/telemetry#readme)
-
+
-## Configuration
+Full configuration object
```typescript
@@ -296,7 +90,7 @@ datadogWebpackPlugin({
};
customPlugins?: (options: Options, context: GlobalContext, log: Logger) => UnpluginPlugin[];
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'none';
- rum?: {
+ errorTracking?: {
disabled?: boolean;
sourcemaps?: {
bailOnError?: boolean;
@@ -308,6 +102,41 @@ datadogWebpackPlugin({
service: string;
};
};
+ rum?: {
+ disabled?: boolean;
+ react?: {
+ router?: boolean;
+ };
+ sdk?: {
+ actionNameAttribute?: string;
+ allowedTracingUrls?: string[];
+ allowUntrustedEvents?: boolean;
+ applicationId: string;
+ clientToken?: string;
+ compressIntakeRequests?: boolean;
+ defaultPrivacyLevel?: 'mask' | 'mask-user-input' | 'allow';
+ enablePrivacyForActionName?: boolean;
+ env?: string;
+ excludedActivityUrls?: string[];
+ proxy?: string;
+ service?: string;
+ sessionReplaySampleRate?: number;
+ sessionSampleRate?: number;
+ silentMultipleInit?: boolean;
+ site?: string;
+ startSessionReplayRecordingManually?: boolean;
+ storeContextsAcrossPages?: boolean;
+ telemetrySampleRate?: number;
+ traceSampleRate?: number;
+ trackingConsent?: 'granted' | 'not_granted';
+ trackLongTasks?: boolean;
+ trackResources?: boolean;
+ trackUserInteractions?: boolean;
+ trackViewsManually?: boolean;
+ version?: string;
+ workerUrl?: string;
+ };
+ };
telemetry?: {
disabled?: boolean;
enableTracing?: boolean;
@@ -328,12 +157,24 @@ datadogWebpackPlugin({
```
+
+
### `auth.apiKey`
> default `null`
In order to interact with Datadog, you have to use [your own API Key](https://app.datadoghq.com/organization-settings/api-keys).
+### `auth.appKey`
+
+> default `null`
+
+In order to interact with Datadog, you have to use [your own Application Key](https://app.datadoghq.com/organization-settings/application-keys).
+
+**Required permissions**:
+
+- `rum_apps_read` if you use `rum.sdk` without providing `rum.sdk.clientToken`.
+
### `logLevel`
> default: `'warn'`
@@ -367,7 +208,11 @@ Your function will receive three arguments:
- `context`: The global context shared accross our plugin.
- `log`: A [logger](/packages/factory/README.md#logger) to display messages.
-The `context` is a shared object that is mutated during the build process. It contains the following properties:
+The `context` is a shared object that is mutated during the build process.
+
+
+
+Full context object
```typescript
@@ -433,16 +278,129 @@ type GlobalContext = {
version: string;
}
```
+
-> [!NOTE]
-> Some parts of the context are only available after certain hooks:
-> - `context.bundler.rawConfig` is added in the `buildStart` hook.
-> - `context.build.*` is populated in the `writeBundle` hook.
-> - `context.git.*` is populated in the `buildStart` hook.
+
-
+#### [📝 Full documentation ➡️](/packages/factory#global-context)
+
+## Features
+
+
+### Error Tracking
+
+> Interact with Error Tracking directly from your build system.
+
+#### [📝 Full documentation ➡️](/packages/plugins/error-tracking#readme)
+
+
-Your function will need to return an array of [Unplugin Plugins definitions](https://unplugin.unjs.io/guide/#supported-hooks).
+Configuration
+
+```typescript
+datadogWebpackPlugin({
+ errorTracking?: {
+ disabled?: boolean,
+ sourcemaps?: {
+ bailOnError?: boolean,
+ dryRun?: boolean,
+ intakeUrl?: string,
+ maxConcurrency?: number,
+ minifiedPathPrefix: string,
+ releaseVersion: string,
+ service: string,
+ },
+ }
+});
+```
+
+
+
+### Rum
+
+> Interact with Real User Monitoring (RUM) directly from your build system.
+
+#### [📝 Full documentation ➡️](/packages/plugins/rum#readme)
+
+
+
+Configuration
+
+```typescript
+datadogWebpackPlugin({
+ rum?: {
+ disabled?: boolean,
+ react?: {
+ router?: boolean,
+ },
+ sdk?: {
+ actionNameAttribute?: string,
+ allowedTracingUrls?: string[],
+ allowUntrustedEvents?: boolean,
+ applicationId: string,
+ clientToken?: string,
+ compressIntakeRequests?: boolean,
+ defaultPrivacyLevel?: 'mask' | 'mask-user-input' | 'allow',
+ enablePrivacyForActionName?: boolean,
+ env?: string,
+ excludedActivityUrls?: string[],
+ proxy?: string,
+ service?: string,
+ sessionReplaySampleRate?: number,
+ sessionSampleRate?: number,
+ silentMultipleInit?: boolean,
+ site?: string,
+ startSessionReplayRecordingManually?: boolean,
+ storeContextsAcrossPages?: boolean,
+ telemetrySampleRate?: number,
+ traceSampleRate?: number,
+ trackingConsent?: 'granted' | 'not_granted',
+ trackLongTasks?: boolean,
+ trackResources?: boolean,
+ trackUserInteractions?: boolean,
+ trackViewsManually?: boolean,
+ version?: string,
+ workerUrl?: string,
+ },
+ }
+});
+```
+
+
+
+### Telemetry
+
+> Display and send telemetry data as metrics to Datadog.
+
+#### [📝 Full documentation ➡️](/packages/plugins/telemetry#readme)
+
+
+
+Configuration
+
+```typescript
+datadogWebpackPlugin({
+ telemetry?: {
+ disabled?: boolean,
+ enableTracing?: boolean,
+ endPoint?: string,
+ output?: boolean
+ | string
+ | {
+ destination: string,
+ timings?: boolean,
+ metrics?: boolean,
+ },
+ prefix?: string,
+ tags?: string[],
+ timestamp?: number,
+ filters?: ((metric: Metric) => Metric | null)[],
+ }
+});
+```
+
+
+
## Contributing
@@ -452,4 +410,4 @@ Check out [CONTRIBUTING.md](/CONTRIBUTING.md) for more information about how to
[MIT](/LICENSE)
-[Back to top :arrow_up:](#top)
+### [Back to top :arrow_up:](#top)
diff --git a/package.json b/package.json
index d4bbd2602..fbb2c2344 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,10 @@
"workspaces": [
"packages/*",
"packages/plugins/*",
- "packages/published/*",
- "packages/tests/src/_jest/fixtures/project"
+ "packages/published/*"
],
"volta": {
- "node": "18.19.0",
+ "node": "18.20.5",
"yarn": "1.22.19"
},
"scripts": {
@@ -27,8 +26,7 @@
"loop": "yarn workspaces foreach -Apti --include \"@datadog/*\" --exclude \"@datadog/build-plugins\"",
"oss": "yarn cli oss -d packages -l mit",
"publish:all": "yarn loop --no-private npm publish",
- "test": "yarn build:all && yarn workspace @dd/tests test",
- "test:noisy": "yarn workspace @dd/tests test:noisy",
+ "test": "yarn workspace @dd/tests test",
"typecheck:all": "yarn workspaces foreach -Apti run typecheck",
"version:all": "yarn loop version --deferred ${0} && yarn version apply --all",
"watch:all": "yarn loop run watch"
diff --git a/packages/core/package.json b/packages/core/package.json
index f77ffaf15..abfafa166 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -24,12 +24,14 @@
},
"dependencies": {
"async-retry": "1.3.3",
- "chalk": "2.3.1"
+ "chalk": "2.3.1",
+ "glob": "11.0.0"
},
"devDependencies": {
"@types/async-retry": "1.4.8",
"@types/chalk": "2.2.0",
"@types/node": "^18",
+ "esbuild": "0.24.0",
"typescript": "5.4.3",
"unplugin": "1.16.0"
}
diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts
index 582719656..9c2c5abac 100644
--- a/packages/core/src/helpers.ts
+++ b/packages/core/src/helpers.ts
@@ -4,12 +4,14 @@
import { INJECTED_FILE } from '@dd/core/constants';
import retry from 'async-retry';
+import type { PluginBuild } from 'esbuild';
import fsp from 'fs/promises';
import fs from 'fs';
+import { glob } from 'glob';
import path from 'path';
import type { RequestInit } from 'undici-types';
-import type { RequestOpts } from './types';
+import type { GlobalContext, Logger, RequestOpts, ResolvedEntry } from './types';
// Format a duration 0h 0m 0s 0ms
export const formatDuration = (duration: number) => {
@@ -25,19 +27,79 @@ export const formatDuration = (duration: number) => {
}${milliseconds ? `${milliseconds}ms` : ''}`.trim();
};
-export const getResolvedPath = (filepath: string) => {
- try {
- return require.resolve(filepath);
- } catch (e) {
- return filepath;
+// https://esbuild.github.io/api/#glob-style-entry-points
+const getAllEntryFiles = (filepath: string): string[] => {
+ if (!filepath.includes('*')) {
+ return [filepath];
}
+
+ const files = glob.sync(filepath);
+ return files;
+};
+
+// Parse, resolve and return all the entries of esbuild.
+export const getEsbuildEntries = async (
+ build: PluginBuild,
+ context: GlobalContext,
+ log: Logger,
+): Promise => {
+ const entries: { name?: string; resolved: string; original: string }[] = [];
+ const entryPoints = build.initialOptions.entryPoints;
+ const entryPaths: { name?: string; path: string }[] = [];
+ const resolutionErrors: string[] = [];
+
+ if (Array.isArray(entryPoints)) {
+ for (const entry of entryPoints) {
+ const fullPath = entry && typeof entry === 'object' ? entry.in : entry;
+ entryPaths.push({ path: fullPath });
+ }
+ } else if (typeof entryPoints === 'object') {
+ entryPaths.push(
+ ...Object.entries(entryPoints).map(([name, filepath]) => ({ name, path: filepath })),
+ );
+ }
+
+ // Resolve all the paths.
+ const proms = entryPaths
+ .flatMap((entry) =>
+ getAllEntryFiles(entry.path).map<[{ name?: string; path: string }, string]>((p) => [
+ entry,
+ p,
+ ]),
+ )
+ .map(async ([entry, p]) => {
+ const result = await build.resolve(p, {
+ kind: 'entry-point',
+ resolveDir: context.cwd,
+ });
+
+ if (result.errors.length) {
+ resolutionErrors.push(...result.errors.map((e) => e.text));
+ }
+
+ if (result.path) {
+ // Store them for later use.
+ entries.push({
+ name: entry.name,
+ resolved: result.path,
+ original: entry.path,
+ });
+ }
+ });
+
+ for (const resolutionError of resolutionErrors) {
+ log.error(resolutionError);
+ }
+
+ await Promise.all(proms);
+ return entries;
};
export const ERROR_CODES_NO_RETRY = [400, 403, 413];
export const NB_RETRIES = 5;
// Do a retriable fetch.
export const doRequest = (opts: RequestOpts): Promise => {
- const { url, method = 'GET', getData, onRetry, type = 'text' } = opts;
+ const { auth, url, method = 'GET', getData, onRetry, type = 'text' } = opts;
return retry(
async (bail: (e: Error) => void, attempt: number) => {
let response: Response;
@@ -48,14 +110,24 @@ export const doRequest = (opts: RequestOpts): Promise => {
// https://github.com/nodejs/node/issues/46221
duplex: 'half',
};
+ let requestHeaders: RequestInit['headers'] = {};
+
+ // Do auth if present.
+ if (auth?.apiKey) {
+ requestHeaders['DD-API-KEY'] = auth.apiKey;
+ }
+
+ if (auth?.appKey) {
+ requestHeaders['DD-APPLICATION-KEY'] = auth.appKey;
+ }
if (typeof getData === 'function') {
const { data, headers } = await getData();
requestInit.body = data;
- requestInit.headers = headers;
+ requestHeaders = { ...requestHeaders, ...headers };
}
- response = await fetch(url, requestInit);
+ response = await fetch(url, { ...requestInit, headers: requestHeaders });
} catch (error: any) {
// We don't want to retry if there is a non-fetch related error.
bail(error);
@@ -169,3 +241,6 @@ export const readJsonSync = (filepath: string) => {
const data = fs.readFileSync(filepath, { encoding: 'utf-8' });
return JSON.parse(data);
};
+
+let index = 0;
+export const getUniqueId = () => `${Date.now()}.${performance.now()}.${++index}`;
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 11e6a6b46..f94518e59 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -9,6 +9,8 @@
import type { TrackedFilesMatcher } from '@dd/internal-git-plugin/trackedFilesMatcher';
/* eslint-disable arca/import-ordering */
// #imports-injection-marker
+import type { ErrorTrackingOptions } from '@dd/error-tracking-plugin/types';
+import type * as errorTracking from '@dd/error-tracking-plugin';
import type { RumOptions } from '@dd/rum-plugin/types';
import type * as rum from '@dd/rum-plugin';
import type { TelemetryOptions } from '@dd/telemetry-plugin/types';
@@ -41,9 +43,16 @@ export type SerializedInput = Assign;
export type BuildReport = {
+ bundler: Omit;
errors: string[];
warnings: string[];
- logs: { pluginName: string; type: LogLevel; message: string; time: number }[];
+ logs: {
+ bundler: BundlerFullName;
+ pluginName: string;
+ type: LogLevel;
+ message: string;
+ time: number;
+ }[];
entries?: Entry[];
inputs?: Input[];
outputs?: Output[];
@@ -74,7 +83,18 @@ export type BundlerReport = {
version: string;
};
-export type ToInjectItem = { type: 'file' | 'code'; value: string; fallback?: ToInjectItem };
+export type InjectedValue = string | (() => Promise);
+export enum InjectPosition {
+ BEFORE,
+ MIDDLE,
+ AFTER,
+}
+export type ToInjectItem = {
+ type: 'file' | 'code';
+ value: InjectedValue;
+ position?: InjectPosition;
+ fallback?: ToInjectItem;
+};
export type GetLogger = (name: string) => Logger;
export type Logger = {
@@ -116,6 +136,7 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
export type AuthOptions = {
apiKey?: string;
+ appKey?: string;
};
export interface BaseOptions {
@@ -127,6 +148,7 @@ export interface BaseOptions {
export interface Options extends BaseOptions {
// Each product should have a unique entry.
// #types-injection-marker
+ [errorTracking.CONFIG_KEY]?: ErrorTrackingOptions;
[rum.CONFIG_KEY]?: RumOptions;
[telemetry.CONFIG_KEY]?: TelemetryOptions;
// #types-injection-marker
@@ -138,11 +160,14 @@ export type OptionsWithDefaults = Assign;
export type PluginName = `datadog-${Lowercase}-plugin`;
-type Data = { data: BodyInit; headers?: Record };
+type Data = { data?: BodyInit; headers?: Record };
export type RequestOpts = {
url: string;
+ auth?: AuthOptions;
method?: string;
getData?: () => Promise | Data;
type?: 'json' | 'text';
onRetry?: (error: Error, attempt: number) => void;
};
+
+export type ResolvedEntry = { name?: string; resolved: string; original: string };
diff --git a/packages/factory/README.md b/packages/factory/README.md
index 688963d77..ee2c5dae1 100644
--- a/packages/factory/README.md
+++ b/packages/factory/README.md
@@ -27,27 +27,35 @@ Most of the time they will interact via the global context.
> This will populate `context.build` with a bunch of data coming from the build.
-[📝 Full documentation ➡️](/packages/plugins/build-report#readme)
+#### [📝 Full documentation ➡️](/packages/plugins/build-report#readme)
+
### Bundler Report
> A very basic report on the currently used bundler.
> It is useful to unify some configurations.
-[📝 Full documentation ➡️](/packages/plugins/bundler-report#readme)
+#### [📝 Full documentation ➡️](/packages/plugins/bundler-report#readme)
+
### Git
> Adds repository data to the global context from the `buildStart` hook.
-[📝 Full documentation ➡️](/packages/plugins/git#readme)
+#### [📝 Full documentation ➡️](/packages/plugins/git#readme)
+
### Injection
-> This is used to prepend some code to the produced bundle.
-> Particularly useful if you want to share some global context, or to automatically inject some SDK.
+> This is used to inject some code to the produced bundle.
+> Particularly useful :
+> - to share some global context.
+> - to automatically inject some SDK.
+> - to initialise some global dependencies.
+> - ...
+
+#### [📝 Full documentation ➡️](/packages/plugins/injection#readme)
-[📝 Full documentation ➡️](/packages/plugins/injection#readme)
## Logger
@@ -166,3 +174,5 @@ type GlobalContext = {
> - `context.bundler.rawConfig` is added in the `buildStart` hook.
> - `context.build.*` is populated in the `writeBundle` hook.
> - `context.git.*` is populated in the `buildStart` hook.
+
+Your function will need to return an array of [Unplugin Plugins definitions](https://unplugin.unjs.io/guide/#supported-hooks).
diff --git a/packages/factory/package.json b/packages/factory/package.json
index 8b63c6ded..3f8177e3e 100644
--- a/packages/factory/package.json
+++ b/packages/factory/package.json
@@ -17,6 +17,7 @@
},
"dependencies": {
"@dd/core": "workspace:*",
+ "@dd/error-tracking-plugin": "workspace:*",
"@dd/internal-build-report-plugin": "workspace:*",
"@dd/internal-bundler-report-plugin": "workspace:*",
"@dd/internal-git-plugin": "workspace:*",
diff --git a/packages/factory/src/helpers.ts b/packages/factory/src/helpers.ts
index 73c00828a..d8d1d3478 100644
--- a/packages/factory/src/helpers.ts
+++ b/packages/factory/src/helpers.ts
@@ -2,6 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
+import { getUniqueId } from '@dd/core/helpers';
import type {
BuildReport,
BundlerFullName,
@@ -28,6 +29,7 @@ const logPriority: Record = {
export const getLoggerFactory =
(build: BuildReport, logLevel: LogLevel = 'warn'): GetLogger =>
(name) => {
+ const cleanName = name.replace(/(^datadog-|-plugin$)/g, '');
const log = (text: any, type: LogLevel = 'debug') => {
// By default (debug) we print dimmed.
let color = c.dim;
@@ -44,11 +46,17 @@ export const getLoggerFactory =
logFn = console.log;
}
- const prefix = `[${type}|${name}]`;
+ const prefix = `[${type}|${build.bundler.fullName}|${cleanName}]`;
// Keep a trace of the log in the build report.
const content = typeof text === 'string' ? text : JSON.stringify(text, null, 2);
- build.logs.push({ pluginName: name, type, message: content, time: Date.now() });
+ build.logs.push({
+ bundler: build.bundler.fullName,
+ pluginName: cleanName,
+ type,
+ message: content,
+ time: Date.now(),
+ });
if (type === 'error') {
build.errors.push(content);
}
@@ -65,7 +73,7 @@ export const getLoggerFactory =
return {
getLogger: (subName: string) => {
const logger = getLoggerFactory(build, logLevel);
- return logger(`${name}:${subName}`);
+ return logger(`${cleanName}:${subName}`);
},
error: (text: any) => log(text, 'error'),
warn: (text: any) => log(text, 'warn'),
@@ -84,7 +92,7 @@ export const getContext = ({
options: OptionsWithDefaults;
bundlerName: BundlerName;
bundlerVersion: string;
- injections: ToInjectItem[];
+ injections: Map;
version: FactoryMeta['version'];
}): GlobalContext => {
const cwd = process.cwd();
@@ -93,21 +101,26 @@ export const getContext = ({
errors: [],
warnings: [],
logs: [],
+ bundler: {
+ name: bundlerName,
+ fullName: `${bundlerName}${variant}` as BundlerFullName,
+ variant,
+ version: bundlerVersion,
+ },
};
const context: GlobalContext = {
auth: options.auth,
pluginNames: [],
bundler: {
- name: bundlerName,
- fullName: `${bundlerName}${variant}` as BundlerFullName,
- variant,
+ ...build.bundler,
+ // This will be updated in the bundler-report plugin once we have the configuration.
outDir: cwd,
- version: bundlerVersion,
},
build,
+ // This will be updated in the bundler-report plugin once we have the configuration.
cwd,
inject: (item: ToInjectItem) => {
- injections.push(item);
+ injections.set(getUniqueId(), item);
},
start: Date.now(),
version,
diff --git a/packages/factory/src/index.ts b/packages/factory/src/index.ts
index 72043aa56..ac4596b4a 100644
--- a/packages/factory/src/index.ts
+++ b/packages/factory/src/index.ts
@@ -2,6 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
+/* eslint-disable arca/import-ordering, arca/newline-after-import-section */
// This file is mostly generated.
// Anything between
// - #imports-injection-marker
@@ -22,11 +23,13 @@ import type {
} from '@dd/core/types';
import type { UnpluginContextMeta, UnpluginInstance, UnpluginOptions } from 'unplugin';
import { createUnplugin } from 'unplugin';
+import chalk from 'chalk';
import { getContext, getLoggerFactory, validateOptions } from './helpers';
-/* eslint-disable arca/import-ordering, arca/newline-after-import-section */
// #imports-injection-marker
+import type { OptionsWithErrorTracking } from '@dd/error-tracking-plugin/types';
+import * as errorTracking from '@dd/error-tracking-plugin';
import type { OptionsWithRum } from '@dd/rum-plugin/types';
import * as rum from '@dd/rum-plugin';
import type { OptionsWithTelemetry } from '@dd/telemetry-plugin/types';
@@ -37,10 +40,10 @@ import { getGitPlugins } from '@dd/internal-git-plugin';
import { getInjectionPlugins } from '@dd/internal-injection-plugin';
// #imports-injection-marker
// #types-export-injection-marker
+export type { types as ErrorTrackingTypes } from '@dd/error-tracking-plugin';
export type { types as RumTypes } from '@dd/rum-plugin';
export type { types as TelemetryTypes } from '@dd/telemetry-plugin';
// #types-export-injection-marker
-/* eslint-enable arca/import-ordering, arca/newline-after-import-section */
export const helpers = {
// Each product should have a unique entry.
@@ -68,7 +71,7 @@ export const buildPluginFactory = ({
}
// Create the global context.
- const injections: ToInjectItem[] = [];
+ const injections: Map = new Map();
const context: GlobalContext = getContext({
options,
bundlerVersion: bundler.version || bundler.VERSION,
@@ -91,6 +94,7 @@ export const buildPluginFactory = ({
...getGitPlugins(options, context),
...getInjectionPlugins(
bundler,
+ options,
context,
injections,
getLogger('datadog-injection-plugin'),
@@ -110,6 +114,18 @@ export const buildPluginFactory = ({
// Based on configuration add corresponding plugin.
// #configs-injection-marker
+ if (
+ options[errorTracking.CONFIG_KEY] &&
+ options[errorTracking.CONFIG_KEY].disabled !== true
+ ) {
+ plugins.push(
+ ...errorTracking.getPlugins(
+ options as OptionsWithErrorTracking,
+ context,
+ getLogger(errorTracking.PLUGIN_NAME),
+ ),
+ );
+ }
if (options[rum.CONFIG_KEY] && options[rum.CONFIG_KEY].disabled !== true) {
plugins.push(
...rum.getPlugins(options as OptionsWithRum, context, getLogger(rum.PLUGIN_NAME)),
@@ -129,6 +145,18 @@ export const buildPluginFactory = ({
// List all our plugins in the context.
context.pluginNames.push(...plugins.map((plugin) => plugin.name));
+ // Verify we don't have plugins with the same name, as they would override each other.
+ const duplicates = new Set(
+ context.pluginNames.filter(
+ (name) => context.pluginNames.filter((n) => n === name).length > 1,
+ ),
+ );
+ if (duplicates.size > 0) {
+ throw new Error(
+ `Duplicate plugin names: ${chalk.bold.red(Array.from(duplicates).join(', '))}`,
+ );
+ }
+
return plugins;
});
};
diff --git a/packages/plugins/build-report/package.json b/packages/plugins/build-report/package.json
index 77579e3a0..7aecd4e94 100644
--- a/packages/plugins/build-report/package.json
+++ b/packages/plugins/build-report/package.json
@@ -19,7 +19,6 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@dd/core": "workspace:*",
- "glob": "11.0.0"
+ "@dd/core": "workspace:*"
}
}
diff --git a/packages/plugins/build-report/src/esbuild.ts b/packages/plugins/build-report/src/esbuild.ts
index 709b54665..624d06bd4 100644
--- a/packages/plugins/build-report/src/esbuild.ts
+++ b/packages/plugins/build-report/src/esbuild.ts
@@ -2,9 +2,16 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import { getResolvedPath, isInjectionFile } from '@dd/core/helpers';
-import type { Logger, Entry, GlobalContext, Input, Output, PluginOptions } from '@dd/core/types';
-import { glob } from 'glob';
+import { getEsbuildEntries, isInjectionFile } from '@dd/core/helpers';
+import type {
+ Logger,
+ Entry,
+ GlobalContext,
+ Input,
+ Output,
+ PluginOptions,
+ ResolvedEntry,
+} from '@dd/core/types';
import { cleanName, getAbsolutePath, getType } from './helpers';
@@ -17,56 +24,27 @@ const reIndexMeta = (obj: Record, cwd: string) =>
}),
);
-// https://esbuild.github.io/api/#glob-style-entry-points
-const getAllEntryFiles = (filepath: string, cwd: string): string[] => {
- if (!filepath.includes('*')) {
- return [filepath];
- }
-
- const files = glob.sync(filepath);
- return files;
-};
-
-// Exported for testing purposes.
-export const getEntryNames = (
- entrypoints: string[] | Record | { in: string; out: string }[] | undefined,
- context: GlobalContext,
-): Map => {
- const entryNames = new Map();
- if (Array.isArray(entrypoints)) {
- // We don't have an indexed object as entry, so we can't get an entry name from it.
- for (const entry of entrypoints) {
- const fullPath = entry && typeof entry === 'object' ? entry.in : entry;
- const allFiles = getAllEntryFiles(fullPath, context.cwd);
- for (const file of allFiles) {
- // Using getResolvedPath because entries can be written with unresolved paths.
- const cleanedName = cleanName(context, getResolvedPath(file));
- entryNames.set(cleanedName, cleanedName);
- }
- }
- } else if (typeof entrypoints === 'object') {
- const entryList = entrypoints ? Object.entries(entrypoints) : [];
- for (const [entryName, entryPath] of entryList) {
- const allFiles = getAllEntryFiles(entryPath, context.cwd);
- for (const file of allFiles) {
- const cleanedName = cleanName(context, getResolvedPath(file));
- entryNames.set(cleanedName, entryName);
- }
- }
- }
- return entryNames;
-};
-
export const getEsbuildPlugin = (context: GlobalContext, log: Logger): PluginOptions['esbuild'] => {
return {
setup(build) {
- const cwd = context.cwd;
-
- // Store entry names based on the configuration.
- const entrypoints = build.initialOptions.entryPoints;
- const entryNames = getEntryNames(entrypoints, context);
+ const entryNames = new Map();
+ const resolvedEntries: ResolvedEntry[] = [];
+
+ build.onStart(async () => {
+ // Store entry names based on the configuration.
+ resolvedEntries.push(...(await getEsbuildEntries(build, context, log)));
+ for (const entry of resolvedEntries) {
+ const cleanedName = cleanName(context, entry.resolved);
+ if (entry.name) {
+ entryNames.set(cleanedName, entry.name);
+ } else {
+ entryNames.set(cleanedName, cleanedName);
+ }
+ }
+ });
build.onEnd((result) => {
+ const cwd = context.cwd;
for (const error of result.errors) {
context.build.errors.push(error.text);
}
diff --git a/packages/plugins/build-report/src/helpers.ts b/packages/plugins/build-report/src/helpers.ts
index 5c62745fb..449fce03c 100644
--- a/packages/plugins/build-report/src/helpers.ts
+++ b/packages/plugins/build-report/src/helpers.ts
@@ -50,6 +50,7 @@ export const serializeBuildReport = (report: BuildReport): SerializedBuildReport
// To make it JSON serializable, we need to remove the self references
// and replace them with strings, we'll use "filepath" to still have them uniquely identifiable.
const jsonReport: SerializedBuildReport = {
+ bundler: report.bundler,
errors: report.errors,
warnings: report.warnings,
logs: report.logs,
@@ -103,6 +104,7 @@ export const serializeBuildReport = (report: BuildReport): SerializedBuildReport
// Mostly useful for debugging and testing.
export const unserializeBuildReport = (report: SerializedBuildReport): BuildReport => {
const buildReport: BuildReport = {
+ bundler: report.bundler,
errors: report.errors,
warnings: report.warnings,
logs: report.logs,
diff --git a/packages/plugins/build-report/src/xpack.ts b/packages/plugins/build-report/src/xpack.ts
index 62e6b8a63..e04d7ac30 100644
--- a/packages/plugins/build-report/src/xpack.ts
+++ b/packages/plugins/build-report/src/xpack.ts
@@ -2,6 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
+import { isInjectionFile } from '@dd/core/helpers';
import type {
Logger,
Entry,
@@ -45,13 +46,13 @@ export const getXpackPlugin =
new Map();
const isModuleSupported = (moduleIdentifier?: string): boolean => {
- // console.log('Module Identifier supported', moduleIdentifier);
return (
// Ignore unidentified modules and runtimes.
!!moduleIdentifier &&
!moduleIdentifier.startsWith('webpack/runtime') &&
!moduleIdentifier.includes('/webpack4/buildin/') &&
- !moduleIdentifier.startsWith('multi ')
+ !moduleIdentifier.startsWith('multi ') &&
+ !isInjectionFile(moduleIdentifier)
);
};
diff --git a/packages/plugins/bundler-report/src/index.ts b/packages/plugins/bundler-report/src/index.ts
index 19c81fd08..7684fd8f4 100644
--- a/packages/plugins/bundler-report/src/index.ts
+++ b/packages/plugins/bundler-report/src/index.ts
@@ -7,20 +7,36 @@ import path from 'path';
export const PLUGIN_NAME = 'datadog-bundler-report-plugin';
-const rollupPlugin: (context: GlobalContext) => PluginOptions['rollup'] = (context) => ({
- options(options) {
- context.bundler.rawConfig = options;
- const outputOptions = (options as any).output;
- if (outputOptions) {
- context.bundler.outDir = outputOptions.dir;
- }
- },
- outputOptions(options) {
- if (options.dir) {
- context.bundler.outDir = options.dir;
+// From a list of path, return the nearest common directory.
+const getNearestCommonDirectory = (dirs: string[], cwd: string) => {
+ const splitPaths = dirs.map((dir) => {
+ const absolutePath = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
+ return absolutePath.split(path.sep);
+ });
+
+ // Use the shortest length for faster results.
+ const minLength = Math.min(...splitPaths.map((parts) => parts.length));
+ const commonParts = [];
+
+ for (let i = 0; i < minLength; i++) {
+ // We use the first path as our basis.
+ const component = splitPaths[0][i];
+ if (splitPaths.every((parts) => parts[i] === component)) {
+ commonParts.push(component);
+ } else {
+ break;
}
- },
-});
+ }
+
+ return commonParts.length > 0 ? commonParts.join(path.sep) : path.sep;
+};
+
+const handleCwd = (dirs: string[], context: GlobalContext) => {
+ const nearestDir = getNearestCommonDirectory(dirs, context.cwd);
+ if (nearestDir !== path.sep) {
+ context.cwd = nearestDir;
+ }
+};
const xpackPlugin: (context: GlobalContext) => PluginOptions['webpack'] & PluginOptions['rspack'] =
(context) => (compiler) => {
@@ -29,34 +45,106 @@ const xpackPlugin: (context: GlobalContext) => PluginOptions['webpack'] & Plugin
if (compiler.options.output?.path) {
context.bundler.outDir = compiler.options.output.path;
}
+
+ if (compiler.options.context) {
+ context.cwd = compiler.options.context;
+ }
};
// TODO: Add universal config report with list of plugins (names), loaders.
-export const getBundlerReportPlugins = (globalContext: GlobalContext): PluginOptions[] => {
+export const getBundlerReportPlugins = (context: GlobalContext): PluginOptions[] => {
+ const directories: Set = new Set();
+ const handleOutputOptions = (outputOptions: any) => {
+ if (!outputOptions) {
+ return;
+ }
+
+ if (outputOptions.dir) {
+ context.bundler.outDir = outputOptions.dir;
+ directories.add(outputOptions.dir);
+ } else if (outputOptions.file) {
+ context.bundler.outDir = path.dirname(outputOptions.file);
+ directories.add(outputOptions.dir);
+ }
+
+ // Vite has the "root" option we're using.
+ if (context.bundler.name === 'vite') {
+ return;
+ }
+
+ handleCwd(Array.from(directories), context);
+ };
+
+ const rollupPlugin: () => PluginOptions['rollup'] & PluginOptions['vite'] = () => {
+ return {
+ options(options) {
+ context.bundler.rawConfig = options;
+ if (options.input) {
+ if (Array.isArray(options.input)) {
+ for (const input of options.input) {
+ directories.add(path.dirname(input));
+ }
+ } else if (typeof options.input === 'object') {
+ for (const input of Object.values(options.input)) {
+ directories.add(path.dirname(input));
+ }
+ } else if (typeof options.input === 'string') {
+ directories.add(path.dirname(options.input));
+ } else {
+ throw new Error('Invalid input type');
+ }
+ }
+
+ if ('output' in options) {
+ handleOutputOptions(options.output);
+ }
+ },
+ outputOptions(options) {
+ handleOutputOptions(options);
+ },
+ };
+ };
+
const bundlerReportPlugin: PluginOptions = {
name: PLUGIN_NAME,
enforce: 'pre',
esbuild: {
setup(build) {
- globalContext.bundler.rawConfig = build.initialOptions;
+ context.bundler.rawConfig = build.initialOptions;
if (build.initialOptions.outdir) {
- globalContext.bundler.outDir = build.initialOptions.outdir;
+ context.bundler.outDir = build.initialOptions.outdir;
}
if (build.initialOptions.outfile) {
- globalContext.bundler.outDir = path.dirname(build.initialOptions.outfile);
+ context.bundler.outDir = path.dirname(build.initialOptions.outfile);
+ }
+
+ if (build.initialOptions.absWorkingDir) {
+ context.cwd = build.initialOptions.absWorkingDir;
}
// We force esbuild to produce its metafile.
build.initialOptions.metafile = true;
},
},
- webpack: xpackPlugin(globalContext),
- rspack: xpackPlugin(globalContext),
- // Vite and Rollup have the same API.
- vite: rollupPlugin(globalContext),
- rollup: rollupPlugin(globalContext),
+ webpack: xpackPlugin(context),
+ rspack: xpackPlugin(context),
+ // Vite and Rollup have (almost) the same API.
+ // They don't really support the CWD concept,
+ // so we have to compute it based on existing configurations.
+ // The basic idea is to compare input vs output and keep the common part of the paths.
+ vite: {
+ config(config) {
+ if (config.root) {
+ context.cwd = config.root;
+ } else {
+ handleCwd(Array.from(directories), context);
+ }
+ },
+ ...rollupPlugin(),
+ },
+ rollup: rollupPlugin(),
};
return [bundlerReportPlugin];
diff --git a/packages/plugins/error-tracking/README.md b/packages/plugins/error-tracking/README.md
new file mode 100644
index 000000000..756fdf881
--- /dev/null
+++ b/packages/plugins/error-tracking/README.md
@@ -0,0 +1,92 @@
+# Error Tracking Plugin
+
+Interact with Error Tracking directly from your build system.
+
+
+
+## Table of content
+
+
+
+
+- [Configuration](#configuration)
+- [Sourcemaps Upload](#sourcemaps-upload)
+ - [errorTracking.sourcemaps.bailOnError](#errortrackingsourcemapsbailonerror)
+ - [errorTracking.sourcemaps.dryRun](#errortrackingsourcemapsdryrun)
+ - [errorTracking.sourcemaps.intakeUrl](#errortrackingsourcemapsintakeurl)
+ - [errorTracking.sourcemaps.maxConcurrency](#errortrackingsourcemapsmaxconcurrency)
+ - [errorTracking.sourcemaps.minifiedPathPrefix](#errortrackingsourcemapsminifiedpathprefix)
+ - [errorTracking.sourcemaps.releaseVersion](#errortrackingsourcemapsreleaseversion)
+ - [errorTracking.sourcemaps.service](#errortrackingsourcemapsservice)
+
+
+## Configuration
+
+```ts
+errorTracking?: {
+ disabled?: boolean;
+ sourcemaps?: {
+ bailOnError?: boolean;
+ dryRun?: boolean;
+ intakeUrl?: string;
+ maxConcurrency?: number;
+ minifiedPathPrefix: string;
+ releaseVersion: string;
+ service: string;
+ };
+}
+```
+
+## Sourcemaps Upload
+
+Upload JavaScript sourcemaps to Datadog to un-minify your errors.
+
+> [!NOTE]
+> You can override the intake URL by setting the `DATADOG_SOURCEMAP_INTAKE_URL` environment variable (eg. `https://sourcemap-intake.datadoghq.com/v1/input`).
+> Or only the domain with the `DATADOG_SITE` environment variable (eg. `datadoghq.com`).
+
+### errorTracking.sourcemaps.bailOnError
+
+> default: `false`
+
+Should the upload of sourcemaps fail the build on first error?
+
+### errorTracking.sourcemaps.dryRun
+
+> default: `false`
+
+It will not upload the sourcemaps to Datadog, but will do everything else.
+
+### errorTracking.sourcemaps.intakeUrl
+
+> default: `https://sourcemap-intake.datadoghq.com/api/v2/srcmap`
+
+Against which endpoint do you want to upload the sourcemaps.
+
+### errorTracking.sourcemaps.maxConcurrency
+
+> default: `20`
+
+Number of concurrent upload to the API.
+
+### errorTracking.sourcemaps.minifiedPathPrefix
+
+> required
+
+Should be a prefix common to all your JS source files, depending on the URL they are served from.
+
+The prefix can be a full URL or an absolute path.
+
+Example: if you're uploading `dist/file.js` to `https://example.com/static/file.js`, you can use `minifiedPathPrefix: 'https://example.com/static/'` or `minifiedPathPrefix: '/static/'`.`minifiedPathPrefix: '/'` is a valid input when you upload JS at the root directory of the server.
+
+### errorTracking.sourcemaps.releaseVersion
+
+> required
+
+Is similar and will be used to match the `version` tag set on the RUM SDK.
+
+### errorTracking.sourcemaps.service
+
+> required
+
+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.
diff --git a/packages/plugins/error-tracking/package.json b/packages/plugins/error-tracking/package.json
new file mode 100644
index 000000000..2c19e7e22
--- /dev/null
+++ b/packages/plugins/error-tracking/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@dd/error-tracking-plugin",
+ "packageManager": "yarn@4.0.2",
+ "license": "MIT",
+ "private": true,
+ "author": "Datadog",
+ "description": "Interact with Error Tracking directly from your build system.",
+ "homepage": "https://github.com/DataDog/build-plugins/tree/main/packages/plugins/error-tracking#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/DataDog/build-plugins",
+ "directory": "packages/plugins/error-tracking"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./sourcemaps/*": "./src/sourcemaps/*.ts",
+ "./*": "./src/*.ts"
+ },
+ "scripts": {
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@dd/core": "workspace:*",
+ "chalk": "2.3.1",
+ "outdent": "0.8.0",
+ "p-queue": "6.6.2"
+ }
+}
diff --git a/packages/plugins/error-tracking/src/constants.ts b/packages/plugins/error-tracking/src/constants.ts
new file mode 100644
index 000000000..2dde88b79
--- /dev/null
+++ b/packages/plugins/error-tracking/src/constants.ts
@@ -0,0 +1,8 @@
+// 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 { PluginName } from '@dd/core/types';
+
+export const CONFIG_KEY = 'errorTracking' as const;
+export const PLUGIN_NAME: PluginName = 'datadog-error-tracking-plugin' as const;
diff --git a/packages/plugins/error-tracking/src/index.ts b/packages/plugins/error-tracking/src/index.ts
new file mode 100644
index 000000000..e2604e16f
--- /dev/null
+++ b/packages/plugins/error-tracking/src/index.ts
@@ -0,0 +1,51 @@
+// 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 { GlobalContext, GetPlugins, Logger } from '@dd/core/types';
+
+import { PLUGIN_NAME } from './constants';
+import { uploadSourcemaps } from './sourcemaps';
+import type {
+ OptionsWithErrorTracking,
+ ErrorTrackingOptions,
+ ErrorTrackingOptionsWithSourcemaps,
+} from './types';
+import { validateOptions } from './validate';
+
+export { CONFIG_KEY, PLUGIN_NAME } from './constants';
+
+export type types = {
+ // Add the types you'd like to expose here.
+ ErrorTrackingOptions: ErrorTrackingOptions;
+ OptionsWithErrorTracking: OptionsWithErrorTracking;
+};
+
+export const getPlugins: GetPlugins = (
+ opts: OptionsWithErrorTracking,
+ context: GlobalContext,
+ log: Logger,
+) => {
+ // Verify configuration.
+ const options = validateOptions(opts, log);
+ return [
+ {
+ name: PLUGIN_NAME,
+ enforce: 'post',
+ async writeBundle() {
+ if (options.disabled) {
+ return;
+ }
+
+ if (options.sourcemaps) {
+ // Need the "as" because Typescript doesn't understand that we've already checked for sourcemaps.
+ await uploadSourcemaps(
+ options as ErrorTrackingOptionsWithSourcemaps,
+ context,
+ log,
+ );
+ }
+ },
+ },
+ ];
+};
diff --git a/packages/plugins/rum/src/sourcemaps/files.ts b/packages/plugins/error-tracking/src/sourcemaps/files.ts
similarity index 91%
rename from packages/plugins/rum/src/sourcemaps/files.ts
rename to packages/plugins/error-tracking/src/sourcemaps/files.ts
index 30aa4b4c9..8df981071 100644
--- a/packages/plugins/rum/src/sourcemaps/files.ts
+++ b/packages/plugins/error-tracking/src/sourcemaps/files.ts
@@ -6,12 +6,12 @@ import type { GlobalContext } from '@dd/core/types';
import chalk from 'chalk';
import path from 'path';
-import type { RumSourcemapsOptionsWithDefaults, Sourcemap } from '../types';
+import type { SourcemapsOptionsWithDefaults, Sourcemap } from '../types';
type PartialSourcemap = Pick;
const decomposePath = (
- options: RumSourcemapsOptionsWithDefaults,
+ options: SourcemapsOptionsWithDefaults,
context: GlobalContext,
sourcemapFilePath: string,
): PartialSourcemap => {
@@ -33,7 +33,7 @@ const decomposePath = (
};
export const getSourcemapsFiles = (
- options: RumSourcemapsOptionsWithDefaults,
+ options: SourcemapsOptionsWithDefaults,
context: GlobalContext,
): Sourcemap[] => {
if (!context.build.outputs || context.build.outputs.length === 0) {
diff --git a/packages/plugins/rum/src/sourcemaps/index.ts b/packages/plugins/error-tracking/src/sourcemaps/index.ts
similarity index 90%
rename from packages/plugins/rum/src/sourcemaps/index.ts
rename to packages/plugins/error-tracking/src/sourcemaps/index.ts
index 732a3b3c2..4101bdcdd 100644
--- a/packages/plugins/rum/src/sourcemaps/index.ts
+++ b/packages/plugins/error-tracking/src/sourcemaps/index.ts
@@ -6,13 +6,13 @@ import type { Logger, GlobalContext } from '@dd/core/types';
import chalk from 'chalk';
import { outdent } from 'outdent';
-import type { RumOptionsWithSourcemaps } from '../types';
+import type { ErrorTrackingOptionsWithSourcemaps } from '../types';
import { getSourcemapsFiles } from './files';
import { sendSourcemaps } from './sender';
export const uploadSourcemaps = async (
- options: RumOptionsWithSourcemaps,
+ options: ErrorTrackingOptionsWithSourcemaps,
context: GlobalContext,
log: Logger,
) => {
diff --git a/packages/plugins/rum/src/sourcemaps/payload.ts b/packages/plugins/error-tracking/src/sourcemaps/payload.ts
similarity index 99%
rename from packages/plugins/rum/src/sourcemaps/payload.ts
rename to packages/plugins/error-tracking/src/sourcemaps/payload.ts
index 7c83c5512..08b201389 100644
--- a/packages/plugins/rum/src/sourcemaps/payload.ts
+++ b/packages/plugins/error-tracking/src/sourcemaps/payload.ts
@@ -66,6 +66,7 @@ export const prefixRepeat = (filePath: string, prefix: string): string => {
let result = '';
for (let i = 0; i < prefixParts.length; i += 1) {
+ // TODO: Check compatibility with Windows paths.
const partialPrefix = prefixParts.slice(-i).join('/');
if (normalizedPath.startsWith(partialPrefix)) {
result = partialPrefix;
diff --git a/packages/plugins/rum/src/sourcemaps/sender.ts b/packages/plugins/error-tracking/src/sourcemaps/sender.ts
similarity index 97%
rename from packages/plugins/rum/src/sourcemaps/sender.ts
rename to packages/plugins/error-tracking/src/sourcemaps/sender.ts
index 05c3c1442..e33debcdd 100644
--- a/packages/plugins/rum/src/sourcemaps/sender.ts
+++ b/packages/plugins/error-tracking/src/sourcemaps/sender.ts
@@ -12,7 +12,7 @@ import { Readable } from 'stream';
import type { Gzip } from 'zlib';
import { createGzip } from 'zlib';
-import type { RumSourcemapsOptionsWithDefaults, Sourcemap } from '../types';
+import type { SourcemapsOptionsWithDefaults, Sourcemap } from '../types';
import type { LocalAppendOptions, Metadata, MultipartFileValue, Payload } from './payload';
import { getPayload } from './payload';
@@ -78,7 +78,7 @@ export const getData =
export const upload = async (
payloads: Payload[],
- options: RumSourcemapsOptionsWithDefaults,
+ options: SourcemapsOptionsWithDefaults,
context: GlobalContext,
log: Logger,
) => {
@@ -153,7 +153,7 @@ export const upload = async (
export const sendSourcemaps = async (
sourcemaps: Sourcemap[],
- options: RumSourcemapsOptionsWithDefaults,
+ options: SourcemapsOptionsWithDefaults,
context: GlobalContext,
log: Logger,
) => {
diff --git a/packages/plugins/error-tracking/src/types.ts b/packages/plugins/error-tracking/src/types.ts
new file mode 100644
index 000000000..3319b5740
--- /dev/null
+++ b/packages/plugins/error-tracking/src/types.ts
@@ -0,0 +1,48 @@
+// 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 { GetPluginsOptions } from '@dd/core/types';
+
+import type { CONFIG_KEY } from './constants';
+
+export type MinifiedPathPrefix = `http://${string}` | `https://${string}` | `/${string}`;
+
+export type SourcemapsOptions = {
+ bailOnError?: boolean;
+ dryRun?: boolean;
+ intakeUrl?: string;
+ maxConcurrency?: number;
+ minifiedPathPrefix: MinifiedPathPrefix;
+ releaseVersion: string;
+ service: string;
+};
+
+export type SourcemapsOptionsWithDefaults = Required;
+
+export type ErrorTrackingOptions = {
+ disabled?: boolean;
+ sourcemaps?: SourcemapsOptions;
+};
+
+export type ErrorTrackingOptionsWithDefaults = {
+ disabled?: boolean;
+ sourcemaps?: SourcemapsOptionsWithDefaults;
+};
+
+export type ErrorTrackingOptionsWithSourcemaps = {
+ disabled?: boolean;
+ sourcemaps: SourcemapsOptionsWithDefaults;
+};
+
+export interface OptionsWithErrorTracking extends GetPluginsOptions {
+ [CONFIG_KEY]: ErrorTrackingOptions;
+}
+
+export type Sourcemap = {
+ minifiedFilePath: string;
+ minifiedPathPrefix: MinifiedPathPrefix;
+ minifiedUrl: string;
+ relativePath: string;
+ sourcemapFilePath: string;
+};
diff --git a/packages/plugins/error-tracking/src/validate.ts b/packages/plugins/error-tracking/src/validate.ts
new file mode 100644
index 000000000..ec7061502
--- /dev/null
+++ b/packages/plugins/error-tracking/src/validate.ts
@@ -0,0 +1,117 @@
+// 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';
+
+import { CONFIG_KEY, PLUGIN_NAME } from './constants';
+import type {
+ OptionsWithErrorTracking,
+ ErrorTrackingOptions,
+ ErrorTrackingOptionsWithDefaults,
+ SourcemapsOptionsWithDefaults,
+} from './types';
+
+export const defaultIntakeUrl = `https://sourcemap-intake.${process.env.DATADOG_SITE || 'datadoghq.com'}/api/v2/srcmap`;
+
+// Deal with validation and defaults here.
+export const validateOptions = (
+ config: Partial,
+ log: Logger,
+): ErrorTrackingOptionsWithDefaults => {
+ const errors: string[] = [];
+
+ // Validate and add defaults sub-options.
+ const sourcemapsResults = validateSourcemapsOptions(config);
+ errors.push(...sourcemapsResults.errors);
+
+ // Throw if there are any errors.
+ if (errors.length) {
+ log.error(`\n - ${errors.join('\n - ')}`);
+ throw new Error(`Invalid configuration for ${PLUGIN_NAME}.`);
+ }
+
+ // Build the final configuration.
+ const toReturn: ErrorTrackingOptionsWithDefaults = {
+ ...config[CONFIG_KEY],
+ sourcemaps: undefined,
+ };
+
+ // Fill in the defaults.
+ if (sourcemapsResults.config) {
+ toReturn.sourcemaps = sourcemapsResults.config;
+ }
+
+ return toReturn;
+};
+
+type ToReturn = {
+ errors: string[];
+ config?: T;
+};
+
+const validateMinifiedPathPrefix = (minifiedPathPrefix: string): boolean => {
+ let host;
+ try {
+ const objUrl = new URL(minifiedPathPrefix!);
+ host = objUrl.host;
+ } catch {
+ // Do nothing.
+ }
+
+ if (!host && !minifiedPathPrefix!.startsWith('/')) {
+ return false;
+ }
+
+ return true;
+};
+
+export const validateSourcemapsOptions = (
+ config: Partial,
+): ToReturn => {
+ const red = chalk.bold.red;
+ const validatedOptions: ErrorTrackingOptions = config[CONFIG_KEY] || {};
+ const toReturn: ToReturn = {
+ errors: [],
+ };
+
+ if (validatedOptions.sourcemaps) {
+ // Validate the configuration.
+ if (!validatedOptions.sourcemaps.releaseVersion) {
+ toReturn.errors.push(`${red('sourcemaps.releaseVersion')} is required.`);
+ }
+ if (!validatedOptions.sourcemaps.service) {
+ toReturn.errors.push(`${red('sourcemaps.service')} is required.`);
+ }
+ if (!validatedOptions.sourcemaps.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 '/'.`,
+ );
+ }
+ }
+
+ // Add the defaults.
+ const sourcemapsWithDefaults: SourcemapsOptionsWithDefaults = {
+ bailOnError: false,
+ dryRun: false,
+ maxConcurrency: 20,
+ intakeUrl:
+ process.env.DATADOG_SOURCEMAP_INTAKE_URL ||
+ validatedOptions.sourcemaps.intakeUrl ||
+ defaultIntakeUrl,
+ ...validatedOptions.sourcemaps,
+ };
+
+ // Save the config.
+ toReturn.config = sourcemapsWithDefaults;
+ }
+
+ return toReturn;
+};
diff --git a/packages/plugins/error-tracking/tsconfig.json b/packages/plugins/error-tracking/tsconfig.json
new file mode 100644
index 000000000..6c1d3065e
--- /dev/null
+++ b/packages/plugins/error-tracking/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "baseUrl": "./",
+ "rootDir": "./",
+ "outDir": "./dist"
+ },
+ "include": ["**/*"],
+ "exclude": ["dist", "node_modules"]
+}
\ No newline at end of file
diff --git a/packages/plugins/git/README.md b/packages/plugins/git/README.md
index a6d964cca..132b5b8ee 100644
--- a/packages/plugins/git/README.md
+++ b/packages/plugins/git/README.md
@@ -18,4 +18,4 @@ Adds repository data to the global context from the `buildStart` hook.
```
> [!NOTE]
-> This won't be added if `options.disabledGit = true` or `options.rum.sourcemaps.disabledGit = true`.
+> This won't be added if `options.disabledGit = true` or `options.errorTracking.sourcemaps.disabledGit = true`.
diff --git a/packages/plugins/git/package.json b/packages/plugins/git/package.json
index 3a0bf5c86..b47e000b2 100644
--- a/packages/plugins/git/package.json
+++ b/packages/plugins/git/package.json
@@ -4,12 +4,12 @@
"license": "MIT",
"private": true,
"author": "Datadog",
- "description": "Interact with our Real User Monitoring product (RUM) in Datadog directly from your build system.",
- "homepage": "https://github.com/DataDog/build-plugins/tree/main/packages/features/build-report#readme",
+ "description": "",
+ "homepage": "https://github.com/DataDog/build-plugins/tree/main/packages/plugins/git#readme",
"repository": {
"type": "git",
"url": "https://github.com/DataDog/build-plugins",
- "directory": "packages/features/build-report"
+ "directory": "packages/plugins/git"
},
"exports": {
".": "./src/index.ts",
diff --git a/packages/plugins/git/src/index.ts b/packages/plugins/git/src/index.ts
index 6c7c29b79..26c20f024 100644
--- a/packages/plugins/git/src/index.ts
+++ b/packages/plugins/git/src/index.ts
@@ -16,7 +16,8 @@ export const getGitPlugins = (options: Options, context: GlobalContext): PluginO
async buildStart() {
// Verify that we should get the git information based on the options.
// Only get git information if sourcemaps are enabled and git is not disabled.
- const shouldGetGitInfo = options.rum?.sourcemaps && options.disableGit !== true;
+ const shouldGetGitInfo =
+ options.errorTracking?.sourcemaps && options.disableGit !== true;
if (!shouldGetGitInfo) {
return;
diff --git a/packages/plugins/injection/README.md b/packages/plugins/injection/README.md
index 555c4e653..67316b895 100644
--- a/packages/plugins/injection/README.md
+++ b/packages/plugins/injection/README.md
@@ -1,14 +1,26 @@
# Injection Plugin
-This is used to prepend some code to the produced bundle.
-Particularly useful if you want to share some global context, or to automatically inject some SDK.
+This is used to inject some code to the produced bundle.
+Particularly useful :
+- to share some global context.
+- to automatically inject some SDK.
+- to initialise some global dependencies.
+- ...
It gives you access to the `context.inject()` function.
All the injections will be resolved during the `buildStart` hook,
-so you'll have to have submitted your injection prior to that.
+so you'll have to "submit" your injection(s) prior to that.
Ideally, you'd submit it during your plugin's initialization.
+There are three positions to inject content:
+
+- `InjectPosition.START`: Added at the very beginning of the bundle, outside any closure.
+- `InjectPosition.MIDDLE`: Added at the begining of the entry file, within the context of the bundle.
+- `InjectPosition.END`: Added at the very end of the bundle, outside any closure.
+
+There are three types of injection:
+
## Distant file
You can give it a distant file.
@@ -18,6 +30,7 @@ Be mindful that a 5s timeout is enforced.
context.inject({
type: 'file',
value: 'https://example.com/my_file.js',
+ position: InjectPosition.START,
});
```
@@ -31,6 +44,7 @@ Remember that the plugins are also bundled before distribution.
context.inject({
type: 'file',
value: path.resolve(__dirname, '../my_file.js'),
+ position: InjectPosition.END,
});
```
@@ -43,5 +57,6 @@ Be mindful that the code needs to be executable, or the plugins will crash.
context.inject({
type: 'code',
value: 'console.log("My un-invasive code");',
+ position: InjectPosition.MIDDLE,
});
```
diff --git a/packages/plugins/injection/src/constants.ts b/packages/plugins/injection/src/constants.ts
index 0d4746d5f..72561fa79 100644
--- a/packages/plugins/injection/src/constants.ts
+++ b/packages/plugins/injection/src/constants.ts
@@ -2,6 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-export const PREPARATION_PLUGIN_NAME = 'datadog-injection-preparation-plugin';
export const PLUGIN_NAME = 'datadog-injection-plugin';
export const DISTANT_FILE_RX = /^https?:\/\//;
+export const BEFORE_INJECTION = `// begin injection by Datadog build plugins`;
+export const AFTER_INJECTION = `// end injection by Datadog build plugins`;
diff --git a/packages/plugins/injection/src/esbuild.ts b/packages/plugins/injection/src/esbuild.ts
new file mode 100644
index 000000000..2097cf388
--- /dev/null
+++ b/packages/plugins/injection/src/esbuild.ts
@@ -0,0 +1,122 @@
+// 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 { INJECTED_FILE } from '@dd/core/constants';
+import { getEsbuildEntries, getUniqueId, outputFile, rm } from '@dd/core/helpers';
+import type { Logger, PluginOptions, GlobalContext, ResolvedEntry } from '@dd/core/types';
+import { InjectPosition } from '@dd/core/types';
+import { getAbsolutePath } from '@dd/internal-build-report-plugin/helpers';
+import fsp from 'fs/promises';
+import path from 'path';
+
+import { PLUGIN_NAME } from './constants';
+import { getContentToInject } from './helpers';
+import type { ContentsToInject } from './types';
+
+export const getEsbuildPlugin = (
+ log: Logger,
+ context: GlobalContext,
+ contentsToInject: ContentsToInject,
+): PluginOptions['esbuild'] => ({
+ setup(build) {
+ const { onStart, onLoad, onEnd, esbuild, initialOptions } = build;
+ const entries: ResolvedEntry[] = [];
+ const filePath = `${getUniqueId()}.${InjectPosition.MIDDLE}.${INJECTED_FILE}.js`;
+ const absoluteFilePath = path.resolve(context.bundler.outDir, filePath);
+ const injectionRx = new RegExp(`${filePath}$`);
+
+ // InjectPosition.MIDDLE
+ // Inject the file in the build using the "inject" option.
+ // NOTE: This is made "safer" for sub-builds by actually creating the file.
+ initialOptions.inject = initialOptions.inject || [];
+ initialOptions.inject.push(absoluteFilePath);
+
+ onStart(async () => {
+ // Get all the entry points for later reference.
+ entries.push(...(await getEsbuildEntries(build, context, log)));
+
+ // Remove our injected file from the config, so we reduce our chances to leak our changes.
+ initialOptions.inject =
+ initialOptions.inject?.filter((file) => file !== absoluteFilePath) || [];
+
+ try {
+ // Create the MIDDLE file because esbuild will crash if it doesn't exist.
+ // It seems to load entries outside of the onLoad hook once.
+ await outputFile(absoluteFilePath, '');
+ } catch (e: any) {
+ log.error(`Could not create the files: ${e.message}`);
+ }
+ });
+
+ onLoad(
+ {
+ filter: injectionRx,
+ namespace: PLUGIN_NAME,
+ },
+ async () => {
+ const content = getContentToInject(contentsToInject[InjectPosition.MIDDLE]);
+
+ // Safe to delete the temp file now, the hook will take over.
+ await rm(absoluteFilePath);
+
+ return {
+ // We can't use an empty string otherwise esbuild will crash.
+ contents: content || ' ',
+ // Resolve the imports from the project's root.
+ resolveDir: context.cwd,
+ loader: 'js',
+ };
+ },
+ );
+
+ // InjectPosition.START and InjectPosition.END
+ onEnd(async (result) => {
+ if (!result.metafile) {
+ log.warn('Missing metafile from build result.');
+ return;
+ }
+
+ const banner = getContentToInject(contentsToInject[InjectPosition.BEFORE]);
+ const footer = getContentToInject(contentsToInject[InjectPosition.AFTER]);
+
+ if (!banner && !footer) {
+ // Nothing to inject.
+ return;
+ }
+
+ // Rewrite outputs with the injected content.
+ // Only keep the entry files.
+ const outputs: string[] = Object.entries(result.metafile.outputs)
+ .map(([p, o]) => {
+ const entryPoint = o.entryPoint;
+ if (!entryPoint) {
+ return;
+ }
+
+ const entry = entries.find((e) => e.resolved.endsWith(entryPoint));
+ if (!entry) {
+ return;
+ }
+
+ return getAbsolutePath(context.cwd, p);
+ })
+ .filter(Boolean) as string[];
+
+ // Write the content.
+ const proms = outputs.map(async (output) => {
+ const source = await fsp.readFile(output, 'utf-8');
+ const data = await esbuild.transform(source, {
+ loader: 'default',
+ banner,
+ footer,
+ });
+
+ // FIXME: Handle sourcemaps.
+ await fsp.writeFile(output, data.code);
+ });
+
+ await Promise.all(proms);
+ });
+ },
+});
diff --git a/packages/plugins/injection/src/helpers.ts b/packages/plugins/injection/src/helpers.ts
index d73871d1a..ed5b61dbe 100644
--- a/packages/plugins/injection/src/helpers.ts
+++ b/packages/plugins/injection/src/helpers.ts
@@ -4,20 +4,30 @@
import { doRequest, truncateString } from '@dd/core/helpers';
import type { Logger, ToInjectItem } from '@dd/core/types';
+import { InjectPosition } from '@dd/core/types';
import { getAbsolutePath } from '@dd/internal-build-report-plugin/helpers';
import { readFile } from 'fs/promises';
-import { DISTANT_FILE_RX } from './constants';
+import { AFTER_INJECTION, BEFORE_INJECTION, DISTANT_FILE_RX } from './constants';
+import type { ContentsToInject } from './types';
const MAX_TIMEOUT_IN_MS = 5000;
+export const getInjectedValue = async (item: ToInjectItem): Promise => {
+ if (typeof item.value === 'function') {
+ return item.value();
+ }
+
+ return item.value;
+};
+
export const processDistantFile = async (
- item: ToInjectItem,
+ url: string,
timeout: number = MAX_TIMEOUT_IN_MS,
): Promise => {
let timeoutId: ReturnType | undefined;
return Promise.race([
- doRequest({ url: item.value }).finally(() => {
+ doRequest({ url }).finally(() => {
if (timeout) {
clearTimeout(timeoutId);
}
@@ -30,32 +40,36 @@ export const processDistantFile = async (
]);
};
-export const processLocalFile = async (item: ToInjectItem): Promise => {
- const absolutePath = getAbsolutePath(process.cwd(), item.value);
+export const processLocalFile = async (
+ filepath: string,
+ cwd: string = process.cwd(),
+): Promise => {
+ const absolutePath = getAbsolutePath(cwd, filepath);
return readFile(absolutePath, { encoding: 'utf-8' });
};
-export const processRawCode = async (item: ToInjectItem): Promise => {
- // TODO: Confirm the code actually executes without errors.
- return item.value;
-};
-
-export const processItem = async (item: ToInjectItem, log: Logger): Promise => {
+export const processItem = async (
+ item: ToInjectItem,
+ log: Logger,
+ cwd: string = process.cwd(),
+): Promise => {
let result: string;
+ const value = await getInjectedValue(item);
try {
if (item.type === 'file') {
- if (item.value.match(DISTANT_FILE_RX)) {
- result = await processDistantFile(item);
+ if (value.match(DISTANT_FILE_RX)) {
+ result = await processDistantFile(value);
} else {
- result = await processLocalFile(item);
+ result = await processLocalFile(value, cwd);
}
} else if (item.type === 'code') {
- result = await processRawCode(item);
+ // TODO: Confirm the code actually executes without errors.
+ result = value;
} else {
throw new Error(`Invalid item type "${item.type}", only accepts "code" or "file".`);
}
} catch (error: any) {
- const itemId = `${item.type} - ${truncateString(item.value)}`;
+ const itemId = `${item.type} - ${truncateString(value)}`;
if (item.fallback) {
// In case of any error, we'll fallback to next item in queue.
log.warn(`Fallback for "${itemId}": ${error.toString()}`);
@@ -71,15 +85,46 @@ export const processItem = async (item: ToInjectItem, log: Logger): Promise,
log: Logger,
-): Promise => {
- const proms: (Promise | string)[] = [];
+ cwd: string = process.cwd(),
+): Promise